diff --git a/containers/libreportal/frontend/components/admin/config/js/config-sidebar.js b/containers/libreportal/frontend/components/admin/config/js/config-sidebar.js index fcd8439..726295d 100755 --- a/containers/libreportal/frontend/components/admin/config/js/config-sidebar.js +++ b/containers/libreportal/frontend/components/admin/config/js/config-sidebar.js @@ -76,10 +76,10 @@ class ConfigSidebar { var self = this; // Preserve 'this' context categoriesArray.forEach(function(category) { - // The backup config category (engine/schedule/retention) surfaces here in - // Admin. The operational backup center (/backup, reached from Overview › - // Backups) keeps locations, migrate and snapshots. Both bind the same - // generated category, so edits stay in sync. + // Backup config (engine/schedule/retention) now lives in the Backups tab's + // embedded center (Overview › Backups › Configuration), so it's hidden from + // the Admin config sidebar to avoid a second surface for the same data. + if (category.id === 'backup') return; const categoryItem = document.createElement('div'); categoryItem.className = 'category'; diff --git a/containers/libreportal/frontend/components/apps/index.js b/containers/libreportal/frontend/components/apps/index.js index caa4739..2dd20e6 100644 --- a/containers/libreportal/frontend/components/apps/index.js +++ b/containers/libreportal/frontend/components/apps/index.js @@ -123,5 +123,9 @@ LP.features.register({ // overviewManager singleton + its DOM persist with the layout; its run() // self-guards, but unregistering is the clean release. try { ctx && ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('overview'); } catch (_) {} + // The Backups tab embeds a BackupPage; release its document listeners + + // task-refresh registration on the way out (the "stacks on revisit" bug). + try { window.overviewBackupPage && window.overviewBackupPage.dispose && window.overviewBackupPage.dispose(); } catch (_) {} + window.overviewBackupPage = null; }, }); diff --git a/containers/libreportal/frontend/components/apps/overview/css/overview.css b/containers/libreportal/frontend/components/apps/overview/css/overview.css index 07fbc4f..64c5144 100644 --- a/containers/libreportal/frontend/components/apps/overview/css/overview.css +++ b/containers/libreportal/frontend/components/apps/overview/css/overview.css @@ -131,47 +131,57 @@ .updater-detail-meta { color: var(--text-muted, rgba(255, 255, 255, .6)); font-size: .85rem; } .updater-detail-empty { color: var(--text-muted, rgba(255, 255, 255, .6)); margin: 0; } -/* ---- Backups tab -------------------------------------------------------- */ -.ov-backup-summary { font-weight: 600; } -.ov-backup-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 12px; - margin-top: 14px; -} -.ov-backup-tile { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 14px; - border-radius: 10px; - border: 1px solid var(--border-color, rgba(255, 255, 255, .12)); - background: var(--input-bg, rgba(255, 255, 255, .03)); -} -.ov-backup-tile[role="button"] { cursor: pointer; transition: border-color .15s ease, background .15s ease; } -.ov-backup-tile[role="button"]:hover { border-color: rgba(var(--page-backups-rgb), .5); background: rgba(var(--page-backups-rgb), .08); } -.ov-backup-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; } -.ov-backup-dot.ok { background: var(--page-verify, #1fb88a); } -.ov-backup-dot.warn { background: var(--page-system, #f0883e); } -.ov-backup-name { font-weight: 600; } -.ov-backup-time { margin-left: auto; color: var(--text-muted, rgba(255, 255, 255, .6)); font-size: .82rem; } +/* ---- Backups tab: embedded backup center -------------------------------- */ +/* The Backups tab mounts the real BackupPage. Its own page-header replaces the + generic fleet header, and its left sidebar is restyled into a horizontal + nested tab strip so the whole thing reads as tabs-within-tabs. */ +#overview-view.ov-backups-active .overview-header { display: none; } -.ov-loc-list { margin-top: 20px; } -.ov-loc-list h4 { - margin: 0 0 8px; - font-size: .8rem; - text-transform: uppercase; - letter-spacing: .04em; - color: var(--text-muted, rgba(255, 255, 255, .6)); +#overview-view #ov-pane-backups .backup-layout { display: block; } +#overview-view #ov-pane-backups .backup-layout > .sidebar { + width: auto; + max-width: none; + height: auto; + min-height: 0; + margin: 0 0 16px; + padding: 0; + background: transparent; + border: none; } -.ov-loc-row { +#overview-view #ov-pane-backups .backup-layout > .sidebar #backup-sidebar-list, +#overview-view #ov-pane-backups .backup-layout > .sidebar .sidebar-section { display: flex; - align-items: center; - gap: 12px; - padding: 8px 0; - border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, .08)); + flex-wrap: wrap; + gap: 8px; +} +#overview-view #ov-pane-backups .backup-layout > .sidebar .category { + display: inline-flex; + align-items: center; + gap: 8px; + width: auto; + margin: 0; + padding: 8px 14px; + border-radius: 8px; + border: 1px solid var(--border-color, rgba(255, 255, 255, .14)); + cursor: pointer; +} +#overview-view #ov-pane-backups .backup-layout > .sidebar .category:hover { + background: rgba(255, 255, 255, .06); +} +#overview-view #ov-pane-backups .backup-layout > .sidebar .category.active { + background: rgba(var(--page-backups-rgb), .18); + border-color: rgba(var(--page-backups-rgb), .5); + color: #fff; +} +#overview-view #ov-pane-backups .backup-layout > .sidebar .category .category-icon { + width: 16px; + height: 16px; + flex: 0 0 auto; +} +#overview-view #ov-pane-backups .backup-layout > .main { + width: 100%; + min-width: 0; } -.ov-loc-row span:first-child { font-weight: 500; } /* ---- per-app Updates tab header ----------------------------------------- */ .app-updater-section { padding: 4px 0; } 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 58197de..6901295 100644 --- a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js +++ b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js @@ -117,6 +117,10 @@ class OverviewManager { _applyTab(id) { if (this.tabs) this.tabs.switch(id); + // The Backups tab embeds the full backup center, which brings its own header + // and a nested sub-tab strip — hide the generic fleet header for it. + const root = document.getElementById('overview-view'); + if (root) root.classList.toggle('ov-backups-active', id === 'backups'); this.updateHeader(id); this.renderTab(id); this.current = id; @@ -153,7 +157,7 @@ class OverviewManager { break; } case 'improvements': pane.innerHTML = this.renderImprovements(); break; - case 'backups': pane.innerHTML = this.renderBackups(); break; + case 'backups': this.mountBackupCenter(pane); break; } } @@ -190,8 +194,6 @@ class OverviewManager { case 'goto': this.switchTab(oa.dataset.tab); break; case 'filter': this.filter = oa.dataset.filter || 'all'; this.renderTab('updates'); break; case 'toggle': this.toggleAppDetails(oa.dataset.app); break; - case 'open-app': window.navigateToRoute && window.navigateToRoute(`/app/${oa.dataset.app}/backups`); break; - case 'open-backup': window.navigateToRoute && window.navigateToRoute('/backup'); break; } } } @@ -352,34 +354,69 @@ class OverviewManager { // ---- Backups tab (fleet health glance; actions deep-link per app) --------- - renderBackups() { - const d = this.backup; - if (!d) return `