The teardown audit found the backup-stacking leak class across 4 more feature modules (12 confirmed leaks); unmount() left document/window listeners, intervals, and SSE subscriptions firing on stale controllers after navigation: - admin: overview/ssh/peers/system each leaked a document click listener -> AbortController + dispose() per page; admin unmount() aborts each. - dashboard: the 1 Hz update-countdown interval + the LiveSystem view sub -> stopUpdateCountdown()/detachDashboardLive(), registered via ctx.sub(). - tasks: constructor-started global live-log poller (discarded handle) -> stored + idempotent + cleared on unmount + re-armed on mount; per-task monitorTask window listeners + interval -> tracked in a map, released on unmount. - apps: app-tabbed reconcile setTimeout loop + watchdog window/document listeners + popstate -> per-instance AbortController + dispose() that clears the timer, resets the guards, and unloads the active tab's Services intervals + log SSE. All mirror the kernel's MountContext teardown discipline. 12 files, all pass node --check. Backup (fixed earlier) re-confirmed clean by the audit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
45 lines
2.2 KiB
JavaScript
45 lines
2.2 KiB
JavaScript
// components/dashboard/index.js — the landing Dashboard as a feature module.
|
|
//
|
|
// dashboard.js exposes bare global functions (loadInstalledApps,
|
|
// loadDashboardData live in dashboard.js / data-loader.js, both eager core
|
|
// scripts) — there is no controller class to lazy-load, so scripts is empty.
|
|
// system-loader's 'dashboard' component already ran initializeData()/
|
|
// loadSystemInfo()/setupEventListeners() once at boot; mount() only does the
|
|
// per-navigation refresh (mirrors the old handleDashboard + the navigate()
|
|
// data-reload special-case, which has been removed from spa.js so the reload
|
|
// now fires exactly once here).
|
|
LP.features.register({
|
|
id: 'dashboard',
|
|
routes: ['/', '/dashboard'],
|
|
|
|
async mount(ctx) {
|
|
const html = await ctx.loadFragment('/components/dashboard/html/dashboard-content.html');
|
|
ctx.setContent(html, 'Dashboard');
|
|
|
|
// Render the installed-apps icon grid (handleDashboard's only post-render call).
|
|
if (typeof loadInstalledApps === 'function') loadInstalledApps();
|
|
|
|
// Repaint the stat cards / disk donut + (re)wire the 1 Hz live SSE. The
|
|
// 100 ms cushion lets the freshly-injected DOM settle (loadSystemInfo also
|
|
// guards on element presence). ctx.sub() cancels it if we navigate away
|
|
// before it fires.
|
|
const reloadTimer = setTimeout(() => {
|
|
if (typeof loadDashboardData === 'function') loadDashboardData();
|
|
}, 100);
|
|
ctx.sub(() => clearTimeout(reloadTimer));
|
|
|
|
// Stop the 1 Hz update-countdown interval + drop this view's LiveSystem
|
|
// subscription on navigation away — both are module-private in data-loader.js
|
|
// and otherwise outlive the page (the leak class). ctx.teardown runs these.
|
|
ctx.sub(() => { if (typeof stopUpdateCountdown === 'function') stopUpdateCountdown(); });
|
|
ctx.sub(() => { if (typeof detachDashboardLive === 'function') detachDashboardLive(); });
|
|
},
|
|
|
|
async unmount() {
|
|
// This view's teardown is all registered via ctx.sub() in mount() and runs
|
|
// in ctx.teardown(): the pending reload timer, the 1 Hz update-countdown
|
|
// interval (stopUpdateCountdown), and this view's LiveSystem subscription
|
|
// (detachDashboardLive). No handler-newed controller or singleton to tear down.
|
|
},
|
|
});
|