From fab6997cd78c47d6e8aa4d07d3dad63334fb762d Mon Sep 17 00:00:00 2001 From: librelad Date: Sat, 23 May 2026 18:36:06 +0100 Subject: [PATCH] refactor(webui): path-based Admin routing (/admin/config/, /admin/tools/ssh-access) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Admin area's ?= query URLs with clean, hierarchical paths that mirror the breadcrumb: /admin -> Overview /admin/config/-> Config / /admin/tools/ssh-access -> Tools / SSH Access New /admin (+ /admin*) SPA route -> handleAdmin, which parses the path via the shared window.adminPath / window.adminCategoryFromPath helpers and renders through the existing ConfigManager. Legacy /config, /config?= and /ssh now redirect into the matching /admin path, so old links/bookmarks keep working (server already serves index.html for any depth). Sidebar, Admin Overview, dashboard link and top-nav now build /admin paths; active-nav + config data loading recognise /admin across spa.js, topbar.js, router.js, data-loader.js. Scope: Admin area only — /app, /apps, /tasks, /backup keep their existing ?= URLs. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- .../frontend/html/dashboard-content.html | 2 +- .../libreportal/frontend/html/topbar.html | 2 +- .../js/components/admin/admin-overview.js | 2 +- .../js/components/config/config-sidebar.js | 7 +- .../frontend/js/components/topbar.js | 10 +-- containers/libreportal/frontend/js/spa.js | 78 +++++++++++-------- .../frontend/js/utils/data-loader.js | 4 +- .../libreportal/frontend/js/utils/router.js | 2 +- 8 files changed, 59 insertions(+), 48 deletions(-) diff --git a/containers/libreportal/frontend/html/dashboard-content.html b/containers/libreportal/frontend/html/dashboard-content.html index 5027fb3..fb1c568 100755 --- a/containers/libreportal/frontend/html/dashboard-content.html +++ b/containers/libreportal/frontend/html/dashboard-content.html @@ -46,7 +46,7 @@ - + Admin overview diff --git a/containers/libreportal/frontend/html/topbar.html b/containers/libreportal/frontend/html/topbar.html index e14d7a2..366abf0 100755 --- a/containers/libreportal/frontend/html/topbar.html +++ b/containers/libreportal/frontend/html/topbar.html @@ -31,7 +31,7 @@ App Center - + diff --git a/containers/libreportal/frontend/js/components/admin/admin-overview.js b/containers/libreportal/frontend/js/components/admin/admin-overview.js index edeb1fa..376f3c1 100644 --- a/containers/libreportal/frontend/js/components/admin/admin-overview.js +++ b/containers/libreportal/frontend/js/components/admin/admin-overview.js @@ -58,7 +58,7 @@ class AdminOverview { window.librePortalSPA?.navigate('/backup', true); } else if (where === 'ssh' || where === 'security') { const target = where === 'ssh' ? 'ssh-access' : 'security'; - window.history.pushState({}, '', `/config?=${target}`); + window.history.pushState({}, '', window.adminPath(target)); window.configCategory = target; window.configManager?.renderConfig?.(target); } diff --git a/containers/libreportal/frontend/js/components/config/config-sidebar.js b/containers/libreportal/frontend/js/components/config/config-sidebar.js index d221766..c975f96 100755 --- a/containers/libreportal/frontend/js/components/config/config-sidebar.js +++ b/containers/libreportal/frontend/js/components/config/config-sidebar.js @@ -26,7 +26,7 @@ class ConfigSidebar { overviewItem.setAttribute('data-category', 'overview'); overviewItem.innerHTML = ' Overview'; overviewItem.addEventListener('click', function () { - window.history.pushState({}, '', '/config?=overview'); + window.history.pushState({}, '', window.adminPath('overview')); document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); }); this.classList.add('active'); window.configCategory = 'overview'; @@ -76,8 +76,7 @@ class ConfigSidebar { categoryItem.addEventListener('click', function() { // Update URL without full page reload - const url = '/config?=' + category.id; - window.history.pushState({}, '', url); + window.history.pushState({}, '', window.adminPath(category.id)); // Update active state document.querySelectorAll('.category').forEach(function(item) { @@ -109,7 +108,7 @@ class ConfigSidebar { sshItem.setAttribute('data-category', 'ssh-access'); sshItem.innerHTML = 'SSH Access SSH Access'; sshItem.addEventListener('click', function () { - window.history.pushState({}, '', '/config?=ssh-access'); + window.history.pushState({}, '', window.adminPath('ssh-access')); document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); }); this.classList.add('active'); window.configCategory = 'ssh-access'; diff --git a/containers/libreportal/frontend/js/components/topbar.js b/containers/libreportal/frontend/js/components/topbar.js index 082c843..329b9c8 100755 --- a/containers/libreportal/frontend/js/components/topbar.js +++ b/containers/libreportal/frontend/js/components/topbar.js @@ -17,7 +17,7 @@ class TopbarComponent { if (path.startsWith('/apps') || path === '/apps') { return 'apps'; } - if (path.startsWith('/config') || path === '/config') { + if (path.startsWith('/admin') || path.startsWith('/config') || path.startsWith('/ssh')) { return 'config'; } if (path.startsWith('/tasks') || path === '/tasks') { @@ -266,14 +266,12 @@ class TopbarComponent { // PRIMARY: Use path-based detection only (most reliable) if (path.startsWith('/app') || path.startsWith('/apps')) { activeNavId = 'nav-app-center'; - } else if (path.startsWith('/config')) { - activeNavId = 'nav-config'; + } else if (path.startsWith('/admin') || path.startsWith('/config') || path.startsWith('/ssh')) { + activeNavId = 'nav-config'; // Admin area (config + SSH live here) } else if (path.startsWith('/tasks')) { activeNavId = 'nav-tasks'; } else if (path.startsWith('/backup')) { activeNavId = 'nav-backup'; - } else if (path.startsWith('/ssh')) { - activeNavId = 'nav-config'; // SSH Access lives under the Admin (config) area } else if (path === '/' || path === '/dashboard') { activeNavId = 'nav-dashboard'; } else { @@ -349,7 +347,7 @@ class TopbarComponent { if (path.startsWith('/app') || path.startsWith('/apps')) { activeNavId = 'nav-app-center'; - } else if (path.startsWith('/config')) { + } else if (path.startsWith('/admin') || path.startsWith('/config') || path.startsWith('/ssh')) { activeNavId = 'nav-config'; } else if (path.startsWith('/tasks')) { activeNavId = 'nav-tasks'; diff --git a/containers/libreportal/frontend/js/spa.js b/containers/libreportal/frontend/js/spa.js index 1ec72df..b9dc866 100755 --- a/containers/libreportal/frontend/js/spa.js +++ b/containers/libreportal/frontend/js/spa.js @@ -68,13 +68,15 @@ class LibrePortalSPAClean { this.routes.set('/apps', () => this.handleApps()); this.routes.set('/app', () => this.handleAppDetail()); // Handle /app without query this.routes.set('/app*', () => this.handleAppDetail()); // Handle /app with query - this.routes.set('/config', () => this.handleConfig()); // Handle /config without query - this.routes.set('/config*', () => this.handleConfig()); // Handle /config with query + this.routes.set('/admin', () => this.handleAdmin()); // Admin area (path-based: /admin/config/, /admin/tools/) + this.routes.set('/admin*', () => this.handleAdmin()); + this.routes.set('/config', () => this.handleConfigRedirect()); // legacy → /admin + this.routes.set('/config*', () => this.handleConfigRedirect()); this.routes.set('/tasks', () => this.handleTasks()); // Handle /tasks without query this.routes.set('/tasks*', () => this.handleTasks()); // Handle /tasks with query this.routes.set('/backup', () => this.handleBackup()); this.routes.set('/backup*', () => this.handleBackup()); - this.routes.set('/ssh', () => this.handleSsh()); + this.routes.set('/ssh', () => this.handleSsh()); // legacy → /admin/tools/ssh-access this.routes.set('/ssh*', () => this.handleSsh()); //console.log('📍 Routes registered:', Array.from(this.routes.keys())); @@ -270,9 +272,8 @@ class LibrePortalSPAClean { } async handleSsh() { - // SSH Access now lives inside the Admin (config) area as a sidebar item. - // Redirect old /ssh links to it. - this.navigate('/config?=ssh-access', true); + // Legacy /ssh → SSH Access under the Admin area. + this.navigate('/admin/tools/ssh-access', true); } async handleApps() { @@ -363,43 +364,41 @@ class LibrePortalSPAClean { } } - async handleConfig() { - //console.log('⚙️ Loading config...'); - - // Handle query parameters for config - const path = window.location.pathname + window.location.search; - if (path.includes('?=')) { - const [basePath, query] = path.split('?='); - window.configCategory = query || 'overview'; - } else if (path.includes('?')) { - const url = new URL(path, window.location.origin); - const searchParams = url.searchParams; - window.configCategory = searchParams.get('config') || 'overview'; - } else { - window.configCategory = 'overview'; - } - + // Admin area. Path-based: /admin (overview), /admin/config/, + // /admin/tools/ssh-access. Reuses the config page shell + ConfigManager. + async handleAdmin() { + window.configCategory = window.adminCategoryFromPath(window.location.pathname); + try { const html = await this.fetchContent('/html/config-content.html'); - this.loadContent(html, 'Configuration'); - - // Config manager should already be initialized by SystemLoader + this.loadContent(html, 'Admin'); + if (window.configManager) { - // Render the actual configuration if (typeof window.configManager.renderConfig === 'function') { await window.configManager.renderConfig(window.configCategory || 'overview'); } - //console.log('✅ Config loaded'); } else { console.error('ConfigManager not available - SystemLoader should have initialized it'); throw new Error('ConfigManager not initialized by SystemLoader'); } } catch (error) { - console.error('❌ Config load error:', error); - this.showError('Failed to load configuration'); + console.error('❌ Admin load error:', error); + this.showError('Failed to load the Admin area'); } } + // Legacy /config and /config?= → the path-based /admin equivalent. + async handleConfigRedirect() { + const search = window.location.search || ''; + let cat = 'overview'; + if (search.includes('?=')) { + cat = (window.location.pathname + search).split('?=')[1] || 'overview'; + } else { + cat = new URLSearchParams(search).get('config') || 'overview'; + } + this.navigate(window.adminPath(cat), true); + } + async handleTasks() { //console.log('📋 Loading tasks...'); @@ -476,7 +475,7 @@ class LibrePortalSPAClean { if (path.startsWith('/app') || path.startsWith('/apps')) { activeId = 'nav-app-center'; - } else if (path.startsWith('/config')) { + } else if (path.startsWith('/admin') || path.startsWith('/config') || path.startsWith('/ssh')) { activeId = 'nav-config'; } else if (path.startsWith('/tasks')) { activeId = 'nav-tasks'; @@ -507,6 +506,21 @@ class LibrePortalSPAClean { } } +// Admin area path helpers (shared by the SPA, sidebar, overview, ssh page). +// 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 === 'ssh-access') return '/admin/tools/ssh-access'; + return '/admin/config/' + category; +}; +window.adminCategoryFromPath = function (pathname) { + const segs = String(pathname || '').replace(/^\/admin\/?/, '').split('/').filter(Boolean); + if (!segs.length || segs[0] === 'overview') return 'overview'; + if (segs[0] === 'config') return segs[1] || 'general'; + if (segs[0] === 'tools') return segs[1] || 'overview'; + return segs[0]; +}; + // Global navigation function for click handlers window.navigateToRoute = function(href) { if (window.spaClean) { @@ -522,8 +536,8 @@ window.navigateToRoute = function(href) { route = '/dashboard'; } else if (route === 'apps') { route = '/apps'; - } else if (route === 'config') { - route = '/config?=general'; + } else if (route === 'config' || route === 'admin') { + route = '/admin'; } else if (route === 'tasks') { route = '/tasks'; } else if (!route.startsWith('/')) { diff --git a/containers/libreportal/frontend/js/utils/data-loader.js b/containers/libreportal/frontend/js/utils/data-loader.js index b517708..ffe1d28 100755 --- a/containers/libreportal/frontend/js/utils/data-loader.js +++ b/containers/libreportal/frontend/js/utils/data-loader.js @@ -150,8 +150,8 @@ async function initializeData() { } else if (currentPath === '/apps' || currentPath === '/app' || searchParams.has('apps') || searchParams.has('app')) { // Apps page: All apps + categories (no system configs needed) await loadAppsPageData(); - } else if (currentPath.startsWith('/config') || searchParams.has('config')) { - // Config pages: System configs + apps + categories + } else if (currentPath.startsWith('/admin') || currentPath.startsWith('/config') || currentPath.startsWith('/ssh') || searchParams.has('config')) { + // Admin area (config + SSH): system configs + apps + categories await loadConfigDetailData(); } else { // Default: Load all data for SPA diff --git a/containers/libreportal/frontend/js/utils/router.js b/containers/libreportal/frontend/js/utils/router.js index 3901d7c..64f5e86 100755 --- a/containers/libreportal/frontend/js/utils/router.js +++ b/containers/libreportal/frontend/js/utils/router.js @@ -264,7 +264,7 @@ class Router { // PRIMARY: Use path-based detection only (most reliable) if (pathname.startsWith('/app') || pathname.startsWith('/apps')) { activeNavId = 'nav-app-center'; - } else if (pathname.startsWith('/config')) { + } else if (pathname.startsWith('/admin') || pathname.startsWith('/config') || pathname.startsWith('/ssh')) { activeNavId = 'nav-config'; } else if (pathname.startsWith('/tasks')) { activeNavId = 'nav-tasks';