Compare commits
2 Commits
429ec419cf
...
7d15fa2a22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d15fa2a22 | ||
|
|
306e6223c0 |
@ -49,6 +49,12 @@ LP.features.register({
|
|||||||
// Drop OverviewPage's task-refresh registration so a finished verify/update
|
// Drop OverviewPage's task-refresh registration so a finished verify/update
|
||||||
// task doesn't repaint a torn-down board.
|
// task doesn't repaint a torn-down board.
|
||||||
try { ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('overview-page'); } catch (_) {}
|
try { ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('overview-page'); } catch (_) {}
|
||||||
|
// Each sub-page binds a document-level click listener; nulling the global
|
||||||
|
// alone leaks it (the backup-stacking bug class), so abort each first.
|
||||||
|
try { window.overviewPage && window.overviewPage.dispose && window.overviewPage.dispose(); } catch (_) {}
|
||||||
|
try { window.systemPage && window.systemPage.dispose && window.systemPage.dispose(); } catch (_) {}
|
||||||
|
try { window.sshPage && window.sshPage.dispose && window.sshPage.dispose(); } catch (_) {}
|
||||||
|
try { window.peersPage && window.peersPage.dispose && window.peersPage.dispose(); } catch (_) {}
|
||||||
window.overviewPage = null;
|
window.overviewPage = null;
|
||||||
window.systemPage = null;
|
window.systemPage = null;
|
||||||
window.sshPage = null;
|
window.sshPage = null;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ class OverviewPage {
|
|||||||
this.rootId = rootId;
|
this.rootId = rootId;
|
||||||
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
||||||
this._bound = false;
|
this._bound = false;
|
||||||
|
this._ac = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
root() { return document.getElementById(this.rootId); }
|
root() { return document.getElementById(this.rootId); }
|
||||||
@ -52,7 +53,7 @@ class OverviewPage {
|
|||||||
if (go) { this.go(go.dataset.adminGo); return; }
|
if (go) { this.go(go.dataset.adminGo); return; }
|
||||||
if (e.target.closest('[data-admin-update]')) { this.runUpdate(); return; }
|
if (e.target.closest('[data-admin-update]')) { this.runUpdate(); return; }
|
||||||
if (e.target.closest('[data-admin-verify]')) { this.runVerify(); return; }
|
if (e.target.closest('[data-admin-verify]')) { this.runVerify(); return; }
|
||||||
});
|
}, { signal: this._ac.signal });
|
||||||
// When a verify or update task finishes, re-read the integrity status
|
// When a verify or update task finishes, re-read the integrity status
|
||||||
// and re-render so the badge reflects reality without a manual reload.
|
// and re-render so the badge reflects reality without a manual reload.
|
||||||
// Registered with the task-refresh coordinator (single source of truth).
|
// Registered with the task-refresh coordinator (single source of truth).
|
||||||
@ -65,6 +66,8 @@ class OverviewPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }
|
||||||
|
|
||||||
go(where) {
|
go(where) {
|
||||||
if (where === 'backup') {
|
if (where === 'backup') {
|
||||||
window.spaClean?.navigate('/backup', true);
|
window.spaClean?.navigate('/backup', true);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ class PeersPage {
|
|||||||
this.backupLocations = []; // populated for the loc_idx dropdown
|
this.backupLocations = []; // populated for the loc_idx dropdown
|
||||||
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
||||||
this.eventBound = false;
|
this.eventBound = false;
|
||||||
|
this._ac = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@ -71,9 +72,11 @@ class PeersPage {
|
|||||||
this.closeAllModals();
|
this.closeAllModals();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
}, { signal: this._ac.signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const list = document.getElementById('peers-list');
|
const list = document.getElementById('peers-list');
|
||||||
const empty = document.getElementById('peers-empty');
|
const empty = document.getElementById('peers-empty');
|
||||||
|
|||||||
@ -10,6 +10,7 @@ class SshPage {
|
|||||||
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
||||||
this.data = null;
|
this.data = null;
|
||||||
this._bound = false;
|
this._bound = false;
|
||||||
|
this._ac = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
root() { return document.getElementById(this.rootId); }
|
root() { return document.getElementById(this.rootId); }
|
||||||
@ -51,9 +52,11 @@ class SshPage {
|
|||||||
if (rm) { this.removeKey(rm.dataset.fp); return; }
|
if (rm) { this.removeKey(rm.dataset.fp); return; }
|
||||||
const tog = e.target.closest('[data-action="ssh-toggle-password"]');
|
const tog = e.target.closest('[data-action="ssh-toggle-password"]');
|
||||||
if (tog) { this.togglePassword(tog.dataset.next); return; }
|
if (tog) { this.togglePassword(tog.dataset.next); return; }
|
||||||
});
|
}, { signal: this._ac.signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const root = this.root();
|
const root = this.root();
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
|
|||||||
@ -28,6 +28,7 @@ class SystemPage {
|
|||||||
this.d = {};
|
this.d = {};
|
||||||
// Active sub-view renderer. Disposed on each init().
|
// Active sub-view renderer. Disposed on each init().
|
||||||
this._subview = null;
|
this._subview = null;
|
||||||
|
this._ac = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
root() { return document.getElementById(this.rootId); }
|
root() { return document.getElementById(this.rootId); }
|
||||||
@ -157,9 +158,11 @@ class SystemPage {
|
|||||||
if (window.navigateToRoute) window.navigateToRoute('/admin/system/storage');
|
if (window.navigateToRoute) window.navigateToRoute('/admin/system/storage');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
}, { signal: this._ac.signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }
|
||||||
|
|
||||||
/* ---- formatting helpers (used by sub-pages via window.SystemFmt) ---- */
|
/* ---- formatting helpers (used by sub-pages via window.SystemFmt) ---- */
|
||||||
escape(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
|
escape(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
|
||||||
bytes(n) {
|
bytes(n) {
|
||||||
|
|||||||
@ -30,6 +30,7 @@ class AppTabbedManager {
|
|||||||
this.tasksManager = new TasksManager();
|
this.tasksManager = new TasksManager();
|
||||||
this.appsManager = new AppsManager();
|
this.appsManager = new AppsManager();
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
|
this._ac = new AbortController(); // backs teardown of the watchdog + popstate listeners (dispose())
|
||||||
|
|
||||||
// Button state management
|
// Button state management
|
||||||
this.disabledButtons = new Set();
|
this.disabledButtons = new Set();
|
||||||
@ -614,11 +615,11 @@ class AppTabbedManager {
|
|||||||
this._reconcileTimer = setTimeout(tick, next);
|
this._reconcileTimer = setTimeout(tick, next);
|
||||||
};
|
};
|
||||||
this._reconcileTimer = setTimeout(tick, 1500);
|
this._reconcileTimer = setTimeout(tick, 1500);
|
||||||
window.addEventListener('taskBusReady', reconcile);
|
window.addEventListener('taskBusReady', reconcile, { signal: this._ac.signal });
|
||||||
window.addEventListener('focus', reconcile);
|
window.addEventListener('focus', reconcile, { signal: this._ac.signal });
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'visible') reconcile();
|
if (document.visibilityState === 'visible') reconcile();
|
||||||
});
|
}, { signal: this._ac.signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set current app from URL BEFORE setting up URL monitoring
|
// Set current app from URL BEFORE setting up URL monitoring
|
||||||
@ -687,7 +688,20 @@ class AppTabbedManager {
|
|||||||
if (this.currentApp && newAppName !== this.currentApp) {
|
if (this.currentApp && newAppName !== this.currentApp) {
|
||||||
this.updateApp(newAppName);
|
this.updateApp(newAppName);
|
||||||
}
|
}
|
||||||
});
|
}, { signal: this._ac.signal });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release everything bound for this mount so navigating away doesn't leave the
|
||||||
|
// reconcile loop + window/document listeners firing on the stale singleton (the
|
||||||
|
// backup-stacking bug class). Re-armed on the next initialize().
|
||||||
|
dispose() {
|
||||||
|
try { this._ac && this._ac.abort(); } catch (_) {}
|
||||||
|
this._ac = new AbortController();
|
||||||
|
if (this._reconcileTimer) { clearTimeout(this._reconcileTimer); this._reconcileTimer = null; }
|
||||||
|
this._watchdogStarted = false;
|
||||||
|
this._listenersWired = false;
|
||||||
|
try { window.servicesManager && window.servicesManager.unload && window.servicesManager.unload(); } catch (_) {}
|
||||||
|
try { window.toolsManager && window.toolsManager.unload && window.toolsManager.unload(); } catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for new tasks and switch to logs tab
|
// Watch for new tasks and switch to logs tab
|
||||||
|
|||||||
@ -76,7 +76,11 @@ LP.features.register({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async unmount() {
|
async unmount() {
|
||||||
// No-op: appsManager / appTabbedManager are shared system-loader singletons.
|
// appsManager / appTabbedManager are shared singletons (never null them), but
|
||||||
// The dirty-config nav guard fires in navigate() before unmount.
|
// the detail view's per-mount resources DO need releasing: the reconcile loop,
|
||||||
|
// the watchdog window/document listeners, and the active tab's Services
|
||||||
|
// intervals + log SSE. dispose() handles all of it (re-armed on next mount).
|
||||||
|
// The dirty-config nav guard still fires in navigate() before unmount.
|
||||||
|
try { window.appTabbedManager && window.appTabbedManager.dispose && window.appTabbedManager.dispose(); } catch (_) {}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,16 +27,18 @@ LP.features.register({
|
|||||||
if (typeof loadDashboardData === 'function') loadDashboardData();
|
if (typeof loadDashboardData === 'function') loadDashboardData();
|
||||||
}, 100);
|
}, 100);
|
||||||
ctx.sub(() => clearTimeout(reloadTimer));
|
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() {
|
async unmount() {
|
||||||
// Nothing this view owns is destroyable from here: the dashboard has no
|
// This view's teardown is all registered via ctx.sub() in mount() and runs
|
||||||
// handler-newed controller and no system-loader singleton to tear down.
|
// in ctx.teardown(): the pending reload timer, the 1 Hz update-countdown
|
||||||
// The pending reload timer is cancelled via the ctx.sub() above (ctx.teardown).
|
// interval (stopUpdateCountdown), and this view's LiveSystem subscription
|
||||||
// The 1 Hz LiveSystem SSE sub (attachDashboardLive) self-releases on its
|
// (detachDashboardLive). No handler-newed controller or singleton to tear down.
|
||||||
// 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.
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,6 +16,10 @@ LP.features.register({
|
|||||||
|
|
||||||
if (window.tasksManager) {
|
if (window.tasksManager) {
|
||||||
await window.tasksManager.init();
|
await window.tasksManager.init();
|
||||||
|
// Re-arm the global live-log poller (idempotent) — it's first started in
|
||||||
|
// the constructor, which doesn't re-run, so a revisit after unmount cleared
|
||||||
|
// it would otherwise have no poller. unmount() clears it again.
|
||||||
|
if (typeof window.tasksManager.startGlobalLiveLogUpdater === 'function') window.tasksManager.startGlobalLiveLogUpdater();
|
||||||
} else {
|
} else {
|
||||||
// Don't throw — matches handleTasks: the page still renders, task
|
// Don't throw — matches handleTasks: the page still renders, task
|
||||||
// functionality is just limited until the task-system component is ready.
|
// functionality is just limited until the task-system component is ready.
|
||||||
@ -31,6 +35,17 @@ LP.features.register({
|
|||||||
clearInterval(tm.refreshInterval);
|
clearInterval(tm.refreshInterval);
|
||||||
tm.refreshInterval = null;
|
tm.refreshInterval = null;
|
||||||
}
|
}
|
||||||
|
// The global live-log poller (constructor-started, handle was discarded) and
|
||||||
|
// any still-running per-task monitors — release them so they don't keep
|
||||||
|
// firing on a torn-down page.
|
||||||
|
if (tm && tm.globalLiveLogInterval) {
|
||||||
|
clearInterval(tm.globalLiveLogInterval);
|
||||||
|
tm.globalLiveLogInterval = null;
|
||||||
|
}
|
||||||
|
if (tm && tm.taskMonitors) {
|
||||||
|
for (const stop of Array.from(tm.taskMonitors.values())) { try { stop(); } catch (_) {} }
|
||||||
|
tm.taskMonitors.clear();
|
||||||
|
}
|
||||||
// Stop any open per-task log streams this view started (each removes its own
|
// Stop any open per-task log streams this view started (each removes its own
|
||||||
// SSE listeners + map entry). Snapshot keys first — stopLogStreaming mutates
|
// SSE listeners + map entry). Snapshot keys first — stopLogStreaming mutates
|
||||||
// the map. Does NOT touch the shared bus.
|
// the map. Does NOT touch the shared bus.
|
||||||
|
|||||||
@ -200,9 +200,11 @@ Object.assign(TasksManager.prototype, {
|
|||||||
},
|
},
|
||||||
// Start global live log updater - simple 2-second updates for all running tasks
|
// Start global live log updater - simple 2-second updates for all running tasks
|
||||||
startGlobalLiveLogUpdater() {
|
startGlobalLiveLogUpdater() {
|
||||||
|
// Idempotent + keep the handle so unmount can clear it (it was discarded
|
||||||
|
// before — a permanent 2 s poll that outlived the page).
|
||||||
|
if (this.globalLiveLogInterval) return;
|
||||||
// Update every 2 seconds
|
// Update every 2 seconds
|
||||||
setInterval(async () => {
|
this.globalLiveLogInterval = setInterval(async () => {
|
||||||
|
|
||||||
// Find all running tasks
|
// Find all running tasks
|
||||||
const runningTasks = this.tasks.filter(task => task.status === 'running');
|
const runningTasks = this.tasks.filter(task => task.status === 'running');
|
||||||
|
|||||||
@ -92,10 +92,19 @@ Object.assign(TasksManager.prototype, {
|
|||||||
this.stopLogStreaming(taskId);
|
this.stopLogStreaming(taskId);
|
||||||
window.removeEventListener('taskUpdated', onUpdate);
|
window.removeEventListener('taskUpdated', onUpdate);
|
||||||
window.removeEventListener('taskCompleted', onComplete);
|
window.removeEventListener('taskCompleted', onComplete);
|
||||||
|
if (this.taskMonitors) this.taskMonitors.delete(taskId);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('taskUpdated', onUpdate);
|
window.addEventListener('taskUpdated', onUpdate);
|
||||||
window.addEventListener('taskCompleted', onComplete);
|
window.addEventListener('taskCompleted', onComplete);
|
||||||
|
// Track this monitor so the tasks module's unmount() can release it if the
|
||||||
|
// user navigates away while the task is still running (these window
|
||||||
|
// listeners + interval otherwise outlive the page until the task completes).
|
||||||
|
(this.taskMonitors = this.taskMonitors || new Map()).set(taskId, () => {
|
||||||
|
if (statusUpdateInterval) { clearInterval(statusUpdateInterval); statusUpdateInterval = null; }
|
||||||
|
window.removeEventListener('taskUpdated', onUpdate);
|
||||||
|
window.removeEventListener('taskCompleted', onComplete);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
// Auto-expand a task when it's created
|
// Auto-expand a task when it's created
|
||||||
async autoExpandTask(taskId) {
|
async autoExpandTask(taskId) {
|
||||||
|
|||||||
@ -215,6 +215,10 @@ async function loadDashboardData() {
|
|||||||
// Countdown timer for next automatic update
|
// Countdown timer for next automatic update
|
||||||
let updateCountdownInterval = null;
|
let updateCountdownInterval = null;
|
||||||
|
|
||||||
|
function stopUpdateCountdown() {
|
||||||
|
if (updateCountdownInterval) { clearInterval(updateCountdownInterval); updateCountdownInterval = null; }
|
||||||
|
}
|
||||||
|
|
||||||
function startUpdateCountdown() {
|
function startUpdateCountdown() {
|
||||||
// Clear any existing countdown
|
// Clear any existing countdown
|
||||||
if (updateCountdownInterval) {
|
if (updateCountdownInterval) {
|
||||||
@ -467,6 +471,9 @@ async function loadSystemInfo() {
|
|||||||
// ticker feeds both. Cleanup hangs off a route-change check: if the dashboard
|
// ticker feeds both. Cleanup hangs off a route-change check: if the dashboard
|
||||||
// DOM goes away we drop the sub on the next sample.
|
// DOM goes away we drop the sub on the next sample.
|
||||||
let _dashboardLiveUnsub = null;
|
let _dashboardLiveUnsub = null;
|
||||||
|
function detachDashboardLive() {
|
||||||
|
if (typeof _dashboardLiveUnsub === 'function') { try { _dashboardLiveUnsub(); } catch (_) {} _dashboardLiveUnsub = null; }
|
||||||
|
}
|
||||||
function attachDashboardLive() {
|
function attachDashboardLive() {
|
||||||
if (!window.LiveSystem) return;
|
if (!window.LiveSystem) return;
|
||||||
if (_dashboardLiveUnsub) { try { _dashboardLiveUnsub(); } catch (_) {} _dashboardLiveUnsub = null; }
|
if (_dashboardLiveUnsub) { try { _dashboardLiveUnsub(); } catch (_) {} _dashboardLiveUnsub = null; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user