fix(webui/backups): sync fleet Backups sub-tab URLs (/apps/overview/backups/<sub>)

The embedded backup center deep-links *in* fine (mountBackupCenter parses the
sub-segment), but clicking a sub-tab never wrote the URL: BackupPage disables
pushTabToUrl() when embedded and nothing on the fleet side stepped in, so the
address bar stayed stale and the sub-tabs weren't shareable/deep-linkable —
unlike the sibling Migrate sub-tabs.

Give BackupPage an onTabChange hook fired on every switch; the OverviewManager
supplies it to replaceState /apps/overview/backups/<sub> (replaceState, matching
Migrate, so the on-entry deep-link re-sync can't stack a duplicate history
entry). Standalone /backup keeps using pushTabToUrl() unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-07-07 20:46:45 +01:00
parent ff711d3c2b
commit 73a93cf145
2 changed files with 30 additions and 5 deletions

View File

@ -643,7 +643,16 @@ class OverviewManager {
return;
}
try { if (window.overviewBackupPage) window.overviewBackupPage.dispose(); } catch (_) {}
window.overviewBackupPage = new BackupPage({ embedded: true });
// Own the URL for the embedded center's sub-tabs: reflect each switch as
// /apps/overview/backups/<sub> so they're shareable + deep-linkable (the
// read side is already handled by mountBackupCenter's segment parse). Use
// replaceState, mirroring the Migrate sub-tabs — the switchTab() we fire
// on entry to honor a deep link would otherwise stack a duplicate history
// entry on top of the one we navigated in on.
window.overviewBackupPage = new BackupPage({
embedded: true,
onTabChange: (sub) => this._pushUrl(`/apps/overview/backups/${sub}`, true),
});
await window.overviewBackupPage.init();
if (sub) { try { window.overviewBackupPage.switchTab(sub); } catch (_) {} }
} catch (_) {

View File

@ -8,8 +8,14 @@
class BackupPage {
constructor(opts = {}) {
// Embedded in the fleet Overview's Backups tab: the same controller, but
// it must not couple to the /backup URL (the fleet owns /overview/backups).
// it must not write the standalone /backup URL — the fleet owns the
// address bar. It hands us onTabChange so a sub-tab switch still syncs the
// canonical /apps/overview/backups/<sub> URL via the OverviewManager.
this.embedded = !!opts.embedded;
// Host-supplied when embedded: called with the new sub-tab slug after
// every switch so the host can reflect it in the URL. Null for the
// standalone page, which uses pushTabToUrl() instead.
this.onTabChange = typeof opts.onTabChange === 'function' ? opts.onTabChange : null;
this.currentTab = 'dashboard';
this.dashboard = null;
this.locations = null;
@ -37,7 +43,10 @@ class BackupPage {
and /backup?backup=dashboard (standard query string) so links from
either source resolve correctly. */
parseTabFromUrl() {
if (this.embedded) return null; // embedded: always open on Dashboard; sub-tabs are in-page only
// Embedded: the fleet host (OverviewManager.mountBackupCenter) parses the
// /apps/overview/backups/<sub> segment and drives switchTab, so the
// embedded controller never reads the URL itself.
if (this.embedded) return null;
const allowed = new Set(['dashboard', 'backups', 'locations', 'configuration']);
// Path-based: /backup/<tab> (bare /backup → default tab).
const seg = window.location.pathname.replace(/^\/backup\/?/, '').split('/')[0];
@ -339,11 +348,18 @@ class BackupPage {
this.currentTab = tab;
this.applyActiveTabUi(tab);
this.updatePrimaryAction();
if (!opts.fromPopstate) this.pushTabToUrl(tab);
if (!opts.fromPopstate) {
// Standalone writes /backup/<tab> itself; embedded lets the fleet host
// map the switch onto /apps/overview/backups/<tab> via onTabChange (its
// own pushTabToUrl is a no-op). Both are skipped when the switch was
// triggered by a popstate — the URL has already moved.
this.pushTabToUrl(tab);
if (this.onTabChange) { try { this.onTabChange(tab); } catch (_) {} }
}
}
pushTabToUrl(tab) {
if (this.embedded) return; // embedded: keep the URL at /overview/backups (no sub-tab coupling)
if (this.embedded) return; // embedded: the fleet host owns the URL (see onTabChange)
const url = `/backup/${tab}`;
// Use replaceState for the *first* push (initial tab inferred from
// URL); otherwise pushState so back/forward navigates between tabs.