feat(webui): phase 0a — feature-module kernel scaffold (passive)
Adds the foundation of the feature-module architecture (docs/frontend-modularization.md) as inert, additive code: - kernel/feature-registry.js: window.LP.features — runtime register(), manifest loader, route-table + nav builders. - features/manifest.dev.json: hand-committed manifest mirroring spa.js setupRoutes() exactly (route -> handler + navId). - index.html loads the kernel before spa.js. Zero behaviour change: nothing consults the registry yet. Phase 0b flips routing to be registry-driven with the spa.js Map as fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
8312f2222f
commit
2eaa5857a1
65
containers/libreportal/frontend/features/manifest.dev.json
Normal file
65
containers/libreportal/frontend/features/manifest.dev.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"version": 1,
|
||||
"note": "Phase-0 hand-committed manifest. Mirrors spa.js setupRoutes() exactly. Will be replaced by the generated /data/webui/generated/features.json once the scan generator lands (see docs/frontend-modularization.md). 'handler' names the LibrePortalSPAClean method that still does the rendering during the strangler migration; 'navId' is the existing topbar element id for active-state highlighting.",
|
||||
"features": [
|
||||
{
|
||||
"id": "dashboard",
|
||||
"routes": ["/", "/dashboard"],
|
||||
"handler": "handleDashboard",
|
||||
"navId": "nav-dashboard",
|
||||
"nav": { "label": "Dashboard", "order": 10 }
|
||||
},
|
||||
{
|
||||
"id": "apps",
|
||||
"routes": ["/apps", "/apps*"],
|
||||
"handler": "handleApps",
|
||||
"navId": "nav-app-center",
|
||||
"nav": { "label": "App Center", "order": 20 }
|
||||
},
|
||||
{
|
||||
"id": "app-detail",
|
||||
"routes": ["/app", "/app*"],
|
||||
"handler": "handleAppDetail",
|
||||
"navId": "nav-app-center"
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"routes": ["/admin", "/admin*"],
|
||||
"handler": "handleAdmin",
|
||||
"navId": "nav-config",
|
||||
"nav": { "label": "Admin", "order": 40 }
|
||||
},
|
||||
{
|
||||
"id": "config-redirect",
|
||||
"routes": ["/config", "/config*"],
|
||||
"handler": "handleConfigRedirect",
|
||||
"navId": "nav-config"
|
||||
},
|
||||
{
|
||||
"id": "tasks",
|
||||
"routes": ["/tasks", "/tasks*"],
|
||||
"handler": "handleTasks",
|
||||
"navId": "nav-tasks",
|
||||
"nav": { "label": "Tasks", "order": 60 }
|
||||
},
|
||||
{
|
||||
"id": "backup",
|
||||
"routes": ["/backup", "/backup*"],
|
||||
"handler": "handleBackup",
|
||||
"navId": "nav-backup",
|
||||
"nav": { "label": "Backups", "order": 50 }
|
||||
},
|
||||
{
|
||||
"id": "peers",
|
||||
"routes": ["/peers", "/peers*"],
|
||||
"handler": "handlePeers",
|
||||
"navId": "nav-config"
|
||||
},
|
||||
{
|
||||
"id": "ssh",
|
||||
"routes": ["/ssh", "/ssh*"],
|
||||
"handler": "handleSsh",
|
||||
"navId": "nav-config"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -102,6 +102,10 @@
|
||||
<script src="/js/system/setup-wizard.js"></script>
|
||||
<script src="/js/system/setup-completion-watcher.js"></script>
|
||||
<script src="/js/system/system-orchestrator.js"></script>
|
||||
<!-- Feature-module kernel. Currently passive: defines window.LP.features and
|
||||
loads the page manifest; spa.js consults it for routing. See
|
||||
docs/frontend-modularization.md. -->
|
||||
<script src="/kernel/feature-registry.js"></script>
|
||||
<!--
|
||||
Page-specific controllers are loaded on demand by spa.js / config-manager.js
|
||||
when the user navigates to the relevant route. Keeping them out of the
|
||||
|
||||
93
containers/libreportal/frontend/kernel/feature-registry.js
Normal file
93
containers/libreportal/frontend/kernel/feature-registry.js
Normal file
@ -0,0 +1,93 @@
|
||||
// kernel/feature-registry.js — the single source of truth for "what pages exist".
|
||||
//
|
||||
// This is the first piece of the feature-module architecture (see
|
||||
// docs/frontend-modularization.md). It replaces the four hand-maintained
|
||||
// registries the SPA grew over time:
|
||||
// 1. the eager <script> list in index.html,
|
||||
// 2. spa.js's hardcoded `routes` Map (setupRoutes),
|
||||
// 3. system-loader.js's component registry,
|
||||
// 4. config-manager.js's renderConfig() category if-chain.
|
||||
//
|
||||
// In this first phase it is deliberately *passive*: it loads a checked-in
|
||||
// manifest describing today's pages and exposes a routes Map. spa.js consults
|
||||
// it for routing; rendering still happens in the existing handleX() bodies.
|
||||
// Later phases replace handlers with self-registering feature modules that call
|
||||
// LP.features.register({...}) — the runtime registry below already supports it.
|
||||
(function () {
|
||||
const LP = (window.LP = window.LP || {});
|
||||
|
||||
LP.features = {
|
||||
// Runtime registrations (feature index.js files call register()). Unused in
|
||||
// phase 0 — present so later phases can land without touching this file.
|
||||
_modules: new Map(),
|
||||
|
||||
// The parsed manifest (the generated/checked-in description of all pages).
|
||||
_manifest: null,
|
||||
|
||||
// Register a live feature module. id must be unique; last-wins with a warn
|
||||
// so a duplicate during migration is loud, not silent.
|
||||
register(def) {
|
||||
if (!def || !def.id) {
|
||||
console.error('[features] register() called without an id', def);
|
||||
return;
|
||||
}
|
||||
if (this._modules.has(def.id)) {
|
||||
console.warn(`[features] duplicate registration for "${def.id}" — last wins`);
|
||||
}
|
||||
this._modules.set(def.id, def);
|
||||
return def;
|
||||
},
|
||||
|
||||
get(id) {
|
||||
return this._modules.get(id) || null;
|
||||
},
|
||||
|
||||
// Fetch + cache the manifest. Read-only GET of a static/generated file,
|
||||
// same pattern as DataLoader.loadApps(). Returns the manifest or null on
|
||||
// failure (callers fall back to their built-in defaults).
|
||||
async loadManifest(url = '/features/manifest.dev.json') {
|
||||
if (this._manifest) return this._manifest;
|
||||
try {
|
||||
const res = await fetch(url, { cache: 'no-cache' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
this._manifest = Array.isArray(data.features) ? data : { version: 0, features: [] };
|
||||
return this._manifest;
|
||||
} catch (err) {
|
||||
console.warn('[features] manifest load failed, callers will fall back:', err.message);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// The manifest entries, or [] if not loaded.
|
||||
list() {
|
||||
return (this._manifest && this._manifest.features) || [];
|
||||
},
|
||||
|
||||
// Build a route-pattern -> entry Map from the manifest. The pattern strings
|
||||
// (including the trailing-'*' wildcards) are kept verbatim so the consumer's
|
||||
// matching logic is unchanged from spa.js's findRouteHandler.
|
||||
buildRouteTable() {
|
||||
const table = new Map();
|
||||
for (const f of this.list()) {
|
||||
for (const route of (f.routes || [])) {
|
||||
if (table.has(route)) {
|
||||
console.warn(`[features] duplicate route "${route}" (feature "${f.id}") — last wins`);
|
||||
}
|
||||
table.set(route, f);
|
||||
}
|
||||
}
|
||||
return table;
|
||||
},
|
||||
|
||||
// Nav entries (topbar) derived from the manifest, sorted by nav.order.
|
||||
// Phase 0 keeps topbar.html authoritative; this is here for the nav kernel
|
||||
// that lands with the topbar migration.
|
||||
navItems() {
|
||||
return this.list()
|
||||
.filter(f => f.nav && f.nav.label)
|
||||
.sort((a, b) => (a.nav.order || 0) - (b.nav.order || 0))
|
||||
.map(f => ({ id: f.id, ...f.nav, routes: f.routes }));
|
||||
},
|
||||
};
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user