Compare commits
2 Commits
de5621746d
...
9a58869899
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a58869899 | ||
|
|
0bcde854e6 |
@ -70,7 +70,7 @@ class OverviewPage {
|
||||
|
||||
go(where) {
|
||||
if (where === 'backup') {
|
||||
window.spaClean?.navigate('/backup', true);
|
||||
window.spaClean?.navigate('/apps/overview/backups', true);
|
||||
} else if (where === 'ssh' || where === 'security' || where === 'system') {
|
||||
const target = where === 'ssh' ? 'ssh-access' : where;
|
||||
window.history.pushState({}, '', window.adminPath(target));
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<!-- Fleet Overview entry — pinned above the search box, opens the
|
||||
Overview · Updates · Improvements · Backups tabs in the main pane. -->
|
||||
<div class="sidebar-overview-entry" id="sidebar-overview-entry" role="button" tabindex="0"
|
||||
onclick="if(window.navigateToRoute){window.navigateToRoute('/overview');}else{window.location.href='/overview';}"
|
||||
onclick="if(window.navigateToRoute){window.navigateToRoute('/apps/overview');}else{window.location.href='/apps/overview';}"
|
||||
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();this.click();}">
|
||||
<svg class="ov-entry-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="3" y="3" width="7" height="9"></rect>
|
||||
@ -210,7 +210,7 @@
|
||||
</svg>
|
||||
Backup now
|
||||
</button>
|
||||
<a class="btn btn-secondary" href="/backup" onclick="event.preventDefault(); if(window.navigateToRoute){window.navigateToRoute('/backup');}else{window.location.href='/backup';}">
|
||||
<a class="btn btn-secondary" href="/apps/overview/backups" onclick="event.preventDefault(); if(window.navigateToRoute){window.navigateToRoute('/apps/overview/backups');}else{window.location.href='/apps/overview/backups';}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||
<polyline points="15 3 21 3 21 9"></polyline>
|
||||
|
||||
@ -497,9 +497,9 @@ class AppsManager {
|
||||
chip.style.cursor = 'pointer';
|
||||
chip.style.display = '';
|
||||
chip.onclick = () => {
|
||||
if (typeof window.navigateToRoute === 'function') window.navigateToRoute('/overview/improvements');
|
||||
else if (typeof window.spaClean === 'function') window.spaClean('/overview/improvements');
|
||||
else window.location.href = '/overview/improvements';
|
||||
if (typeof window.navigateToRoute === 'function') window.navigateToRoute('/apps/overview/improvements');
|
||||
else if (typeof window.spaClean === 'function') window.spaClean('/apps/overview/improvements');
|
||||
else window.location.href = '/apps/overview/improvements';
|
||||
};
|
||||
} catch (_) { /* best-effort */ }
|
||||
}
|
||||
|
||||
@ -4,9 +4,7 @@
|
||||
"/apps",
|
||||
"/apps*",
|
||||
"/app",
|
||||
"/app*",
|
||||
"/overview",
|
||||
"/overview*"
|
||||
"/app*"
|
||||
],
|
||||
"module": "/components/apps/index.js",
|
||||
"handler": "handleApps",
|
||||
|
||||
@ -6,12 +6,15 @@
|
||||
// sibling component; it's the same feature, so it lives here.)
|
||||
LP.features.register({
|
||||
id: 'apps',
|
||||
routes: ['/apps', '/apps*', '/app', '/app*', '/overview', '/overview*'],
|
||||
routes: ['/apps', '/apps*', '/app', '/app*'],
|
||||
|
||||
async mount(ctx) {
|
||||
// /overview* -> fleet Overview; /apps* -> grid; everything else (/app*) ->
|
||||
// detail. Check '/apps' before '/app' (since '/apps'.startsWith('/app')).
|
||||
if (window.location.pathname.startsWith('/overview')) {
|
||||
// /apps/overview* -> fleet Overview; /apps* -> grid; everything else
|
||||
// (/app*) -> detail. The Overview check must precede the bare '/apps' one
|
||||
// (since '/apps/overview'.startsWith('/apps')), and '/apps' before '/app'
|
||||
// (since '/apps'.startsWith('/app')). Legacy /overview* is rewritten to
|
||||
// /apps/overview* by _legacyRedirect() before this runs.
|
||||
if (window.location.pathname.startsWith('/apps/overview')) {
|
||||
return this._mountOverview(ctx);
|
||||
}
|
||||
if (window.location.pathname.startsWith('/apps')) {
|
||||
@ -78,7 +81,7 @@ LP.features.register({
|
||||
await window.appTabbedManager.initialize();
|
||||
},
|
||||
|
||||
// ---- fleet Overview (/overview[/<tab>]) ----
|
||||
// ---- fleet Overview (/apps/overview[/<tab>]) ----
|
||||
async _mountOverview(ctx) {
|
||||
// Lazy-load the fleet controller + its deps. Guard by typeof so re-entry
|
||||
// never re-declares the classes (loadScripts dedupes by URL too). The
|
||||
|
||||
@ -124,7 +124,7 @@ class OverviewManager {
|
||||
|
||||
parseTabFromUrl() {
|
||||
const allowed = new Set(['overview', 'updates', 'improvements', 'backups', 'migrate']);
|
||||
const seg = window.location.pathname.replace(/^\/overview\/?/, '').split('/')[0];
|
||||
const seg = window.location.pathname.replace(/^\/apps\/overview\/?/, '').split('/')[0];
|
||||
return (seg && allowed.has(seg)) ? seg : null;
|
||||
}
|
||||
|
||||
@ -134,7 +134,7 @@ class OverviewManager {
|
||||
switchTab(id) {
|
||||
if (!id || id === this.current) return;
|
||||
this._applyTab(id);
|
||||
const url = id === 'overview' ? '/overview' : `/overview/${id}`;
|
||||
const url = id === 'overview' ? '/apps/overview' : `/apps/overview/${id}`;
|
||||
this._pushUrl(url, false);
|
||||
}
|
||||
|
||||
@ -212,7 +212,7 @@ class OverviewManager {
|
||||
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(/^\/apps\/overview\/migrate\/?/, '').split('/')[0];
|
||||
const sub = (seg === 'peers' || seg === 'restore') ? seg : (this._migrateSub || 'restore');
|
||||
this.switchMigrateSub(sub, { fromUrl: true });
|
||||
}
|
||||
@ -225,7 +225,7 @@ class OverviewManager {
|
||||
pane.querySelectorAll('.ov-subtabs-content .tab-panel[data-ov-subtab]').forEach((p) => p.classList.toggle('active', p.dataset.ovSubtab === sub));
|
||||
if (sub === 'restore') this._mountRestore();
|
||||
else if (sub === 'peers') this._mountPeers();
|
||||
if (!opts.fromUrl) this._pushUrl(`/overview/migrate/${sub}`, true);
|
||||
if (!opts.fromUrl) this._pushUrl(`/apps/overview/migrate/${sub}`, true);
|
||||
}
|
||||
|
||||
async _mountRestore() {
|
||||
@ -601,9 +601,9 @@ class OverviewManager {
|
||||
// configuration sections (its sidebar restyled as a nested tab strip). On a
|
||||
// revisit we refresh rather than re-mount, to keep its sub-tab + expand state.
|
||||
mountBackupCenter(pane) {
|
||||
// Honor an optional sub-tab deep-link (/overview/backups/<sub>), e.g. the
|
||||
// Restore empty-state's "Open Locations" button.
|
||||
const seg = window.location.pathname.replace(/^\/overview\/backups\/?/, '').split('/')[0];
|
||||
// Honor an optional sub-tab deep-link (/apps/overview/backups/<sub>), e.g.
|
||||
// the Restore empty-state's "Open Locations" button.
|
||||
const seg = window.location.pathname.replace(/^\/apps\/overview\/backups\/?/, '').split('/')[0];
|
||||
const sub = ['dashboard', 'backups', 'locations', 'configuration'].includes(seg) ? seg : null;
|
||||
// Detect a prior mount by the embedded fragment in THIS pane — not by
|
||||
// #backup-section, which the per-app Backups tab also defines.
|
||||
|
||||
@ -68,7 +68,7 @@ class MigratePage {
|
||||
}
|
||||
const locBtn = e.target.closest('[data-action="go-to-locations"]');
|
||||
if (locBtn && this.root()?.contains(locBtn)) {
|
||||
if (window.navigateToRoute) window.navigateToRoute('/overview/backups/locations');
|
||||
if (window.navigateToRoute) window.navigateToRoute('/apps/overview/backups/locations');
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('#ov-migrate-confirm')) { this.confirmMigrate(); return; }
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"id": "backup",
|
||||
"routes": ["/backup", "/backup*"],
|
||||
"module": "/components/backup/index.js",
|
||||
"handler": "handleBackup",
|
||||
"navId": "nav-backup",
|
||||
"order": 50
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
// components/backup/index.js — Backup Center as a self-contained feature module.
|
||||
//
|
||||
// FIRST page migrated to the feature-module contract (docs/architecture/webui-architecture.md).
|
||||
// The kernel drives mount()/unmount() for the /backup route instead of
|
||||
// spa.js's handleBackup(). The heavy controller (backup-page.js, ~129KB) is
|
||||
// still lazy-loaded on first mount, so cold-load cost is unchanged.
|
||||
//
|
||||
// Snap-out demo: deleting this folder removes the backup route's module
|
||||
// registration; the manifest entry then falls back to the legacy handler
|
||||
// (and, once handleBackup is retired, to the kernel's not-found route). The
|
||||
// full decomposition of backup-page.js into per-tab modules is Phase 5.
|
||||
LP.features.register({
|
||||
id: 'backup',
|
||||
routes: ['/backup', '/backup*'],
|
||||
// Controllers the feature needs; lazy-loaded on first mount (idempotent).
|
||||
// Controllers, organised by sub-system (tabs). core/ first: schema + base
|
||||
// class + the shared data/cron, then each tab's prototype-augment clusters.
|
||||
scripts: [
|
||||
'/components/backup/core/js/backup-schema.js',
|
||||
'/components/backup/core/js/backup-page.js', // base: class + constructor + init/switchTab/render
|
||||
'/components/backup/core/js/backup-fetch-client.js',
|
||||
'/components/backup/core/js/backup-cron-schedule.js',
|
||||
'/components/backup/dashboard/js/backup-dashboard.js',
|
||||
'/components/backup/snapshots/js/backup-snapshots.js',
|
||||
'/components/backup/snapshots/js/backup-snapshot-actions.js',
|
||||
'/components/backup/locations/js/backup-locations.js',
|
||||
'/components/backup/locations/js/backup-location-fields.js',
|
||||
'/components/backup/locations/js/backup-location-modal.js',
|
||||
'/components/backup/locations/js/backup-ssh-key.js',
|
||||
'/components/backup/configuration/js/backup-configuration.js',
|
||||
'/components/backup/configuration/js/backup-retention-presets.js',
|
||||
'/components/backup/configuration/js/backup-engine-details.js',
|
||||
'/core/backup-card/js/backup-app-card.js',
|
||||
],
|
||||
|
||||
async mount(ctx) {
|
||||
await ctx.loadScripts(this.scripts);
|
||||
const html = await ctx.loadFragment('/components/backup/core/html/backup-content.html');
|
||||
ctx.setContent(html, 'Backups');
|
||||
if (typeof BackupPage === 'undefined') {
|
||||
throw new Error('BackupPage controller failed to load');
|
||||
}
|
||||
window.backupPage = new BackupPage();
|
||||
await window.backupPage.init();
|
||||
},
|
||||
|
||||
async unmount(ctx) {
|
||||
// Release the page's document listeners + task-refresh registration so a
|
||||
// navigation away doesn't leave stale BackupPage listeners firing on the
|
||||
// live DOM — the backup sidebar "content stacks on revisit" bug. dispose()
|
||||
// aborts the click/input/change listeners and drops the coordinator reg.
|
||||
try { window.backupPage && window.backupPage.dispose(); } catch (_) {}
|
||||
try { ctx.services.tasks.refresh && ctx.services.tasks.refresh.unregister('backups'); } catch (_) {}
|
||||
window.backupPage = null;
|
||||
},
|
||||
});
|
||||
@ -23,9 +23,7 @@
|
||||
"/apps",
|
||||
"/apps*",
|
||||
"/app",
|
||||
"/app*",
|
||||
"/overview",
|
||||
"/overview*"
|
||||
"/app*"
|
||||
],
|
||||
"module": "/components/apps/index.js",
|
||||
"handler": "handleApps",
|
||||
@ -75,17 +73,6 @@
|
||||
"module": "/components/updater/index.js",
|
||||
"navId": "nav-updater",
|
||||
"order": 30
|
||||
},
|
||||
{
|
||||
"id": "backup",
|
||||
"routes": [
|
||||
"/backup",
|
||||
"/backup*"
|
||||
],
|
||||
"module": "/components/backup/index.js",
|
||||
"handler": "handleBackup",
|
||||
"navId": "nav-backup",
|
||||
"order": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -379,7 +379,10 @@ class UpdaterPage {
|
||||
|
||||
renderSecurity() {
|
||||
const withCves = this.apps.filter(a => (a.cves || []).length);
|
||||
if (!this.cves) return this.empty('No vulnerability scan yet — one runs automatically within a couple of minutes.', true);
|
||||
// No inline Check button: the host auto-scan runs the vulnerability scan on
|
||||
// its own within a couple of minutes (and the embedding header carries a
|
||||
// manual Check), so the message alone is the right button-free empty UI.
|
||||
if (!this.cves) return this.empty('No vulnerability scan yet — one runs automatically within a couple of minutes.');
|
||||
if (!withCves.length) return this.empty('No known vulnerabilities in your installed apps. 🎉');
|
||||
const blocks = withCves.map(a => {
|
||||
const items = (a.cves || []).map(c => `
|
||||
|
||||
@ -80,7 +80,7 @@ class BackupAppCard {
|
||||
<div class="backup-snapshot-rows">
|
||||
${allSnaps.slice(0, 50).map(s => this._renderRow(s, iconUrl)).join('')}
|
||||
</div>
|
||||
${allSnaps.length > 50 ? `<div class="backup-snapshot-overflow">Showing the most recent 50 of ${allSnaps.length} backups. Use the <a href="/backup">backup center</a> for the full list.</div>` : ''}
|
||||
${allSnaps.length > 50 ? `<div class="backup-snapshot-overflow">Showing the most recent 50 of ${allSnaps.length} backups. Use the <a href="/apps/overview/backups">backup center</a> for the full list.</div>` : ''}
|
||||
`;
|
||||
|
||||
// Deep-link: /app/<name>/backups?snapshot=<id> auto-expands that row
|
||||
|
||||
@ -69,10 +69,9 @@ class LibrePortalSPAClean {
|
||||
this.routes.set('/admin*', () => this.handleAdmin());
|
||||
this.routes.set('/tasks', () => this.handleTasks()); // Handle /tasks without query
|
||||
this.routes.set('/tasks*', () => this.handleTasks()); // Handle /tasks with query
|
||||
this.routes.set('/backup', () => this.handleBackup());
|
||||
this.routes.set('/backup*', () => this.handleBackup());
|
||||
// Legacy /config, /peers, /ssh are handled by _legacyRedirect() at the top
|
||||
// of navigate() (rewrites to the canonical /admin/* path).
|
||||
// Legacy /config, /peers, /ssh, /backup, /overview are handled by
|
||||
// _legacyRedirect() at the top of navigate() (rewritten to their canonical
|
||||
// /admin/* or /apps/overview* paths before routing).
|
||||
|
||||
}
|
||||
|
||||
@ -333,28 +332,6 @@ class LibrePortalSPAClean {
|
||||
}
|
||||
}
|
||||
|
||||
async handleBackup() {
|
||||
try {
|
||||
// backup-page.js + backup-app-card.js are loaded on first navigation.
|
||||
// loadScript is idempotent — subsequent /backup navigations are no-ops.
|
||||
await Promise.all([
|
||||
this.loadScript('/components/backup/core/js/backup-page.js'),
|
||||
this.loadScript('/core/backup-card/js/backup-app-card.js')
|
||||
]);
|
||||
const html = await this.fetchContent('/components/backup/core/html/backup-content.html');
|
||||
this.loadContent(html, 'Backups');
|
||||
if (typeof BackupPage !== 'undefined') {
|
||||
window.backupPage = new BackupPage();
|
||||
await window.backupPage.init();
|
||||
} else {
|
||||
console.error('BackupPage class not loaded');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Backup page load error:', error);
|
||||
this.showError('Failed to load backup page');
|
||||
}
|
||||
}
|
||||
|
||||
// Map a legacy short URL (/ssh, /peers, /config[?=cat]) to its canonical
|
||||
// /admin/* path, or return null if it isn't a legacy redirect. Used at the top
|
||||
// of navigate(). Replaces the old config-redirect/peers/ssh handlers.
|
||||
@ -362,8 +339,8 @@ class LibrePortalSPAClean {
|
||||
const full = path || '';
|
||||
const p = full.split('?')[0];
|
||||
if (p === '/ssh' || p.startsWith('/ssh/')) return '/admin/tools/ssh-access';
|
||||
// Peers now lives under Overview › Migrate › Peers (moved out of Admin).
|
||||
if (p === '/peers' || p.startsWith('/peers/') || p === '/admin/tools/peers' || p.startsWith('/admin/tools/peers/')) return '/overview/migrate/peers';
|
||||
// Peers now lives under App Center › Overview › Migrate › Peers (moved out of Admin).
|
||||
if (p === '/peers' || p.startsWith('/peers/') || p === '/admin/tools/peers' || p.startsWith('/admin/tools/peers/')) return '/apps/overview/migrate/peers';
|
||||
if (p === '/config' || p.startsWith('/config/') || full.startsWith('/config?')) {
|
||||
let cat = 'overview';
|
||||
if (full.includes('?=')) cat = full.split('?=')[1] || 'overview';
|
||||
@ -371,15 +348,27 @@ class LibrePortalSPAClean {
|
||||
else { const seg = p.replace(/^\/config\/?/, ''); if (seg) cat = seg; }
|
||||
return (typeof window.adminPath === 'function') ? window.adminPath(cat) : '/admin';
|
||||
}
|
||||
// The standalone Updater page is now the fleet Overview area. /updater[/tab]
|
||||
// -> /overview[/tab]; security/recovery/history folded into the Updates
|
||||
// expander, so they land on /overview/updates. (/backup is NOT redirected —
|
||||
// it remains the operational backup center, reached from Overview › Backups.)
|
||||
// The fleet Overview area lives under App Center: /apps/overview[/tab].
|
||||
// Rewrite the legacy short forms so old bookmarks/links keep working and the
|
||||
// address bar always shows the canonical path:
|
||||
// /overview[/tab] -> /apps/overview[/tab]
|
||||
// /backup[/sub] -> /apps/overview/backups[/sub] (the backup center is
|
||||
// now the Overview › Backups tab, not a standalone page)
|
||||
// /updater[/tab] -> /apps/overview[/tab]; security/recovery/history were
|
||||
// folded into the Updates expander, so they land on
|
||||
// /apps/overview/updates.
|
||||
if (p === '/overview' || p.startsWith('/overview/')) {
|
||||
return '/apps' + p;
|
||||
}
|
||||
if (p === '/backup' || p.startsWith('/backup/')) {
|
||||
const sub = p.replace(/^\/backup\/?/, '');
|
||||
return sub ? '/apps/overview/backups/' + sub : '/apps/overview/backups';
|
||||
}
|
||||
if (p === '/updater' || p.startsWith('/updater/')) {
|
||||
const seg = p.replace(/^\/updater\/?/, '');
|
||||
if (seg === 'updates' || seg === 'improvements') return '/overview/' + seg;
|
||||
if (!seg || seg === 'overview') return '/overview';
|
||||
return '/overview/updates';
|
||||
if (seg === 'updates' || seg === 'improvements') return '/apps/overview/' + seg;
|
||||
if (!seg || seg === 'overview') return '/apps/overview';
|
||||
return '/apps/overview/updates';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user