diff --git a/containers/libreportal/frontend/features/app-detail/index.js b/containers/libreportal/frontend/features/app-detail/index.js new file mode 100644 index 0000000..d32f9a4 --- /dev/null +++ b/containers/libreportal/frontend/features/app-detail/index.js @@ -0,0 +1,58 @@ +// features/app-detail/index.js — the per-app detail page (/app/[/]). +// +// Shares the apps-unified-layout fragment with the App Center grid but renders +// via window.appTabbedManager (a system-loader singleton; call .initialize(), +// never `new`). Registered AFTER the apps feature so '/apps*' wins the wildcard +// match and only true /app* paths land here. mount() mirrors handleAppDetail +// exactly, including the legacy ?app= / ?=name parsing and the ?tab=/?config= +// → path rewrite. unmount() is a no-op for now (the singleton is shared); the +// known unguarded popstate/task-listener re-bind in app-tabbed-manager is +// addressed in a follow-up (it predates this migration). +LP.features.register({ + id: 'app-detail', + routes: ['/app', '/app*'], + + async mount(ctx) { + const url = new URL(window.location); + let appName = url.pathname.replace(/^\/app\/?/, '').split('/')[0]; + appName = appName ? decodeURIComponent(appName) : ''; + if (!appName) appName = url.searchParams.get('app'); + + // Old format ?=appname&tab=tabname + if (!appName && url.search.includes('?=')) { + const queryPart = url.search.replace('?', ''); + for (const part of queryPart.split('&')) { + if (part.startsWith('=')) { appName = part.substring(1); break; } + } + } + + if (!appName) { return ctx.nav('/apps', false); } + + // Back-compat: rewrite legacy ?tab=/?config= to the canonical path shape + // before the page reads URL state (replaceState — no extra history entry). + const legacyTab = url.searchParams.get('tab'); + const legacyConfig = url.searchParams.get('config'); + if (legacyTab || legacyConfig) { + const tab = legacyTab === 'logs' ? 'tasks' : (legacyTab || 'config'); + const sub = (tab === 'config') ? legacyConfig : null; + const taskId = url.searchParams.get('task'); + const canonical = window.appPath(appName, tab, sub, taskId); + if (canonical !== url.pathname + url.search) { + window.history.replaceState({ route: canonical }, '', canonical); + } + } + + const html = await ctx.loadFragment('/html/apps-unified-layout.html'); + ctx.setContent(html, appName); + + if (!window.appTabbedManager) { + throw new Error('AppTabbedManager not initialized by SystemLoader'); + } + await window.appTabbedManager.initialize(); + }, + + async unmount() { + // No-op: AppTabbedManager is a shared system-loader singleton. Its + // listener-rebind leak is pre-existing and fixed separately. + }, +}); diff --git a/containers/libreportal/frontend/features/apps/index.js b/containers/libreportal/frontend/features/apps/index.js new file mode 100644 index 0000000..2f19e3f --- /dev/null +++ b/containers/libreportal/frontend/features/apps/index.js @@ -0,0 +1,46 @@ +// features/apps/index.js — the App Center grid (/apps, /apps/). +// +// window.appsManager is a system-loader singleton (created by +// initializeComponents); mount() calls .initialize(), never `new`. Routes +// '/apps' and '/apps*' are registered BEFORE the app-detail feature's '/app*' +// so findRouteHandler's longest-prefix wildcard match resolves /apps* here +// (the manifest order guarantees this). unmount() is a no-op: the singleton and +// its constructor-time taskRefresh registrations are shared with app-detail and +// the dashboard and must outlive this view. +LP.features.register({ + id: 'apps', + routes: ['/apps', '/apps*'], + + async mount(ctx) { + // Category from the path (/apps/), else legacy ?= / ?apps=. + const seg = window.location.pathname.replace(/^\/apps\/?/, '').split('/')[0]; + if (seg) { + window.appsCategory = decodeURIComponent(seg); + } else { + const search = window.location.search || ''; + if (search.includes('?=')) { + window.appsCategory = (window.location.pathname + search).split('?=')[1] || 'all'; + } else { + window.appsCategory = new URLSearchParams(search).get('apps') || 'all'; + } + } + + // Load the unified layout only if it isn't already present — preserves grid + // state when moving between categories / back from app-detail (legacy behaviour). + if (!document.querySelector('.apps-layout')) { + const html = await ctx.loadFragment('/html/apps-unified-layout.html'); + ctx.setContent(html, 'Applications'); + } + + if (!window.appsManager) { + throw new Error('AppsManager not initialized by SystemLoader'); + } + await window.appsManager.initialize(); + }, + + async unmount() { + // Nothing to release — AppsManager is a shared system-loader singleton. + // The dirty-config nav guard (window.__appConfigNavGuard) fires in + // navigate() BEFORE unmount and is owned by AppsManager, so we leave it. + }, +}); diff --git a/containers/libreportal/frontend/index.html b/containers/libreportal/frontend/index.html index 66021e9..b172361 100755 --- a/containers/libreportal/frontend/index.html +++ b/containers/libreportal/frontend/index.html @@ -112,6 +112,8 @@ + +