feat(webui): unify Overview tab headers as in-content, app-detail style
Drop the floating fleet header; every Overview tab now renders its heading inside the pane, under the tab strip, in the per-app detail .config-title format (emoji + title + description) — matching the Migrate tab. Introduces a small modular system so the area can't drift: - OV_TAB_META is the single source of truth for each tab's icon/title/blurb. - renderHeader(id) is the only thing that turns it into markup; renderTab() prepends it for the string tabs and mountMigrate() injects it once for the static Migrate pane. Body renderers now only ever produce the body. Retires the now-dead floating-header plumbing: updateHeader(), the ov-backups-active/ov-migrate-active hide toggles + CSS, and the .overview-header rules. The tabbed interface owns the top padding the header used to provide. Backups is the documented exception — it embeds the full BackupPage, which supplies its own header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0f9a76503c
commit
4d54d6a9b0
@ -279,14 +279,9 @@
|
||||
<!-- Fleet Overview View (Overview · Updates · Improvements · Backups).
|
||||
Driven by OverviewManager; shares the persistent apps sidebar. -->
|
||||
<div id="overview-view" class="content-view">
|
||||
<div class="overview-header">
|
||||
<div class="overview-header-text">
|
||||
<h2 id="overview-title">Overview</h2>
|
||||
<p id="overview-subtitle">Updates, improvements, and backups across all your apps.</p>
|
||||
</div>
|
||||
<button class="updater-btn" data-updater-action="check">↻ Check now</button>
|
||||
</div>
|
||||
|
||||
<!-- No floating header: every tab renders its own in-content .config-title
|
||||
header (icon + title + description) under the tab strip, just like the
|
||||
per-app detail tabs. See OverviewManager.renderHeader / OV_TAB_META. -->
|
||||
<div class="tabbed-interface overview-tabbed">
|
||||
<div class="tab-navigation">
|
||||
<button class="main-tab-button active" data-tab="overview" onclick="if(window.overviewManager) window.overviewManager.switchTab('overview')">
|
||||
@ -317,13 +312,8 @@
|
||||
<div class="tab-pane" data-tab="improvements" id="ov-pane-improvements"></div>
|
||||
<div class="tab-pane" data-tab="backups" id="ov-pane-backups"></div>
|
||||
<div class="tab-pane" data-tab="migrate" id="ov-pane-migrate">
|
||||
<!-- In-content header (mirrors the per-app Config tab's .config-title:
|
||||
icon + title + description sit inside the pane, above the sub-tabs).
|
||||
The generic fleet header is hidden for this tab (.ov-migrate-active). -->
|
||||
<div class="config-title">
|
||||
<h3>🔀 Migrate</h3>
|
||||
<p>Move apps in from another LibrePortal, and manage the peers you share backup locations with.</p>
|
||||
</div>
|
||||
<!-- The in-content header (.ov-tab-header) is injected here by
|
||||
mountMigrate() from OV_TAB_META, above the sub-tabs. -->
|
||||
<!-- Cross-host area: nested segmented sub-tabs (Config-tab design). -->
|
||||
<div class="tabs-wrapper ov-subtabs">
|
||||
<div class="tabs-list">
|
||||
|
||||
@ -36,18 +36,11 @@
|
||||
}
|
||||
.sidebar-overview-entry .ov-entry-icon { width: 18px; height: 18px; flex: 0 0 auto; }
|
||||
|
||||
/* ---- header ------------------------------------------------------------- */
|
||||
.overview-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 20px 24px 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.overview-header h2 { margin: 0 0 2px; font-size: 1.5rem; }
|
||||
.overview-header p { margin: 0; color: var(--text-muted, rgba(255, 255, 255, .65)); font-size: .9rem; }
|
||||
.overview-tabbed { padding: 0 24px 24px; }
|
||||
/* ---- layout ------------------------------------------------------------- */
|
||||
/* No floating header — each tab renders its own in-content .config-title header
|
||||
under the tab strip (OverviewManager.renderHeader). The tabbed interface owns
|
||||
the area padding the old header used to provide at the top. */
|
||||
.overview-tabbed { margin-top: 0; padding: 22px 24px 24px; }
|
||||
|
||||
/* ---- Updates: filter chips + toolbar ------------------------------------ */
|
||||
.ov-toolbar {
|
||||
@ -132,12 +125,9 @@
|
||||
.updater-detail-empty { color: var(--text-muted, rgba(255, 255, 255, .6)); margin: 0; }
|
||||
|
||||
/* ---- 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,
|
||||
#overview-view.ov-migrate-active .overview-header { display: none; }
|
||||
|
||||
/* The Backups tab mounts the real BackupPage, which supplies its own 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-pane-backups .backup-layout { display: block; }
|
||||
#overview-view #ov-pane-backups .backup-layout > .sidebar {
|
||||
width: auto;
|
||||
|
||||
@ -12,6 +12,24 @@
|
||||
// health is read directly from the same generated JSON the backup page uses.
|
||||
// System config edits are never inline here — they bounce to Admin / the per-app
|
||||
// tabs (operate/view here; change-the-system there).
|
||||
|
||||
// Single source of truth for every fleet tab's in-content header. Each tab
|
||||
// renders the SAME .config-title block the per-app detail tabs use — emoji +
|
||||
// title + description, sitting inside the pane, directly above the body and
|
||||
// under the tab strip. renderHeader() is the only thing that turns this into
|
||||
// markup, and renderTab()/mountMigrate() are the only callers, so the area
|
||||
// cannot drift from the shared app-detail header format: to change a heading,
|
||||
// edit it HERE and nowhere else. (Backups is the one exception — it embeds the
|
||||
// full BackupPage, which supplies its own header — so it has no entry to render
|
||||
// but is listed for completeness.)
|
||||
const OV_TAB_META = {
|
||||
overview: { icon: '🛰️', title: 'Overview', desc: 'Updates, improvements, and backups across all your apps.' },
|
||||
updates: { icon: '⬆️', title: 'Updates', desc: 'Available versions per app — expand a row for CVEs, recovery, and history. Every update is snapshotted first.' },
|
||||
improvements: { icon: '✨', title: 'Improvements', desc: 'Signed, individually-reversible hotfixes from the LibrePortal team — applied with a snapshot first.' },
|
||||
backups: { icon: '💾', title: 'Backups', desc: 'Backup health across your apps. Open an app for snapshots and restore.' },
|
||||
migrate: { icon: '🔀', title: 'Migrate', desc: 'Move apps in from another LibrePortal, and manage the peers you share backup locations with.' },
|
||||
};
|
||||
|
||||
class OverviewManager {
|
||||
constructor(services) {
|
||||
this.services = services || (window.LP && window.LP.services) || {};
|
||||
@ -117,52 +135,39 @@ class OverviewManager {
|
||||
|
||||
_applyTab(id) {
|
||||
if (this.tabs) this.tabs.switch(id);
|
||||
// Backups embeds the full backup center (its own header + nested sub-tab
|
||||
// strip) and Migrate carries its own in-content .config-title header above
|
||||
// its sub-tabs — both supply their own heading, so hide the generic fleet
|
||||
// header for them.
|
||||
const root = document.getElementById('overview-view');
|
||||
if (root) {
|
||||
root.classList.toggle('ov-backups-active', id === 'backups');
|
||||
root.classList.toggle('ov-migrate-active', id === 'migrate');
|
||||
}
|
||||
this.updateHeader(id);
|
||||
this.renderTab(id);
|
||||
this.current = id;
|
||||
}
|
||||
|
||||
updateHeader(id) {
|
||||
const titles = {
|
||||
overview: ['Overview', 'Updates, improvements, and backups across all your apps.'],
|
||||
updates: ['Updates', 'Available versions per app — expand a row for CVEs, recovery, and history. Every update is snapshotted first.'],
|
||||
improvements: ['Improvements', 'Signed, individually-reversible hotfixes from the LibrePortal team — applied with a snapshot first.'],
|
||||
backups: ['Backups', 'Backup health across your apps. Open an app for snapshots and restore.'],
|
||||
migrate: ['Migrate', 'Move apps in from another LibrePortal, and manage the peers you share backup locations with.'],
|
||||
};
|
||||
const t = titles[id] || titles.overview;
|
||||
const titleEl = document.getElementById('overview-title');
|
||||
const subEl = document.getElementById('overview-subtitle');
|
||||
if (titleEl) titleEl.textContent = t[0];
|
||||
if (subEl) subEl.textContent = t[1];
|
||||
// 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) {
|
||||
const m = OV_TAB_META[id];
|
||||
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>`;
|
||||
}
|
||||
|
||||
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).
|
||||
switch (id) {
|
||||
case 'overview': pane.innerHTML = this.renderOverview(); break;
|
||||
case 'overview': pane.innerHTML = this.renderHeader(id) + 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.renderUpdates();
|
||||
pane.innerHTML = this.renderHeader(id) + this.renderUpdates();
|
||||
open.forEach((app) => this._openDetail(app));
|
||||
this._honorAppDeepLink();
|
||||
break;
|
||||
}
|
||||
case 'improvements': pane.innerHTML = this.renderImprovements(); break;
|
||||
case 'improvements': pane.innerHTML = this.renderHeader(id) + this.renderImprovements(); break;
|
||||
case 'backups': this.mountBackupCenter(pane); break;
|
||||
case 'migrate': this.mountMigrate(); break;
|
||||
}
|
||||
@ -171,6 +176,12 @@ class OverviewManager {
|
||||
// ---- Migrate tab: Restore (standalone MigratePage) + Peers (PeersPage) ----
|
||||
|
||||
mountMigrate() {
|
||||
// The Migrate pane is static layout (its sub-tabs live in the HTML, not an
|
||||
// innerHTML rebuild), so inject the shared header once, above the sub-tabs.
|
||||
const pane = document.getElementById('ov-pane-migrate');
|
||||
if (pane && !pane.querySelector(':scope > .ov-tab-header')) {
|
||||
pane.insertAdjacentHTML('afterbegin', this.renderHeader('migrate'));
|
||||
}
|
||||
const seg = window.location.pathname.replace(/^\/overview\/migrate\/?/, '').split('/')[0];
|
||||
const sub = (seg === 'peers' || seg === 'restore') ? seg : (this._migrateSub || 'restore');
|
||||
this.switchMigrateSub(sub, { fromUrl: true });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user