From a06b6cd1d802b0dc96910406f7ac122eab50c470 Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 12 Jun 2026 18:55:10 +0100 Subject: [PATCH] feat(overview): match fleet tab content to the app-detail tab layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every fleet Overview tab now follows the per-app detail tab idiom the rest of the app uses: title + description on the left, action buttons on the right, a divider underneath, and the body inside the recessed dark container (.tasks-container recipe). - renderHeader() gains an action slot; Check/Check now/Update all move out of in-body toolbars into the header (Updates keeps its filter chips in the body; the Apps-tracked stat card drops its duplicate Check button; UpdaterPage.renderImprovements can skip its toolbar). - String tabs wrap their body in .ov-tab-body — margin/padding 16px, rgba(bg,.2) panel — mirroring backup/tasks/updater containers. - The Backups tab's embedded nested strip (Dashboard/Backups/Locations/ Configuration) now sits on the same surface as every other tab strip: added to the nebula sidebar-bg anchor rule (it was stuck on the lighter --hover-bg) and its buttons use .main-tab-button type. Signed-off-by: librelad Co-Authored-By: Claude Fable 5 --- .../components/apps/overview/css/overview.css | 32 ++++++++++++ .../apps/overview/js/overview-manager.js | 49 ++++++++++++------- .../components/updater/js/updater-page.js | 6 ++- .../frontend/themes/nebula/theme.css | 9 +++- 4 files changed, 74 insertions(+), 22 deletions(-) diff --git a/containers/libreportal/frontend/components/apps/overview/css/overview.css b/containers/libreportal/frontend/components/apps/overview/css/overview.css index a42733d..7840eef 100644 --- a/containers/libreportal/frontend/components/apps/overview/css/overview.css +++ b/containers/libreportal/frontend/components/apps/overview/css/overview.css @@ -42,6 +42,33 @@ the area padding the old header used to provide at the top. */ .overview-tabbed { margin-top: 0; padding: 22px 24px 24px; } +/* Tab header: title + blurb left, action buttons right, divider underneath — + the same shape as the per-app .backup-title / .updater-title rows (the + .config-title base supplies the 20px padding + bottom border). */ +#overview-view .ov-tab-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} +#overview-view .ov-tab-header-main { flex: 1; min-width: 0; } +#overview-view .ov-tab-header-actions { + display: flex; + gap: 10px; + align-items: center; + flex-shrink: 0; +} + +/* Recessed dark panel holding each tab's body — same recipe as the per-app + .tasks-container / .backup-snapshots-container / .updater-detail-container, + so the fleet tabs read exactly like the app-detail tabs. */ +#overview-view .ov-tab-body { + margin: 16px; + padding: 16px; + background: rgba(var(--bg-rgb), 0.2); + border-radius: 8px; +} + /* ---- Updates: filter chips + toolbar ------------------------------------ */ .ov-toolbar { display: flex; @@ -164,6 +191,11 @@ border-radius: 0; background: transparent; cursor: pointer; + /* Type matches .main-tab-button so the nested strip reads like the outer + overview tab strip, not like the backup page's vertical sidebar. */ + font-size: 12px; + font-weight: 500; + color: var(--text-secondary, #ccc); } #overview-view #ov-pane-backups .backup-layout > .sidebar .category:hover { color: var(--accent); 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 e9db4ab..9786e7e 100644 --- a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js +++ b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js @@ -140,34 +140,49 @@ class OverviewManager { } // The shared in-content header. Sourced only from OV_TAB_META so every fleet - // tab matches the per-app detail tabs' .config-title (emoji + title + blurb, - // inside the pane, above the body). renderTab() prepends this for the string - // tabs; mountMigrate() injects it once for the static Migrate pane. - renderHeader(id) { + // tab matches the per-app detail tabs' title block (emoji + title + blurb + // left, action buttons right, divider underneath — the .backup-title shape). + // renderTab() prepends this for the string tabs; mountMigrate() injects it + // once for the static Migrate pane. + renderHeader(id, actions = '') { const m = OV_TAB_META[id]; if (!m) return ''; - return `

${m.icon} ${this.escape(m.title)}

${this.escape(m.desc)}

`; + return `
+

${m.icon} ${this.escape(m.title)}

${this.escape(m.desc)}

