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 `
`;
+ // 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);
}