fix(#183): Browser-Speicher — Modal-Chart-destroy, Force-Sim-stop, Polling-Pause

- **Zeitreihe-Modal-Chart**: vor Re-Render alte Chart.destroy(),
  bei closeModal() + Escape ebenfalls. Vorher akkumulierten sich
  Chart-Instanzen bei jedem Modal-Open (Listener bleiben hängen).
- **Cluster-Force-Sim**: `_currentForceSim` global gemerkt, beim
  Verlassen Detail-View (showList) und vor neuem renderClusterGraph
  per sim.stop() beendet. Vorher liefen tick-Listener weiter, hielten
  nodes/links/SVG-Refs.
- **Polling-Pause auf visibilitychange** in queue_widget, admin_stand
  und admin_queue. Wenn Tab versteckt → clearInterval, beim Show wieder
  starten. Spart CPU/Akku + verhindert verwaiste Polls.

Refs: #183. Heap-Snapshot-Verifikation in Folge-Schritt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-07 09:01:48 +02:00
parent d8999f8a64
commit 481a791934
5 changed files with 63 additions and 7 deletions

View File

@ -86,8 +86,17 @@
})
.catch(function () { /* still */ });
}
// erster Aufruf direkt + danach alle 5 s
// Polling: erster Aufruf + alle 5 s. Bei Tab-Wechsel/Hide pausieren,
// damit der Browser CPU/Akku spart und keine Listener akkumuliert (#183).
poll();
setInterval(poll, 5000);
let _qwInterval = setInterval(poll, 5000);
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
if (_qwInterval) { clearInterval(_qwInterval); _qwInterval = null; }
} else if (!_qwInterval) {
poll();
_qwInterval = setInterval(poll, 5000);
}
});
})();
</script>

View File

@ -217,6 +217,15 @@ async function refresh() {
}
refresh();
setInterval(refresh, 5000);
let _queueInterval = setInterval(refresh, 5000);
// Pause-Polling wenn Tab versteckt (#183).
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
if (_queueInterval) { clearInterval(_queueInterval); _queueInterval = null; }
} else if (!_queueInterval) {
refresh();
_queueInterval = setInterval(refresh, 5000);
}
});
</script>
{% endblock %}

View File

@ -282,6 +282,15 @@ async function loadStand() {
}
loadStand();
setInterval(loadStand, 30000);
let _standInterval = setInterval(loadStand, 30000);
// Pause-Polling wenn Tab versteckt (#183).
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
if (_standInterval) { clearInterval(_standInterval); _standInterval = null; }
} else if (!_standInterval) {
loadStand();
_standInterval = setInterval(loadStand, 30000);
}
});
</script>
{% endblock %}

View File

@ -665,8 +665,12 @@ async function showZeitreihe(bundesland, partei) {
'</tbody></table>';
if (window.Chart) {
// Modal-Chart-Tracking: vor Re-Render alte Instanz zerstören.
if (window._zeitreiheModalChart) {
try { window._zeitreiheModalChart.destroy(); } catch (_) {}
}
const ctx = document.getElementById('zeitreihe-chart');
new Chart(ctx, {
window._zeitreiheModalChart = new Chart(ctx, {
type: 'line',
data: {
labels: z.wahlperioden.map(r => 'WP ' + r.wp),
@ -700,13 +704,23 @@ async function showZeitreihe(bundesland, partei) {
}
}
function _destroyModalChart() {
if (window._zeitreiheModalChart) {
try { window._zeitreiheModalChart.destroy(); } catch (_) {}
window._zeitreiheModalChart = null;
}
}
function closeModal(ev) {
if (!ev || ev.target.id === 'modal-backdrop') {
document.getElementById('modal-backdrop').classList.remove('show');
_destroyModalChart();
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') document.getElementById('modal-backdrop').classList.remove('show');
if (e.key === 'Escape') {
document.getElementById('modal-backdrop').classList.remove('show');
_destroyModalChart();
}
});
// ─── Stimmverhalten × Gemeinwohl ────────────────────────────────────────────

View File

@ -267,7 +267,21 @@ function showCluster(idx) {
renderClusterGraph(cl);
}
// Aktuelle Force-Simulation — beim Verlassen der Detail-View stoppen,
// sonst läuft Tick-Listener weiter und akkumuliert Memory (#183).
let _currentForceSim = null;
function _stopForceSim() {
if (_currentForceSim) {
try { _currentForceSim.stop(); } catch (_) {}
_currentForceSim = null;
}
const container = document.getElementById('cluster-graph');
if (container) container.innerHTML = '';
}
function renderClusterGraph(cl) {
_stopForceSim();
const container = document.getElementById('cluster-graph');
if (!container || typeof d3 === 'undefined') return;
const rawNodes = cl.nodes || [];
@ -309,7 +323,7 @@ function renderClusterGraph(cl) {
.attr('width', width).attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`);
const sim = d3.forceSimulation(nodes)
const sim = (_currentForceSim = d3.forceSimulation(nodes))
.force('link', d3.forceLink(links).id(d => d.id)
.distance(l => (1 - (l.sim || 0)) * 220 + 50)
.strength(l => l.sim || 0.5))
@ -358,6 +372,7 @@ function renderClusterGraph(cl) {
}
function showList() {
_stopForceSim();
document.getElementById('cluster-list').style.display = '';
document.getElementById('cluster-detail').style.display = 'none';
}