refactor(routing): move System out of /admin/config to /admin/system

System is live stats, not configuration, so it shouldn't live under
/admin/config. adminPath('system') now emits /admin/system; the path
parser locates 'system' positionally; all nav targets, breadcrumbs and the
dashboard disk-card link point at /admin/system{,/storage,/metric/<k>}.
adminCategoryFromPath already resolves /admin/<x> to that category, so
ConfigManager still mounts AdminSystem unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-28 23:37:28 +01:00
parent 18134a3ee1
commit bbbd035ab2
5 changed files with 26 additions and 22 deletions

View File

@ -3,10 +3,10 @@
// One AdminSystem instance per page mount. Reads the URL path on init and
// dispatches to one of four sub-views:
//
// /admin/config/system → index (gauges + trends + per-app table)
// /admin/config/system/metric/<key> → single-metric deep-dive page
// /admin/config/system/app/<name> → per-container app deep-dive
// /admin/config/system/storage → Docker disk breakdown
// /admin/system → index (gauges + trends + per-app table)
// /admin/system/metric/<key> → single-metric deep-dive page
// /admin/system/app/<name> → per-container app deep-dive
// /admin/system/storage → Docker disk breakdown
//
// Sub-views are separate page renderers (system-metric-page.js etc.) that
// each own their own DOM + lifecycle inside #config-section. We mount one
@ -32,16 +32,19 @@ class AdminSystem {
root() { return document.getElementById(this.rootId); }
// Path → view dispatch. AdminPath base is /admin/config/system; sub-paths
// Path → view dispatch. AdminPath base is /admin/system; sub-paths
// add segments after that. Falls through to 'index' for an unknown shape
// so a typo'd URL doesn't blank the page.
_parsePath() {
const segs = String(window.location.pathname || '').split('/').filter(Boolean);
// segs = ['admin','config','system', ...]
const sub = segs[3];
if (sub === 'metric' && segs[4]) return { view: 'metric', key: decodeURIComponent(segs[4]) };
if (sub === 'app' && segs[4]) return { view: 'app', name: decodeURIComponent(segs[4]) };
if (sub === 'storage') return { view: 'storage' };
// Locate 'system' and read the sub-view after it, so dispatch works for
// /admin/system/<sub>/<arg> regardless of leading segments.
const i = segs.indexOf('system');
const sub = i >= 0 ? segs[i + 1] : undefined;
const arg = i >= 0 ? segs[i + 2] : undefined;
if (sub === 'metric' && arg) return { view: 'metric', key: decodeURIComponent(arg) };
if (sub === 'app' && arg) return { view: 'app', name: decodeURIComponent(arg) };
if (sub === 'storage') return { view: 'storage' };
return { view: 'index' };
}
@ -136,7 +139,7 @@ class AdminSystem {
const ex = e.target.closest('[data-sys-expand]');
if (ex) {
const k = ex.dataset.sysExpand;
if (window.navigateToRoute) window.navigateToRoute(`/admin/config/system/metric/${encodeURIComponent(k)}`);
if (window.navigateToRoute) window.navigateToRoute(`/admin/system/metric/${encodeURIComponent(k)}`);
return;
}
const ap = e.target.closest('[data-sys-app]');
@ -151,7 +154,7 @@ class AdminSystem {
}
const st = e.target.closest('[data-sys-storage]');
if (st) {
if (window.navigateToRoute) window.navigateToRoute('/admin/config/system/storage');
if (window.navigateToRoute) window.navigateToRoute('/admin/system/storage');
return;
}
});

View File

@ -1,7 +1,7 @@
// Admin → System → Metric — single-metric deep-dive PAGE.
//
// Replaces the previous fullscreen overlay (system-detail.js) with a real
// in-flow page mounted at /admin/config/system/metric/<key>. Bookmarkable,
// in-flow page mounted at /admin/system/metric/<key>. Bookmarkable,
// browser-back navigates, refresh keeps you here.
//
// Renders into #config-section as a full-width admin page. Owns its own
@ -85,7 +85,7 @@ class SystemMetricPage {
_onKey(e) {
if (e.key === 'Escape' && window.navigateToRoute) {
window.navigateToRoute('/admin/config/system');
window.navigateToRoute('/admin/system');
}
}
_onResize() { this._renderChart(); }
@ -111,12 +111,12 @@ class SystemMetricPage {
}
const back = e.target.closest('[data-back]');
if (back && window.navigateToRoute) {
window.navigateToRoute('/admin/config/system');
window.navigateToRoute('/admin/system');
return;
}
const go = e.target.closest('[data-go-storage]');
if (go && window.navigateToRoute) {
window.navigateToRoute('/admin/config/system/storage');
window.navigateToRoute('/admin/system/storage');
}
}
@ -151,7 +151,7 @@ class SystemMetricPage {
<div class="page-header config-page-header">
<div class="page-header-title">
<div class="admin-breadcrumb">
<a href="/admin/config/system" data-back>Admin · System</a>
<a href="/admin/system" data-back>Admin · System</a>
</div>
<h1>Unknown metric</h1>
<p>No metric registered under "${(window.SystemFmt?.escape || String)(this.metricKey)}".</p>
@ -169,7 +169,7 @@ class SystemMetricPage {
<div class="page-header config-page-header">
<div class="page-header-title">
<div class="admin-breadcrumb">
<a href="/admin/config/system" data-back>Admin · System</a>
<a href="/admin/system" data-back>Admin · System</a>
</div>
<h1 class="sys-metric-name">${fmt.escape(this.metric.label)}</h1>
<p class="sys-metric-sub">${fmt.escape(this._subline())}</p>

View File

@ -1,6 +1,6 @@
// Admin → System → Storage — where the disk is going.
//
// Mounted at /admin/config/system/storage. Two data sources:
// Mounted at /admin/system/storage. Two data sources:
//
// - Storage by app — on-disk size of each app's bind-mounted data, from the
// generated /data/system/app_storage.json (du, not docker — see the
@ -59,7 +59,7 @@ class SystemStoragePage {
_onClick(e) {
const back = e.target.closest('[data-back]');
if (back && window.navigateToRoute) {
window.navigateToRoute('/admin/config/system');
window.navigateToRoute('/admin/system');
return;
}
const recl = e.target.closest('[data-storage-reclaim]');
@ -155,7 +155,7 @@ class SystemStoragePage {
<div class="page-header config-page-header">
<div class="page-header-title">
<div class="admin-breadcrumb">
<a href="/admin/config/system" data-back>Admin · System</a>
<a href="/admin/system" data-back>Admin · System</a>
</div>
<h1>Storage</h1>
<p class="sys-storage-sub">On-disk space by app, plus Docker's own engine usage.</p>

View File

@ -551,6 +551,7 @@ class LibrePortalSPAClean {
// Map a category to its path-based URL, and parse a path back to a category.
window.adminPath = function (category) {
if (!category || category === 'overview') return '/admin';
if (category === 'system') return '/admin/system'; // stats, not config
if (category === 'ssh-access') return '/admin/tools/ssh-access';
if (category === 'peers') return '/admin/tools/peers';
return '/admin/config/' + category;

View File

@ -453,7 +453,7 @@ async function loadSystemInfo() {
// addEventListener) so a dashboard re-mount can't stack duplicate handlers.
const diskCardEl = document.getElementById('disk-stat-card');
if (diskCardEl) {
const goStorage = () => window.navigateToRoute && window.navigateToRoute('/admin/config/system/storage');
const goStorage = () => window.navigateToRoute && window.navigateToRoute('/admin/system/storage');
diskCardEl.onclick = goStorage;
diskCardEl.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); goStorage(); } };
}