From 0bcde854e6bcf26f935884e06975372ef18a3dd7 Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 17 Jun 2026 18:40:40 +0100 Subject: [PATCH] refactor(webui): move fleet Overview under /apps/overview; retire standalone /backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fleet Overview area (Overview/Updates/Improvements/Backups/Migrate) now lives at /apps/overview* instead of /overview*, reflecting that it belongs to App Center. The Backups tab is therefore /apps/overview/backups, and the old standalone Backup Center page is removed entirely: - apps feature owns /apps/overview* (covered by the existing /apps* route); its mount() dispatches /apps/overview -> fleet Overview before the grid check. - _legacyRedirect() rewrites old short URLs so bookmarks/links keep working and the address bar shows the canonical path: /overview[/tab] -> /apps/overview[/tab] /backup[/sub] -> /apps/overview/backups[/sub] /updater, /peers redirects retargeted to /apps/overview* - Removed the standalone backup feature: components/backup/{index.js,feature.json}, its manifest entry, the /backup route registrations and the dead handleBackup(). The BackupPage classes stay — the Overview Backups tab embeds them. - Repointed every backups/overview link: the admin dashboard's 'Open backup center', the app-card 'Open backup center' button + snapshot-overflow link, the sidebar Overview entry, the improvements deep-link, and the Migrate 'go to locations' deep-link. Also drop the redundant inline Check button from the Security empty state (same rationale as Improvements: the host auto-scan repopulates it and the header carries a manual Check). Co-Authored-By: Claude Opus 4.8 Signed-off-by: librelad --- .../admin/overview/js/overview-page.js | 2 +- .../apps/core/html/apps-unified-layout.html | 4 +- .../components/apps/core/js/apps-manager.js | 6 +- .../frontend/components/apps/feature.json | 4 +- .../frontend/components/apps/index.js | 13 ++-- .../apps/overview/js/overview-manager.js | 14 ++--- .../apps/overview/migrate/js/migrate-page.js | 2 +- .../frontend/components/backup/feature.json | 8 --- .../frontend/components/backup/index.js | 56 ------------------ .../frontend/components/manifest.dev.json | 15 +---- .../components/updater/js/updater-page.js | 5 +- .../core/backup-card/js/backup-app-card.js | 2 +- .../frontend/core/kernel/js/spa.js | 59 ++++++++----------- 13 files changed, 53 insertions(+), 137 deletions(-) delete mode 100644 containers/libreportal/frontend/components/backup/feature.json delete mode 100644 containers/libreportal/frontend/components/backup/index.js diff --git a/containers/libreportal/frontend/components/admin/overview/js/overview-page.js b/containers/libreportal/frontend/components/admin/overview/js/overview-page.js index 780c979..60c5589 100644 --- a/containers/libreportal/frontend/components/admin/overview/js/overview-page.js +++ b/containers/libreportal/frontend/components/admin/overview/js/overview-page.js @@ -70,7 +70,7 @@ class OverviewPage { go(where) { if (where === 'backup') { - window.spaClean?.navigate('/backup', true); + window.spaClean?.navigate('/apps/overview/backups', true); } else if (where === 'ssh' || where === 'security' || where === 'system') { const target = where === 'ssh' ? 'ssh-access' : where; window.history.pushState({}, '', window.adminPath(target)); diff --git a/containers/libreportal/frontend/components/apps/core/html/apps-unified-layout.html b/containers/libreportal/frontend/components/apps/core/html/apps-unified-layout.html index 1f17c9e..bcdd307 100755 --- a/containers/libreportal/frontend/components/apps/core/html/apps-unified-layout.html +++ b/containers/libreportal/frontend/components/apps/core/html/apps-unified-layout.html @@ -9,7 +9,7 @@
Backup now - + ), e.g. the - // Restore empty-state's "Open Locations" button. - const seg = window.location.pathname.replace(/^\/overview\/backups\/?/, '').split('/')[0]; + // Honor an optional sub-tab deep-link (/apps/overview/backups/), e.g. + // the Restore empty-state's "Open Locations" button. + const seg = window.location.pathname.replace(/^\/apps\/overview\/backups\/?/, '').split('/')[0]; const sub = ['dashboard', 'backups', 'locations', 'configuration'].includes(seg) ? seg : null; // Detect a prior mount by the embedded fragment in THIS pane — not by // #backup-section, which the per-app Backups tab also defines. diff --git a/containers/libreportal/frontend/components/apps/overview/migrate/js/migrate-page.js b/containers/libreportal/frontend/components/apps/overview/migrate/js/migrate-page.js index fd40b80..3cb8433 100644 --- a/containers/libreportal/frontend/components/apps/overview/migrate/js/migrate-page.js +++ b/containers/libreportal/frontend/components/apps/overview/migrate/js/migrate-page.js @@ -68,7 +68,7 @@ class MigratePage { } const locBtn = e.target.closest('[data-action="go-to-locations"]'); if (locBtn && this.root()?.contains(locBtn)) { - if (window.navigateToRoute) window.navigateToRoute('/overview/backups/locations'); + if (window.navigateToRoute) window.navigateToRoute('/apps/overview/backups/locations'); return; } if (e.target.closest('#ov-migrate-confirm')) { this.confirmMigrate(); return; } diff --git a/containers/libreportal/frontend/components/backup/feature.json b/containers/libreportal/frontend/components/backup/feature.json deleted file mode 100644 index c727332..0000000 --- a/containers/libreportal/frontend/components/backup/feature.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "backup", - "routes": ["/backup", "/backup*"], - "module": "/components/backup/index.js", - "handler": "handleBackup", - "navId": "nav-backup", - "order": 50 -} diff --git a/containers/libreportal/frontend/components/backup/index.js b/containers/libreportal/frontend/components/backup/index.js deleted file mode 100644 index 457a38d..0000000 --- a/containers/libreportal/frontend/components/backup/index.js +++ /dev/null @@ -1,56 +0,0 @@ -// 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/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; - }, -}); diff --git a/containers/libreportal/frontend/components/manifest.dev.json b/containers/libreportal/frontend/components/manifest.dev.json index fa2e39e..0c5d759 100644 --- a/containers/libreportal/frontend/components/manifest.dev.json +++ b/containers/libreportal/frontend/components/manifest.dev.json @@ -23,9 +23,7 @@ "/apps", "/apps*", "/app", - "/app*", - "/overview", - "/overview*" + "/app*" ], "module": "/components/apps/index.js", "handler": "handleApps", @@ -75,17 +73,6 @@ "module": "/components/updater/index.js", "navId": "nav-updater", "order": 30 - }, - { - "id": "backup", - "routes": [ - "/backup", - "/backup*" - ], - "module": "/components/backup/index.js", - "handler": "handleBackup", - "navId": "nav-backup", - "order": 50 } ] } diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index 7c0c72e..9dedc34 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -379,7 +379,10 @@ class UpdaterPage { renderSecurity() { const withCves = this.apps.filter(a => (a.cves || []).length); - if (!this.cves) return this.empty('No vulnerability scan yet — one runs automatically within a couple of minutes.', true); + // No inline Check button: the host auto-scan runs the vulnerability scan on + // its own within a couple of minutes (and the embedding header carries a + // manual Check), so the message alone is the right button-free empty UI. + if (!this.cves) return this.empty('No vulnerability scan yet — one runs automatically within a couple of minutes.'); if (!withCves.length) return this.empty('No known vulnerabilities in your installed apps. 🎉'); const blocks = withCves.map(a => { const items = (a.cves || []).map(c => ` diff --git a/containers/libreportal/frontend/core/backup-card/js/backup-app-card.js b/containers/libreportal/frontend/core/backup-card/js/backup-app-card.js index 128bf00..f08d317 100644 --- a/containers/libreportal/frontend/core/backup-card/js/backup-app-card.js +++ b/containers/libreportal/frontend/core/backup-card/js/backup-app-card.js @@ -80,7 +80,7 @@ class BackupAppCard {
${allSnaps.slice(0, 50).map(s => this._renderRow(s, iconUrl)).join('')}
- ${allSnaps.length > 50 ? `
` : ''} + ${allSnaps.length > 50 ? `
Showing the most recent 50 of ${allSnaps.length} backups. Use the backup center for the full list.
` : ''} `; // Deep-link: /app//backups?snapshot= auto-expands that row diff --git a/containers/libreportal/frontend/core/kernel/js/spa.js b/containers/libreportal/frontend/core/kernel/js/spa.js index f074258..a5deaab 100755 --- a/containers/libreportal/frontend/core/kernel/js/spa.js +++ b/containers/libreportal/frontend/core/kernel/js/spa.js @@ -69,10 +69,9 @@ class LibrePortalSPAClean { this.routes.set('/admin*', () => this.handleAdmin()); this.routes.set('/tasks', () => this.handleTasks()); // Handle /tasks without query this.routes.set('/tasks*', () => this.handleTasks()); // Handle /tasks with query - this.routes.set('/backup', () => this.handleBackup()); - this.routes.set('/backup*', () => this.handleBackup()); - // Legacy /config, /peers, /ssh are handled by _legacyRedirect() at the top - // of navigate() (rewrites to the canonical /admin/* path). + // Legacy /config, /peers, /ssh, /backup, /overview are handled by + // _legacyRedirect() at the top of navigate() (rewritten to their canonical + // /admin/* or /apps/overview* paths before routing). } @@ -333,28 +332,6 @@ class LibrePortalSPAClean { } } - async handleBackup() { - try { - // backup-page.js + backup-app-card.js are loaded on first navigation. - // loadScript is idempotent — subsequent /backup navigations are no-ops. - await Promise.all([ - this.loadScript('/components/backup/core/js/backup-page.js'), - this.loadScript('/core/backup-card/js/backup-app-card.js') - ]); - const html = await this.fetchContent('/components/backup/core/html/backup-content.html'); - this.loadContent(html, 'Backups'); - if (typeof BackupPage !== 'undefined') { - window.backupPage = new BackupPage(); - await window.backupPage.init(); - } else { - console.error('BackupPage class not loaded'); - } - } catch (error) { - console.error('❌ Backup page load error:', error); - this.showError('Failed to load backup page'); - } - } - // Map a legacy short URL (/ssh, /peers, /config[?=cat]) to its canonical // /admin/* path, or return null if it isn't a legacy redirect. Used at the top // of navigate(). Replaces the old config-redirect/peers/ssh handlers. @@ -362,8 +339,8 @@ class LibrePortalSPAClean { const full = path || ''; const p = full.split('?')[0]; if (p === '/ssh' || p.startsWith('/ssh/')) return '/admin/tools/ssh-access'; - // Peers now lives under Overview › Migrate › Peers (moved out of Admin). - if (p === '/peers' || p.startsWith('/peers/') || p === '/admin/tools/peers' || p.startsWith('/admin/tools/peers/')) return '/overview/migrate/peers'; + // Peers now lives under App Center › Overview › Migrate › Peers (moved out of Admin). + if (p === '/peers' || p.startsWith('/peers/') || p === '/admin/tools/peers' || p.startsWith('/admin/tools/peers/')) return '/apps/overview/migrate/peers'; if (p === '/config' || p.startsWith('/config/') || full.startsWith('/config?')) { let cat = 'overview'; if (full.includes('?=')) cat = full.split('?=')[1] || 'overview'; @@ -371,15 +348,27 @@ class LibrePortalSPAClean { else { const seg = p.replace(/^\/config\/?/, ''); if (seg) cat = seg; } return (typeof window.adminPath === 'function') ? window.adminPath(cat) : '/admin'; } - // The standalone Updater page is now the fleet Overview area. /updater[/tab] - // -> /overview[/tab]; security/recovery/history folded into the Updates - // expander, so they land on /overview/updates. (/backup is NOT redirected — - // it remains the operational backup center, reached from Overview › Backups.) + // The fleet Overview area lives under App Center: /apps/overview[/tab]. + // Rewrite the legacy short forms so old bookmarks/links keep working and the + // address bar always shows the canonical path: + // /overview[/tab] -> /apps/overview[/tab] + // /backup[/sub] -> /apps/overview/backups[/sub] (the backup center is + // now the Overview › Backups tab, not a standalone page) + // /updater[/tab] -> /apps/overview[/tab]; security/recovery/history were + // folded into the Updates expander, so they land on + // /apps/overview/updates. + if (p === '/overview' || p.startsWith('/overview/')) { + return '/apps' + p; + } + if (p === '/backup' || p.startsWith('/backup/')) { + const sub = p.replace(/^\/backup\/?/, ''); + return sub ? '/apps/overview/backups/' + sub : '/apps/overview/backups'; + } if (p === '/updater' || p.startsWith('/updater/')) { const seg = p.replace(/^\/updater\/?/, ''); - if (seg === 'updates' || seg === 'improvements') return '/overview/' + seg; - if (!seg || seg === 'overview') return '/overview'; - return '/overview/updates'; + if (seg === 'updates' || seg === 'improvements') return '/apps/overview/' + seg; + if (!seg || seg === 'overview') return '/apps/overview'; + return '/apps/overview/updates'; } return null; }