From 7e051be196bca92b091293aee7781b9eba49d432 Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 29 May 2026 22:32:42 +0100 Subject: [PATCH] =?UTF-8?q?feat(webui):=20phase=200b=20=E2=80=94=20route?= =?UTF-8?q?=20from=20the=20feature=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LibrePortalSPAClean now builds its route table from window.LP.features (features/manifest.dev.json) instead of the hardcoded setupRoutes() Map. Manifest order is preserved so findRouteHandler()'s wildcard precedence (/apps* before /app*) is unchanged. All-or-nothing fallback to the built-in table if the manifest is missing/empty or names an unknown handler, so routing is never left half-wired. Rendering is unchanged — handlers still do the work; only the routing source moved. Co-Authored-By: Claude Opus 4.8 Signed-off-by: librelad --- containers/libreportal/frontend/js/spa.js | 47 +++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/containers/libreportal/frontend/js/spa.js b/containers/libreportal/frontend/js/spa.js index 97f81c1..2fd4d0f 100755 --- a/containers/libreportal/frontend/js/spa.js +++ b/containers/libreportal/frontend/js/spa.js @@ -13,9 +13,9 @@ class LibrePortalSPAClean { async init() { //console.log('🚀 Clean SPA: Initializing...'); - // Setup routes immediately - this.setupRoutes(); - + // Setup routes from the feature manifest (falls back to the built-in table) + await this.setupRoutesFromManifest(); + // Wait for DOM to be ready if (document.readyState === 'loading') { await new Promise(resolve => { @@ -85,6 +85,47 @@ class LibrePortalSPAClean { //console.log('📍 Routes registered:', Array.from(this.routes.keys())); } + // Build the route table from the feature manifest (window.LP.features) so + // "what pages exist" lives in one declarative place (features/manifest.dev.json) + // instead of being hardcoded here. Route insertion order is preserved from the + // manifest, which keeps the wildcard precedence findRouteHandler() relies on + // (e.g. '/apps*' must be inserted before '/app*'). Falls back to the built-in + // setupRoutes() table if the manifest is missing, empty, or names a handler + // this class doesn't define — routing must never be left half-wired. + async setupRoutesFromManifest() { + try { + const manifest = (window.LP && window.LP.features) + ? await window.LP.features.loadManifest() + : null; + const entries = manifest && manifest.features; + if (!entries || !entries.length) { + this.setupRoutes(); + return; + } + // All-or-nothing: a single missing handler means we don't trust the + // manifest enough to route from it — use the known-good built-in table. + for (const f of entries) { + if (typeof this[f.handler] !== 'function') { + console.warn(`[spa] manifest handler "${f.handler}" (feature "${f.id}") not found — using built-in routes`); + this.setupRoutes(); + return; + } + } + this.routes.clear(); + for (const f of entries) { + const handler = () => this[f.handler](); + for (const route of (f.routes || [])) { + this.routes.set(route, handler); + } + } + this._routesFromManifest = true; + //console.log('📍 Routes registered from manifest:', Array.from(this.routes.keys())); + } catch (err) { + console.error('[spa] manifest routing failed, using built-in routes:', err); + this.setupRoutes(); + } + } + async loadCoreData() { //console.log('📊 Loading core data...');