Compare commits

..

2 Commits

Author SHA1 Message Date
librelad
0e8f645334 Merge claude/2 2026-06-12 18:55:10 +01:00
librelad
a06b6cd1d8 feat(overview): match fleet tab content to the app-detail tab layout
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 <librelad@digitalangels.vip>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:55:10 +01:00
4 changed files with 74 additions and 22 deletions

View File

@ -42,6 +42,33 @@
the area padding the old header used to provide at the top. */ the area padding the old header used to provide at the top. */
.overview-tabbed { margin-top: 0; padding: 22px 24px 24px; } .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 ------------------------------------ */ /* ---- Updates: filter chips + toolbar ------------------------------------ */
.ov-toolbar { .ov-toolbar {
display: flex; display: flex;
@ -164,6 +191,11 @@
border-radius: 0; border-radius: 0;
background: transparent; background: transparent;
cursor: pointer; 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 { #overview-view #ov-pane-backups .backup-layout > .sidebar .category:hover {
color: var(--accent); color: var(--accent);

View File

@ -140,34 +140,49 @@ class OverviewManager {
} }
// The shared in-content header. Sourced only from OV_TAB_META so every fleet // 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, // tab matches the per-app detail tabs' title block (emoji + title + blurb
// inside the pane, above the body). renderTab() prepends this for the string // left, action buttons right, divider underneath — the .backup-title shape).
// tabs; mountMigrate() injects it once for the static Migrate pane. // renderTab() prepends this for the string tabs; mountMigrate() injects it
renderHeader(id) { // once for the static Migrate pane.
renderHeader(id, actions = '') {
const m = OV_TAB_META[id]; const m = OV_TAB_META[id];
if (!m) return ''; if (!m) return '';
return `<div class="config-title ov-tab-header"><h3>${m.icon} ${this.escape(m.title)}</h3><p>${this.escape(m.desc)}</p></div>`; return `<div class="config-title ov-tab-header">
<div class="ov-tab-header-main"><h3>${m.icon} ${this.escape(m.title)}</h3><p>${this.escape(m.desc)}</p></div>
${actions ? `<div class="ov-tab-header-actions">${actions}</div>` : ''}
</div>`;
} }
renderTab(id) { renderTab(id) {
const pane = document.querySelector(`#overview-view .tab-pane[data-tab="${id}"]`); const pane = document.querySelector(`#overview-view .tab-pane[data-tab="${id}"]`);
if (!pane) return; if (!pane) return;
// Every string-rendered tab leads with the shared in-content header, so the // Every string-rendered tab follows the per-app detail tab idiom: the
// body renderers below only ever produce the body (never a heading). // 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) => `<button class="updater-btn" data-updater-action="check">↻ ${label}</button>`;
const body = (html) => `<div class="ov-tab-body">${html}</div>`;
switch (id) { 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': { case 'updates': {
// Preserve which rows are expanded across a rebuild — a background // Preserve which rows are expanded across a rebuild — a background
// task-refresh repaints the whole table, so restore ALL open rows, not // task-refresh repaints the whole table, so restore ALL open rows, not
// just the single ?app= deep-link. // just the single ?app= deep-link.
const open = Array.from(document.querySelectorAll('#overview-view .ov-row-details:not([hidden])')) const open = Array.from(document.querySelectorAll('#overview-view .ov-row-details:not([hidden])'))
.map((d) => d.id.replace(/^ov-detail-/, '')); .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 ? ` <button class="updater-btn updater-btn-primary" data-updater-action="update-all">Update all</button>` : '');
pane.innerHTML = this.renderHeader(id, actions) + body(this.renderUpdates());
open.forEach((app) => this._openDetail(app)); open.forEach((app) => this._openDetail(app));
this._honorAppDeepLink(); this._honorAppDeepLink();
break; 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 'backups': this.mountBackupCenter(pane); break;
case 'migrate': this.mountMigrate(); break; case 'migrate': this.mountMigrate(); break;
} }
@ -310,8 +325,7 @@ class OverviewManager {
`<button class="updater-btn" data-overview-action="goto" data-tab="improvements">View</button>`)} `<button class="updater-btn" data-overview-action="goto" data-tab="improvements">View</button>`)}
${card('backups', b.protectedLabel, 'Backups protected', b.sub, ${card('backups', b.protectedLabel, 'Backups protected', b.sub,
`<button class="updater-btn" data-overview-action="goto" data-tab="backups">Backups</button>`)} `<button class="updater-btn" data-overview-action="goto" data-tab="backups">Backups</button>`)}
${card('system', u.apps, 'Apps tracked', `last scan: ${checked}`, ${card('system', u.apps, 'Apps tracked', `last scan: ${checked}`)}
`<button class="updater-btn" data-updater-action="check">Check now</button>`)}
</div> </div>
${(this.updater && this.updater.updates) ? '' : `<div class="updater-hint">No scan data yet — run <strong>Check now</strong> to fetch versions, CVEs &amp; improvements.</div>`}`; ${(this.updater && this.updater.updates) ? '' : `<div class="updater-hint">No scan data yet — run <strong>Check now</strong> to fetch versions, CVEs &amp; improvements.</div>`}`;
} }
@ -341,19 +355,16 @@ class OverviewManager {
const up = this.updater; const up = this.updater;
if (!up || !up.apps.length) return `<div class="updater-empty">No installed apps to track.</div>`; if (!up || !up.apps.length) return `<div class="updater-empty">No installed apps to track.</div>`;
const shown = this.filterApps(up.apps); 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 nUpd = up.apps.filter((a) => a.update_available).length;
const nSec = up.apps.filter((a) => (a.cves || []).length).length; const nSec = up.apps.filter((a) => (a.cves || []).length).length;
const chip = (id, label, n) => const chip = (id, label, n) =>
`<button class="ov-chip${this.filter === id ? ' active' : ''}" data-overview-action="filter" data-filter="${id}">${label}${n != null ? ` <span class="ov-chip-n">${n}</span>` : ''}</button>`; `<button class="ov-chip${this.filter === id ? ' active' : ''}" data-overview-action="filter" data-filter="${id}">${label}${n != null ? ` <span class="ov-chip-n">${n}</span>` : ''}</button>`;
const rows = shown.map((a) => this.updateRow(a)).join('') || `<div class="updater-empty">Nothing matches this filter.</div>`; const rows = shown.map((a) => this.updateRow(a)).join('') || `<div class="updater-empty">Nothing matches this filter.</div>`;
// Check / Update all live in the tab header's action slot (renderTab), so
// the toolbar is just the filter chips.
return ` return `
<div class="updater-toolbar ov-toolbar"> <div class="updater-toolbar ov-toolbar">
<div class="ov-chips">${chip('all', 'All', up.apps.length)}${chip('updates', 'Updates', nUpd)}${chip('security', 'Security', nSec)}</div> <div class="ov-chips">${chip('all', 'All', up.apps.length)}${chip('updates', 'Updates', nUpd)}${chip('security', 'Security', nSec)}</div>
<div class="ov-toolbar-actions">
<button class="updater-btn" data-updater-action="check"> Check</button>
${anyUpdate ? `<button class="updater-btn updater-btn-primary" data-updater-action="update-all">Update all</button>` : ''}
</div>
</div> </div>
<div class="updater-list ov-updates-list">${rows}</div>`; <div class="updater-list ov-updates-list">${rows}</div>`;
} }
@ -434,7 +445,9 @@ class OverviewManager {
// ---- Improvements tab (reuse the updater's hotfix renderer) --------------- // ---- Improvements tab (reuse the updater's hotfix renderer) ---------------
renderImprovements() { renderImprovements() {
return this.updater ? this.updater.renderImprovements() : `<div class="updater-empty">Improvements unavailable.</div>`; // 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) : `<div class="updater-empty">Improvements unavailable.</div>`;
} }
// ---- Backups tab (fleet health glance; actions deep-link per app) --------- // ---- Backups tab (fleet health glance; actions deep-link per app) ---------

View File

@ -302,7 +302,9 @@ class UpdaterPage {
<div class="updater-list">${rows}</div>`; <div class="updater-list">${rows}</div>`;
} }
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); 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 list = Array.isArray(this.artifacts.artifacts) ? this.artifacts.artifacts : [];
const signed = !!this.artifacts.signed; const signed = !!this.artifacts.signed;
@ -331,7 +333,7 @@ class UpdaterPage {
? `<div class="updater-hint">Small, signed, individually-reversible improvements curated by the LibrePortal team. Security &amp; breakage fixes apply automatically (a snapshot is taken first); the rest are one click. Every apply is logged in History and can be reverted.</div>` ? `<div class="updater-hint">Small, signed, individually-reversible improvements curated by the LibrePortal team. Security &amp; breakage fixes apply automatically (a snapshot is taken first); the rest are one click. Every apply is logged in History and can be reverted.</div>`
: `<div class="updater-hint">⚠ The improvements index is <strong>unsigned</strong> (signing not activated on this build) — applying is disabled for safety.</div>`; : `<div class="updater-hint">⚠ The improvements index is <strong>unsigned</strong> (signing not activated on this build) — applying is disabled for safety.</div>`;
return `${banner} return `${banner}
<div class="updater-toolbar"><button class="updater-btn" data-updater-action="check"> Check for improvements</button></div> ${withToolbar ? `<div class="updater-toolbar"><button class="updater-btn" data-updater-action="check">↻ Check for improvements</button></div>` : ''}
<div class="updater-list">${rows}</div>`; <div class="updater-list">${rows}</div>`;
} }

View File

@ -109,10 +109,15 @@
1. .tab-navigation main app tabs (Config / Services / ) and 1. .tab-navigation main app tabs (Config / Services / ) and
the Backup-location sub-tabs (Local / Remote 1 / Remote 2). the Backup-location sub-tabs (Local / Remote 1 / Remote 2).
2. .tabs-list inside .tabs-wrapper config sub-tabs that the 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"] .tab-navigation,
[data-theme="nebula"] .tabs-wrapper .tabs-list, [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); background: var(--sidebar-bg);
} }