diff --git a/containers/libreportal/frontend/features/dashboard/index.js b/containers/libreportal/frontend/features/dashboard/index.js new file mode 100644 index 0000000..19e978a --- /dev/null +++ b/containers/libreportal/frontend/features/dashboard/index.js @@ -0,0 +1,42 @@ +// features/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('/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)); + }, + + async unmount() { + // Nothing this view owns is destroyable from here: the dashboard has no + // handler-newed controller and no system-loader singleton to tear down. + // The pending reload timer is cancelled via the ctx.sub() above (ctx.teardown). + // The 1 Hz LiveSystem SSE sub (attachDashboardLive) self-releases on its + // next sample once the dashboard DOM is gone, and the 1 s update countdown + // is a module-private interval with no exported stopper (pre-existing; it + // self-clears on the next dashboard mount). Both are noted for the Phase 5 + // dashboard cleanup; neither is reachable here. + }, +}); diff --git a/containers/libreportal/frontend/features/tasks/index.js b/containers/libreportal/frontend/features/tasks/index.js new file mode 100644 index 0000000..5899c18 --- /dev/null +++ b/containers/libreportal/frontend/features/tasks/index.js @@ -0,0 +1,45 @@ +// features/tasks/index.js — the Tasks page as a feature module. +// +// window.tasksManager is a system-loader singleton (its "task-system" component +// loads all task scripts, starts the shared SSE bus, then news TasksManager). +// So mount() does NOT new it or load scripts — it renders the fragment and +// re-inits the view, exactly like the old handleTasks(). unmount() releases +// only this view's per-mount leaks (the 30s auto-refresh interval + open log +// streams); it must never stop the shared taskEventBus or null the singleton. +LP.features.register({ + id: 'tasks', + routes: ['/tasks', '/tasks*'], + + async mount(ctx) { + const html = await ctx.loadFragment('/html/tasks-content.html'); + ctx.setContent(html, 'Tasks'); + + if (window.tasksManager) { + await window.tasksManager.init(); + } else { + // Don't throw — matches handleTasks: the page still renders, task + // functionality is just limited until the task-system component is ready. + console.warn('TasksManager not available yet, task functionality will be limited'); + } + }, + + async unmount() { + const tm = window.tasksManager; + // The one per-view leak init() opens: the 30s auto-refresh interval stored + // on the singleton. Each init() recreates it, so clearing here is idempotent. + if (tm && tm.refreshInterval) { + clearInterval(tm.refreshInterval); + tm.refreshInterval = null; + } + // Stop any open per-task log streams this view started (each removes its own + // SSE listeners + map entry). Snapshot keys first — stopLogStreaming mutates + // the map. Does NOT touch the shared bus. + if (tm && tm.activeLogStreams && typeof tm.stopLogStreaming === 'function') { + for (const id of Array.from(tm.activeLogStreams.keys())) { + try { tm.stopLogStreaming(id); } catch (_) {} + } + } + // DO NOT: stop window.taskEventBus (shared SSE singleton), remove the + // singleton's once-bound task listeners, or null window.tasksManager. + }, +}); diff --git a/containers/libreportal/frontend/index.html b/containers/libreportal/frontend/index.html index 3f1890d..66021e9 100755 --- a/containers/libreportal/frontend/index.html +++ b/containers/libreportal/frontend/index.html @@ -111,7 +111,9 @@ + +