Compare commits
No commits in common. "7d15fa2a22c0892a5e515e4dfce13ea9f5cd005d" and "429ec419cff309b1e61a98aee494b600dfe10fed" have entirely different histories.
7d15fa2a22
...
429ec419cf
@ -49,12 +49,6 @@ LP.features.register({
|
||||
// Drop OverviewPage's task-refresh registration so a finished verify/update
|
||||
// task doesn't repaint a torn-down board.
|
||||
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.systemPage = null;
|
||||
window.sshPage = null;
|
||||
|
||||
@ -7,7 +7,6 @@ class OverviewPage {
|
||||
this.rootId = rootId;
|
||||
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
||||
this._bound = false;
|
||||
this._ac = new AbortController();
|
||||
}
|
||||
|
||||
root() { return document.getElementById(this.rootId); }
|
||||
@ -53,7 +52,7 @@ class OverviewPage {
|
||||
if (go) { this.go(go.dataset.adminGo); return; }
|
||||
if (e.target.closest('[data-admin-update]')) { this.runUpdate(); 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
|
||||
// and re-render so the badge reflects reality without a manual reload.
|
||||
// Registered with the task-refresh coordinator (single source of truth).
|
||||
@ -66,8 +65,6 @@ class OverviewPage {
|
||||
});
|
||||
}
|
||||
|
||||
dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }
|
||||
|
||||
go(where) {
|
||||
if (where === 'backup') {
|
||||
window.spaClean?.navigate('/backup', true);
|
||||
|
||||
@ -14,7 +14,6 @@ class PeersPage {
|
||||
this.backupLocations = []; // populated for the loc_idx dropdown
|
||||
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
||||
this.eventBound = false;
|
||||
this._ac = new AbortController();
|
||||
}
|
||||
|
||||
async init() {
|
||||
@ -72,11 +71,9 @@ class PeersPage {
|
||||
this.closeAllModals();
|
||||
return;
|
||||
}
|
||||
}, { signal: this._ac.signal });
|
||||
});
|
||||
}
|
||||
|
||||
dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }
|
||||
|
||||
render() {
|
||||
const list = document.getElementById('peers-list');
|
||||
const empty = document.getElementById('peers-empty');
|
||||
|
||||
@ -10,7 +10,6 @@ class SshPage {
|
||||
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
||||
this.data = null;
|
||||
this._bound = false;
|
||||
this._ac = new AbortController();
|
||||
}
|
||||
|
||||
root() { return document.getElementById(this.rootId); }
|
||||
@ -52,11 +51,9 @@ class SshPage {
|
||||
if (rm) { this.removeKey(rm.dataset.fp); return; }
|
||||
const tog = e.target.closest('[data-action="ssh-toggle-password"]');
|
||||
if (tog) { this.togglePassword(tog.dataset.next); return; }
|
||||
}, { signal: this._ac.signal });
|
||||
});
|
||||
}
|
||||
|
||||
dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }
|
||||
|
||||
render() {
|
||||
const root = this.root();
|
||||
if (!root) return;
|
||||
|
||||
@ -28,7 +28,6 @@ class SystemPage {
|
||||
this.d = {};
|
||||
// Active sub-view renderer. Disposed on each init().
|
||||
this._subview = null;
|
||||
this._ac = new AbortController();
|
||||
}
|
||||
|
||||
root() { return document.getElementById(this.rootId); }
|
||||
@ -158,11 +157,9 @@ class SystemPage {
|
||||
if (window.navigateToRoute) window.navigateToRoute('/admin/system/storage');
|
||||
return;
|
||||
}
|
||||
}, { signal: this._ac.signal });
|
||||
});
|
||||
}
|
||||
|
||||
dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }
|
||||
|
||||
/* ---- formatting helpers (used by sub-pages via window.SystemFmt) ---- */
|
||||
escape(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
|
||||
bytes(n) {
|
||||
|
||||
@ -30,7 +30,6 @@ class AppTabbedManager {
|
||||
this.tasksManager = new TasksManager();
|
||||
this.appsManager = new AppsManager();
|
||||
this.initialized = false;
|
||||
this._ac = new AbortController(); // backs teardown of the watchdog + popstate listeners (dispose())
|
||||
|
||||
// Button state management
|
||||
this.disabledButtons = new Set();
|
||||
@ -615,11 +614,11 @@ class AppTabbedManager {
|
||||
this._reconcileTimer = setTimeout(tick, next);
|
||||
};
|
||||
this._reconcileTimer = setTimeout(tick, 1500);
|
||||
window.addEventListener('taskBusReady', reconcile, { signal: this._ac.signal });
|
||||
window.addEventListener('focus', reconcile, { signal: this._ac.signal });
|
||||
window.addEventListener('taskBusReady', reconcile);
|
||||
window.addEventListener('focus', reconcile);
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') reconcile();
|
||||
}, { signal: this._ac.signal });
|
||||
});
|
||||
}
|
||||
|
||||
// Set current app from URL BEFORE setting up URL monitoring
|
||||
@ -688,20 +687,7 @@ class AppTabbedManager {
|
||||
if (this.currentApp && newAppName !== this.currentApp) {
|
||||
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
|
||||
|
||||
@ -76,11 +76,7 @@ LP.features.register({
|
||||
},
|
||||
|
||||
async unmount() {
|
||||
// appsManager / appTabbedManager are shared singletons (never null them), but
|
||||
// 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 (_) {}
|
||||
// No-op: appsManager / appTabbedManager are shared system-loader singletons.
|
||||
// The dirty-config nav guard fires in navigate() before unmount.
|
||||
},
|
||||
});
|
||||
|
||||
@ -27,18 +27,16 @@ LP.features.register({
|
||||
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.
|
||||
// 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.
|
||||
},
|
||||
});
|
||||
|
||||
@ -16,10 +16,6 @@ LP.features.register({
|
||||
|
||||
if (window.tasksManager) {
|
||||
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 {
|
||||
// Don't throw — matches handleTasks: the page still renders, task
|
||||
// functionality is just limited until the task-system component is ready.
|
||||
@ -35,17 +31,6 @@ LP.features.register({
|
||||
clearInterval(tm.refreshInterval);
|
||||
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
|
||||
// SSE listeners + map entry). Snapshot keys first — stopLogStreaming mutates
|
||||
// the map. Does NOT touch the shared bus.
|
||||
|
||||
@ -200,11 +200,9 @@ Object.assign(TasksManager.prototype, {
|
||||
},
|
||||
// Start global live log updater - simple 2-second updates for all running tasks
|
||||
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
|
||||
this.globalLiveLogInterval = setInterval(async () => {
|
||||
setInterval(async () => {
|
||||
|
||||
// Find all running tasks
|
||||
const runningTasks = this.tasks.filter(task => task.status === 'running');
|
||||
|
||||
@ -92,19 +92,10 @@ Object.assign(TasksManager.prototype, {
|
||||
this.stopLogStreaming(taskId);
|
||||
window.removeEventListener('taskUpdated', onUpdate);
|
||||
window.removeEventListener('taskCompleted', onComplete);
|
||||
if (this.taskMonitors) this.taskMonitors.delete(taskId);
|
||||
};
|
||||
|
||||
window.addEventListener('taskUpdated', onUpdate);
|
||||
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
|
||||
async autoExpandTask(taskId) {
|
||||
|
||||
@ -215,10 +215,6 @@ async function loadDashboardData() {
|
||||
// Countdown timer for next automatic update
|
||||
let updateCountdownInterval = null;
|
||||
|
||||
function stopUpdateCountdown() {
|
||||
if (updateCountdownInterval) { clearInterval(updateCountdownInterval); updateCountdownInterval = null; }
|
||||
}
|
||||
|
||||
function startUpdateCountdown() {
|
||||
// Clear any existing countdown
|
||||
if (updateCountdownInterval) {
|
||||
@ -471,9 +467,6 @@ async function loadSystemInfo() {
|
||||
// ticker feeds both. Cleanup hangs off a route-change check: if the dashboard
|
||||
// DOM goes away we drop the sub on the next sample.
|
||||
let _dashboardLiveUnsub = null;
|
||||
function detachDashboardLive() {
|
||||
if (typeof _dashboardLiveUnsub === 'function') { try { _dashboardLiveUnsub(); } catch (_) {} _dashboardLiveUnsub = null; }
|
||||
}
|
||||
function attachDashboardLive() {
|
||||
if (!window.LiveSystem) return;
|
||||
if (_dashboardLiveUnsub) { try { _dashboardLiveUnsub(); } catch (_) {} _dashboardLiveUnsub = null; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user