+ ${actions ? `
${actions}
` : ''} +
`; } renderTab(id) { const pane = document.querySelector(`#overview-view .tab-pane[data-tab="${id}"]`); if (!pane) return; - // Every string-rendered tab leads with the shared in-content header, so the - // body renderers below only ever produce the body (never a heading). + // Every string-rendered tab follows the per-app detail tab idiom: the + // shared header (title + actions + divider) on top, then the body inside + // the recessed .ov-tab-body container — so the renderers below only ever + // produce the body (never a heading or its action buttons). + const checkBtn = (label) => ``; + const body = (html) => `
${html}
`; switch (id) { - case 'overview': pane.innerHTML = this.renderHeader(id) + this.renderOverview(); break; + case 'overview': + pane.innerHTML = this.renderHeader(id, checkBtn('Check now')) + body(this.renderOverview()); + break; case 'updates': { // Preserve which rows are expanded across a rebuild — a background // task-refresh repaints the whole table, so restore ALL open rows, not // just the single ?app= deep-link. const open = Array.from(document.querySelectorAll('#overview-view .ov-row-details:not([hidden])')) .map((d) => d.id.replace(/^ov-detail-/, '')); - pane.innerHTML = this.renderHeader(id) + this.renderUpdates(); + const anyUpdate = !!(this.updater && this.updater.apps.some((a) => a.update_available)); + const actions = checkBtn('Check') + + (anyUpdate ? ` ` : ''); + pane.innerHTML = this.renderHeader(id, actions) + body(this.renderUpdates()); open.forEach((app) => this._openDetail(app)); this._honorAppDeepLink(); break; } - case 'improvements': pane.innerHTML = this.renderHeader(id) + this.renderImprovements(); break; + case 'improvements': + pane.innerHTML = this.renderHeader(id, checkBtn('Check')) + body(this.renderImprovements()); + break; case 'backups': this.mountBackupCenter(pane); break; case 'migrate': this.mountMigrate(); break; } @@ -310,8 +325,7 @@ class OverviewManager { ``)} ${card('backups', b.protectedLabel, 'Backups protected', b.sub, ``)} - ${card('system', u.apps, 'Apps tracked', `last scan: ${checked}`, - ``)} + ${card('system', u.apps, 'Apps tracked', `last scan: ${checked}`)} ${(this.updater && this.updater.updates) ? '' : `
No scan data yet — run Check now to fetch versions, CVEs & improvements.
`}`; } @@ -341,19 +355,16 @@ class OverviewManager { const up = this.updater; if (!up || !up.apps.length) return `
No installed apps to track.
`; const shown = this.filterApps(up.apps); - const anyUpdate = up.apps.some((a) => a.update_available); const nUpd = up.apps.filter((a) => a.update_available).length; const nSec = up.apps.filter((a) => (a.cves || []).length).length; const chip = (id, label, n) => ``; const rows = shown.map((a) => this.updateRow(a)).join('') || `
Nothing matches this filter.
`; + // Check / Update all live in the tab header's action slot (renderTab), so + // the toolbar is just the filter chips. return `
${chip('all', 'All', up.apps.length)}${chip('updates', 'Updates', nUpd)}${chip('security', 'Security', nSec)}
-
- - ${anyUpdate ? `` : ''} -
${rows}
`; } @@ -434,7 +445,9 @@ class OverviewManager { // ---- Improvements tab (reuse the updater's hotfix renderer) --------------- renderImprovements() { - return this.updater ? this.updater.renderImprovements() : `
Improvements unavailable.
`; + // withToolbar=false — the fleet tab's "Check" button lives in the shared + // header action slot, so the embedded renderer skips its own toolbar. + return this.updater ? this.updater.renderImprovements(false) : `
Improvements unavailable.
`; } // ---- Backups tab (fleet health glance; actions deep-link per app) --------- diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index eb1e774..82be798 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -302,7 +302,9 @@ class UpdaterPage {
${rows}
`; } - renderImprovements() { + // withToolbar=false lets an embedding surface (the fleet Overview tab) skip + // the inline Check button because it provides one in its own header. + renderImprovements(withToolbar = true) { if (!this.artifacts) return this.empty('No hotfix data yet. Run a check to fetch the signed improvements index.', true); const list = Array.isArray(this.artifacts.artifacts) ? this.artifacts.artifacts : []; const signed = !!this.artifacts.signed; @@ -331,7 +333,7 @@ class UpdaterPage { ? `
Small, signed, individually-reversible improvements curated by the LibrePortal team. Security & breakage fixes apply automatically (a snapshot is taken first); the rest are one click. Every apply is logged in History and can be reverted.
` : `
⚠ The improvements index is unsigned (signing not activated on this build) — applying is disabled for safety.
`; return `${banner} -
+ ${withToolbar ? `
` : ''}
${rows}
`; } diff --git a/containers/libreportal/frontend/themes/nebula/theme.css b/containers/libreportal/frontend/themes/nebula/theme.css index d8a11f7..6519482 100644 --- a/containers/libreportal/frontend/themes/nebula/theme.css +++ b/containers/libreportal/frontend/themes/nebula/theme.css @@ -109,10 +109,15 @@ 1. .tab-navigation — main app tabs (Config / Services / …) and the Backup-location sub-tabs (Local / Remote 1 / Remote 2). 2. .tabs-list inside .tabs-wrapper — config sub-tabs that the - app-config form renders inside the Config tab. */ + app-config form renders inside the Config tab. + 3. The fleet Overview's Backups tab, where the embedded BackupPage + sidebar is restyled into a nested horizontal strip + joined + content card (overview.css) — same surface, same anchor. */ [data-theme="nebula"] .tab-navigation, [data-theme="nebula"] .tabs-wrapper .tabs-list, -[data-theme="nebula"] .tabs-content { +[data-theme="nebula"] .tabs-content, +[data-theme="nebula"] #overview-view #ov-pane-backups .backup-layout > .sidebar #backup-sidebar-list, +[data-theme="nebula"] #overview-view #ov-pane-backups .backup-layout > .main { background: var(--sidebar-bg); }