Merge claude/1
This commit is contained in:
commit
58b76af311
@ -279,14 +279,9 @@
|
|||||||
<!-- Fleet Overview View (Overview · Updates · Improvements · Backups).
|
<!-- Fleet Overview View (Overview · Updates · Improvements · Backups).
|
||||||
Driven by OverviewManager; shares the persistent apps sidebar. -->
|
Driven by OverviewManager; shares the persistent apps sidebar. -->
|
||||||
<div id="overview-view" class="content-view">
|
<div id="overview-view" class="content-view">
|
||||||
<div class="overview-header">
|
<!-- No floating header: every tab renders its own in-content .config-title
|
||||||
<div class="overview-header-text">
|
header (icon + title + description) under the tab strip, just like the
|
||||||
<h2 id="overview-title">Overview</h2>
|
per-app detail tabs. See OverviewManager.renderHeader / OV_TAB_META. -->
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="tabbed-interface overview-tabbed">
|
<div class="tabbed-interface overview-tabbed">
|
||||||
<div class="tab-navigation">
|
<div class="tab-navigation">
|
||||||
<button class="main-tab-button active" data-tab="overview" onclick="if(window.overviewManager) window.overviewManager.switchTab('overview')">
|
<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="improvements" id="ov-pane-improvements"></div>
|
||||||
<div class="tab-pane" data-tab="backups" id="ov-pane-backups"></div>
|
<div class="tab-pane" data-tab="backups" id="ov-pane-backups"></div>
|
||||||
<div class="tab-pane" data-tab="migrate" id="ov-pane-migrate">
|
<div class="tab-pane" data-tab="migrate" id="ov-pane-migrate">
|
||||||
<!-- In-content header (mirrors the per-app Config tab's .config-title:
|
<!-- The in-content header (.ov-tab-header) is injected here by
|
||||||
icon + title + description sit inside the pane, above the sub-tabs).
|
mountMigrate() from OV_TAB_META, 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>
|
|
||||||
<!-- Cross-host area: nested segmented sub-tabs (Config-tab design). -->
|
<!-- Cross-host area: nested segmented sub-tabs (Config-tab design). -->
|
||||||
<div class="tabs-wrapper ov-subtabs">
|
<div class="tabs-wrapper ov-subtabs">
|
||||||
<div class="tabs-list">
|
<div class="tabs-list">
|
||||||
|
|||||||
@ -36,18 +36,11 @@
|
|||||||
}
|
}
|
||||||
.sidebar-overview-entry .ov-entry-icon { width: 18px; height: 18px; flex: 0 0 auto; }
|
.sidebar-overview-entry .ov-entry-icon { width: 18px; height: 18px; flex: 0 0 auto; }
|
||||||
|
|
||||||
/* ---- header ------------------------------------------------------------- */
|
/* ---- layout ------------------------------------------------------------- */
|
||||||
.overview-header {
|
/* No floating header — each tab renders its own in-content .config-title header
|
||||||
display: flex;
|
under the tab strip (OverviewManager.renderHeader). The tabbed interface owns
|
||||||
align-items: flex-end;
|
the area padding the old header used to provide at the top. */
|
||||||
justify-content: space-between;
|
.overview-tabbed { margin-top: 0; padding: 22px 24px 24px; }
|
||||||
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; }
|
|
||||||
|
|
||||||
/* ---- Updates: filter chips + toolbar ------------------------------------ */
|
/* ---- Updates: filter chips + toolbar ------------------------------------ */
|
||||||
.ov-toolbar {
|
.ov-toolbar {
|
||||||
@ -132,12 +125,9 @@
|
|||||||
.updater-detail-empty { color: var(--text-muted, rgba(255, 255, 255, .6)); margin: 0; }
|
.updater-detail-empty { color: var(--text-muted, rgba(255, 255, 255, .6)); margin: 0; }
|
||||||
|
|
||||||
/* ---- Backups tab: embedded backup center -------------------------------- */
|
/* ---- Backups tab: embedded backup center -------------------------------- */
|
||||||
/* The Backups tab mounts the real BackupPage. Its own page-header replaces the
|
/* The Backups tab mounts the real BackupPage, which supplies its own header, and
|
||||||
generic fleet header, and its left sidebar is restyled into a horizontal
|
its left sidebar is restyled into a horizontal nested tab strip so the whole
|
||||||
nested tab strip so the whole thing reads as tabs-within-tabs. */
|
thing reads as tabs-within-tabs. */
|
||||||
#overview-view.ov-backups-active .overview-header,
|
|
||||||
#overview-view.ov-migrate-active .overview-header { display: none; }
|
|
||||||
|
|
||||||
#overview-view #ov-pane-backups .backup-layout { display: block; }
|
#overview-view #ov-pane-backups .backup-layout { display: block; }
|
||||||
#overview-view #ov-pane-backups .backup-layout > .sidebar {
|
#overview-view #ov-pane-backups .backup-layout > .sidebar {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|||||||
@ -12,6 +12,24 @@
|
|||||||
// health is read directly from the same generated JSON the backup page uses.
|
// 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
|
// System config edits are never inline here — they bounce to Admin / the per-app
|
||||||
// tabs (operate/view here; change-the-system there).
|
// 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 {
|
class OverviewManager {
|
||||||
constructor(services) {
|
constructor(services) {
|
||||||
this.services = services || (window.LP && window.LP.services) || {};
|
this.services = services || (window.LP && window.LP.services) || {};
|
||||||
@ -117,52 +135,39 @@ class OverviewManager {
|
|||||||
|
|
||||||
_applyTab(id) {
|
_applyTab(id) {
|
||||||
if (this.tabs) this.tabs.switch(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.renderTab(id);
|
||||||
this.current = id;
|
this.current = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeader(id) {
|
// The shared in-content header. Sourced only from OV_TAB_META so every fleet
|
||||||
const titles = {
|
// tab matches the per-app detail tabs' .config-title (emoji + title + blurb,
|
||||||
overview: ['Overview', 'Updates, improvements, and backups across all your apps.'],
|
// inside the pane, above the body). renderTab() prepends this for the string
|
||||||
updates: ['Updates', 'Available versions per app — expand a row for CVEs, recovery, and history. Every update is snapshotted first.'],
|
// tabs; mountMigrate() injects it once for the static Migrate pane.
|
||||||
improvements: ['Improvements', 'Signed, individually-reversible hotfixes from the LibrePortal team — applied with a snapshot first.'],
|
renderHeader(id) {
|
||||||
backups: ['Backups', 'Backup health across your apps. Open an app for snapshots and restore.'],
|
const m = OV_TAB_META[id];
|
||||||
migrate: ['Migrate', 'Move apps in from another LibrePortal, and manage the peers you share backup locations with.'],
|
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>`;
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
// body renderers below only ever produce the body (never a heading).
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'overview': pane.innerHTML = this.renderOverview(); break;
|
case 'overview': pane.innerHTML = this.renderHeader(id) + 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.renderUpdates();
|
pane.innerHTML = this.renderHeader(id) + 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.renderImprovements(); break;
|
case 'improvements': pane.innerHTML = this.renderHeader(id) + 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;
|
||||||
}
|
}
|
||||||
@ -171,6 +176,12 @@ class OverviewManager {
|
|||||||
// ---- Migrate tab: Restore (standalone MigratePage) + Peers (PeersPage) ----
|
// ---- Migrate tab: Restore (standalone MigratePage) + Peers (PeersPage) ----
|
||||||
|
|
||||||
mountMigrate() {
|
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 seg = window.location.pathname.replace(/^\/overview\/migrate\/?/, '').split('/')[0];
|
||||||
const sub = (seg === 'peers' || seg === 'restore') ? seg : (this._migrateSub || 'restore');
|
const sub = (seg === 'peers' || seg === 'restore') ? seg : (this._migrateSub || 'restore');
|
||||||
this.switchMigrateSub(sub, { fromUrl: true });
|
this.switchMigrateSub(sub, { fromUrl: true });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user