Themes are already modular via folder discovery (GET /api/themes/list scans themes/<name>/). This brings the SAME model to pages: - backend/routes/features.js: public GET /api/features/list scans frontend/features/<id>/feature.json and returns the page manifest. The Node process reads its own bind-mounted /app/frontend — no runFileOp / regen / source-array plumbing needed (sidesteps the shell-generator gotchas). - features/<id>/feature.json: each page now self-describes (id, routes, module, handler, navId, nav, order). 6 real features + 3 redirect-only (config/peers/ssh) so behaviour is preserved exactly. - kernel loadManifest() prefers /api/features/list, falls back to the static features/manifest.dev.json when the endpoint isn't up yet. Result: dropping a features/<id>/ folder registers a page; deleting it removes it — zero central edits, exactly like dropping a theme folder. (Backend route needs a Node restart to activate; the static-manifest fallback keeps everything working until then.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
103 lines
4.0 KiB
JavaScript
103 lines
4.0 KiB
JavaScript
// 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 page manifest. Prefers the live scan endpoint
|
|
// (/api/features/list — folder-discovered, exactly like /api/themes/list),
|
|
// and falls back to the checked-in static manifest if the endpoint is
|
|
// unavailable (e.g. the backend hasn't restarted to pick up the route yet).
|
|
// Returns the manifest or null on total failure (callers fall back to their
|
|
// built-in defaults).
|
|
async loadManifest() {
|
|
if (this._manifest) return this._manifest;
|
|
const sources = ['/api/features/list', '/features/manifest.dev.json'];
|
|
for (const url of sources) {
|
|
try {
|
|
const res = await fetch(url, { cache: 'no-cache' });
|
|
if (!res.ok) continue;
|
|
const data = await res.json();
|
|
if (data && Array.isArray(data.features) && data.features.length) {
|
|
this._manifest = data;
|
|
return this._manifest;
|
|
}
|
|
} catch (err) {
|
|
console.warn(`[features] manifest source ${url} failed:`, err.message);
|
|
}
|
|
}
|
|
console.warn('[features] no manifest source available, callers will fall back');
|
|
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 }));
|
|
},
|
|
};
|
|
})();
|