librelad 182be8c33d feat(webui): phase 3 (first feature) — migrate Backup to a feature module
Introduces the kernel lifecycle and migrates the first real page to the
feature-module contract:
- kernel/lifecycle.js: MountContext (loadScripts/loadFragment/setContent
  + an AbortController/unsub teardown ledger so mounts can't leak
  listeners or live streams).
- features/backup/index.js: Backup Center as a self-contained module
  (LP.features.register with mount/unmount); heavy backup-page.js stays
  lazy-loaded on first mount.
- spa.js: routes whose feature has a registered mount() are driven
  through the kernel; everything else still uses its legacy handleX().
  navigate() unmounts the current feature first. Both fall back to the
  legacy handler if a module is missing or mount throws.

Strangler step: /backup now flows manifest -> registry -> mount/unmount.
The other pages are untouched. handleBackup remains as the fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 23:02:24 +01:00

42 lines
1.8 KiB
JavaScript

// features/backup/index.js — Backup Center as a self-contained feature module.
//
// FIRST page migrated to the feature-module contract (docs/frontend-modularization.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).
scripts: [
'/js/components/backup/backup-page.js',
'/js/components/backup/backup-app-card.js',
],
async mount(ctx) {
await ctx.loadScripts(this.scripts);
const html = await ctx.loadFragment('/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() {
// Best-effort teardown. BackupPage self-guards stale work via
// (window.backupPage === this), so nulling the global neutralises any
// pending task-refresh repaint; we also drop its coordinator registration.
// A proper dispose() (removing the leaked document listeners) lands with
// the Phase 5 backup decomposition.
try { window.taskRefresh && window.taskRefresh.unregister && window.taskRefresh.unregister('backups'); } catch (_) {}
window.backupPage = null;
},
});