The original report: clicking a backup sidebar tab loaded content on top of the old content. Root cause (flagged in the unmount comment as deferred): BackupPage.bindEvents() attaches document-level click/input/change listeners guarded only by the instance-level this.eventBound, and unmount() nulled window.backupPage WITHOUT removing them. Each revisit added another full set of listeners bound to a stale BackupPage, all firing on every click and mutating the live DOM (double tab-switches, double modal opens, stale-instance renders). Fix (mirrors the kernel's MountContext pattern): give BackupPage an AbortController, bind the three document listeners to its signal, add dispose() that aborts them (+ drops the task-refresh reg + clears the timer), and call it from the feature module's unmount(). Revisits now start clean — one live instance, one set of listeners. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
58 lines
2.9 KiB
JavaScript
58 lines
2.9 KiB
JavaScript
// components/backup/index.js — Backup Center as a self-contained feature module.
|
|
//
|
|
// FIRST page migrated to the feature-module contract (docs/architecture/webui-architecture.md).
|
|
// The kernel drives mount()/unmount() for the /backup route instead of
|
|
// spa.js's handleBackup(). The heavy controller (backup-page.js, ~129KB) is
|
|
// still lazy-loaded on first mount, so cold-load cost is unchanged.
|
|
//
|
|
// Snap-out demo: deleting this folder removes the backup route's module
|
|
// registration; the manifest entry then falls back to the legacy handler
|
|
// (and, once handleBackup is retired, to the kernel's not-found route). The
|
|
// full decomposition of backup-page.js into per-tab modules is Phase 5.
|
|
LP.features.register({
|
|
id: 'backup',
|
|
routes: ['/backup', '/backup*'],
|
|
// Controllers the feature needs; lazy-loaded on first mount (idempotent).
|
|
// Controllers, organised by sub-system (tabs). core/ first: schema + base
|
|
// class + the shared data/cron, then each tab's prototype-augment clusters.
|
|
scripts: [
|
|
'/components/backup/core/js/backup-schema.js',
|
|
'/components/backup/core/js/backup-page.js', // base: class + constructor + init/switchTab/render
|
|
'/components/backup/core/js/backup-fetch-client.js',
|
|
'/components/backup/core/js/backup-cron-schedule.js',
|
|
'/components/backup/dashboard/js/backup-dashboard.js',
|
|
'/components/backup/snapshots/js/backup-snapshots.js',
|
|
'/components/backup/snapshots/js/backup-snapshot-actions.js',
|
|
'/components/backup/locations/js/backup-locations.js',
|
|
'/components/backup/locations/js/backup-location-fields.js',
|
|
'/components/backup/locations/js/backup-location-modal.js',
|
|
'/components/backup/locations/js/backup-ssh-key.js',
|
|
'/components/backup/migrate/js/backup-migrate.js',
|
|
'/components/backup/configuration/js/backup-configuration.js',
|
|
'/components/backup/configuration/js/backup-retention-presets.js',
|
|
'/components/backup/configuration/js/backup-engine-details.js',
|
|
'/core/backup-card/js/backup-app-card.js',
|
|
],
|
|
|
|
async mount(ctx) {
|
|
await ctx.loadScripts(this.scripts);
|
|
const html = await ctx.loadFragment('/components/backup/core/html/backup-content.html');
|
|
ctx.setContent(html, 'Backups');
|
|
if (typeof BackupPage === 'undefined') {
|
|
throw new Error('BackupPage controller failed to load');
|
|
}
|
|
window.backupPage = new BackupPage();
|
|
await window.backupPage.init();
|
|
},
|
|
|
|
async unmount(ctx) {
|
|
// Release the page's document listeners + task-refresh registration so a
|
|
// navigation away doesn't leave stale BackupPage listeners firing on the
|
|
// live DOM — the backup sidebar "content stacks on revisit" bug. dispose()
|
|
// aborts the click/input/change listeners and drops the coordinator reg.
|
|
try { window.backupPage && window.backupPage.dispose(); } catch (_) {}
|
|
try { ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('backups'); } catch (_) {}
|
|
window.backupPage = null;
|
|
},
|
|
});
|