From 73a93cf1458cfe589b1cf739fc699419c43d651c Mon Sep 17 00:00:00 2001 From: librelad Date: Tue, 7 Jul 2026 20:46:45 +0100 Subject: [PATCH] fix(webui/backups): sync fleet Backups sub-tab URLs (/apps/overview/backups/) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/ (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) Signed-off-by: librelad --- .../apps/overview/js/overview-manager.js | 11 ++++++++- .../components/backup/core/js/backup-page.js | 24 +++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js index e67406e..a2f88da 100644 --- a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js +++ b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js @@ -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/ 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 (_) { diff --git a/containers/libreportal/frontend/components/backup/core/js/backup-page.js b/containers/libreportal/frontend/components/backup/core/js/backup-page.js index aee2441..e2098c7 100644 --- a/containers/libreportal/frontend/components/backup/core/js/backup-page.js +++ b/containers/libreportal/frontend/components/backup/core/js/backup-page.js @@ -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/ 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/ 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/ (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/ itself; embedded lets the fleet host + // map the switch onto /apps/overview/backups/ 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.