From a103aa6864763123e5b7b973c2a5d4de8ccbc53a Mon Sep 17 00:00:00 2001 From: librelad Date: Sat, 23 May 2026 19:03:54 +0100 Subject: [PATCH] refactor(webui): path-based URLs for apps, app, tasks, backup Convert the remaining sections off the legacy ?= query form to clean paths, matching the Admin area: /apps/ (was /apps?=) /app/?tab=&task= (was /app?=&tab=&task=) /tasks/?task= (was /tasks?=&task=) /backup/ (was /backup?=) Builders updated everywhere (sidebar, dashboard, notifications, tasks, apps, app tabs, task-actions, setup watcher); parsers now read the resource from the path with the legacy ?= kept as a fallback so old links/bookmarks still work (server already serves index.html at any depth). Route table gains /apps* and orders it before /app* (since '/apps' startsWith '/app'); active-nav and config/apps data-loading recognise the new paths. Tab/task remain ordinary query params (modifiers, not the primary resource). Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- .../js/components/app/app-tabbed-manager.js | 112 +++++------------- .../js/components/app/apps-manager.js | 28 ++--- .../js/components/backup/backup-page.js | 6 +- .../js/components/config/config-form.js | 4 +- .../frontend/js/components/dashboard.js | 8 +- .../frontend/js/components/notifications.js | 6 +- .../js/components/task/task-actions.js | 12 +- .../js/components/tasks/tasks-manager.js | 23 ++-- .../frontend/js/components/topbar.js | 2 +- containers/libreportal/frontend/js/spa.js | 34 +++--- .../js/system/setup-completion-watcher.js | 4 +- .../frontend/js/utils/data-loader.js | 2 +- 12 files changed, 98 insertions(+), 143 deletions(-) diff --git a/containers/libreportal/frontend/js/components/app/app-tabbed-manager.js b/containers/libreportal/frontend/js/components/app/app-tabbed-manager.js index 6d7d1d1..a630c98 100755 --- a/containers/libreportal/frontend/js/components/app/app-tabbed-manager.js +++ b/containers/libreportal/frontend/js/components/app/app-tabbed-manager.js @@ -97,15 +97,15 @@ class AppTabbedManager { // Get app name from URL parameter getAppFromURL() { - const urlParams = new URLSearchParams(window.location.search); - let appName = urlParams.get('app'); - - // Fallback to old format if app param not found + // Path-based /app/ first, then legacy ?app= / ?=name. + let appName = window.location.pathname.replace(/^\/app\/?/, '').split('/')[0]; + appName = appName ? decodeURIComponent(appName) : ''; + if (!appName) { - const fullPath = window.location.search; - if (fullPath.includes('?=')) { - const [basePath, query] = fullPath.split('?='); - appName = query.split('&')[0]; // Get only the app name, ignore other params + const urlParams = new URLSearchParams(window.location.search); + appName = urlParams.get('app'); + if (!appName && window.location.search.includes('?=')) { + appName = window.location.search.split('?=')[1].split('&')[0]; } } @@ -163,88 +163,34 @@ class AppTabbedManager { search.includes('?=')); // Old format app pages } - // Update URL with app and tab + // Update URL with app and tab — path-based: /app/?tab=&task=. updateURL(app = null, tab = null) { - // console.log('🔍 updateURL called with:', { app, tab }); - // console.log('🔍 Current URL before update:', window.location.href); - - // Only update URLs on app pages - prevent interference with other pages - if (!this.isAppPage()) { - // console.log('🚫 Not on app page, skipping URL update'); - return; + // Only update URLs on app pages - prevent interference with other pages. + if (!this.isAppPage()) return; + + const params = new URLSearchParams(window.location.search); + const fromPath = window.location.pathname.replace(/^\/app\/?/, '').split('/')[0]; + const currentApp = app || (fromPath ? decodeURIComponent(fromPath) : '') || this.currentApp; + if (!currentApp) return; + + const q = new URLSearchParams(); + const finalTab = tab || params.get('tab'); + if (finalTab) q.set('tab', finalTab); + // Keep a deep-linked task only when staying on the same app (tab-only update). + if (!app) { + const task = params.get('task'); + if (task) q.set('task', task); } - const url = new URL(window.location); - const params = new URLSearchParams(url.search); - const fullPath = window.location.search; // Define here for both blocks - - // Handle both old format (?=appname) and new format (?app=appname) - if (app) { - // Check if we're using the old format - if (fullPath.includes('?=')) { - // Update old format: /app?=appname&tab=tabname - const newURL = `/app?=${app}`; - if (tab) { - // console.log('🔄 Updating URL to:', `${newURL}&tab=${tab}`); - window.history.replaceState({}, '', `${newURL}&tab=${tab}`); - } else { - // console.log('🔄 Updating URL to:', newURL); - window.history.replaceState({}, '', newURL); - } - } else { - // Update new format: /app?app=appname&tab=tabname - if (tab) { - params.set('app', app); - params.set('tab', tab); - } else { - params.set('app', app); - } - const newSearch = params.toString(); - // console.log('🔄 Updating URL to:', `${window.location.pathname}?${newSearch}`); - window.history.replaceState({}, '', `${window.location.pathname}?${newSearch}`); - } - } else { - // Only updating tab, preserve existing app and task parameters - if (fullPath.includes('?=')) { - // Old format: preserve app and task, update tab - const currentApp = params.get('=') || this.currentApp; - const currentTask = params.get('task'); - let newURL = `/app?=${currentApp}&tab=${tab}`; - if (currentTask) { - newURL += `&task=${currentTask}`; - } - // console.log('🔄 Updating URL (old format) to:', newURL); - window.history.replaceState({}, '', newURL); - } else { - // New format: preserve app and task, update tab - const currentApp = params.get('app') || this.currentApp; - const currentTask = params.get('task'); - params.set('app', currentApp); - params.set('tab', tab); - if (currentTask) { - params.set('task', currentTask); - } - const newSearch = params.toString(); - // console.log('🔄 Updating URL (new format) to:', `${window.location.pathname}?${newSearch}`); - window.history.replaceState({}, '', `${window.location.pathname}?${newSearch}`); - } - } + const qs = q.toString(); + window.history.replaceState({}, '', `/app/${encodeURIComponent(currentApp)}${qs ? '?' + qs : ''}`); } // Update current app and refresh content updateApp(newAppName) { this.setCurrentApp(newAppName); - - // Reset URL to config tab - const currentUrl = window.location.href; - let newUrl; - if (currentUrl.includes('tab=')) { - newUrl = currentUrl.replace(/tab=[^&]*/, 'tab=config'); - } else { - newUrl = `${currentUrl}&tab=config`; - } - history.replaceState({}, '', newUrl); - + // Reset to the config tab on the path-based app URL. + history.replaceState({}, '', `/app/${encodeURIComponent(newAppName)}?tab=config`); this.switchTab('config'); } @@ -593,7 +539,7 @@ class AppTabbedManager { const currentApp = this.currentApp || ''; // Construct proper URL with correct parameter order - const newUrl = `/app?=${currentApp}&tab=tasks&task=${taskId}`; + const newUrl = `/app/${currentApp}?tab=tasks&task=${taskId}`; // console.log('🔍 Updating URL with task parameter:', newUrl); history.pushState({}, '', newUrl); } diff --git a/containers/libreportal/frontend/js/components/app/apps-manager.js b/containers/libreportal/frontend/js/components/app/apps-manager.js index bc46e6b..7c66b62 100755 --- a/containers/libreportal/frontend/js/components/app/apps-manager.js +++ b/containers/libreportal/frontend/js/components/app/apps-manager.js @@ -55,7 +55,7 @@ class AppsManager { // reset password) show without needing a page refresh. Don't // switch tabs — they may be reading the tool's task log. const url = new URL(window.location.href); - const currentAppFromUrl = url.searchParams.get('app') || url.searchParams.get(''); + const currentAppFromUrl = decodeURIComponent((window.location.pathname.match(/^\/app\/([^/?]+)/) || [])[1] || '') || url.searchParams.get('app') || url.searchParams.get(''); const onAppDetail = window.location.pathname === '/app' || window.location.pathname.startsWith('/app/'); if (onAppDetail && appName && currentAppFromUrl === appName) { this.displayConfigForm?.((window.apps || []).find(a => @@ -115,7 +115,7 @@ class AppsManager { } const currentUrl = new URL(window.location.href); - const currentAppFromUrl = currentUrl.searchParams.get('app') || currentUrl.searchParams.get(''); + const currentAppFromUrl = decodeURIComponent((window.location.pathname.match(/^\/app\/([^/?]+)/) || [])[1] || '') || currentUrl.searchParams.get('app') || currentUrl.searchParams.get(''); const pathname = window.location.pathname; const isAppsPage = pathname === '/apps' || pathname.startsWith('/apps/'); const isAppDetailPage = pathname === '/app' || pathname.startsWith('/app/'); @@ -251,7 +251,7 @@ class AppsManager { const searchParams = new URLSearchParams(window.location.search); if (path === '/app' || path.startsWith('/app/') || searchParams.has('app')) { - const appName = searchParams.get('app') || window.appName || ''; + const appName = decodeURIComponent((path.match(/^\/app\/([^/?]+)/) || [])[1] || '') || searchParams.get('app') || window.appName || ''; this.showAppDetail(appName); } else { // Use the category parsed by SPA @@ -266,7 +266,7 @@ class AppsManager { // Update URL only for specific categories, not for 'all' if (category && category !== 'all') { - history.pushState({}, '', `/apps?=${category}`); + history.pushState({}, '', `/apps/${category}`); } // For 'all' category, keep URL as /apps to avoid redirect loops @@ -311,7 +311,7 @@ class AppsManager { // console.log('🔍 Preserving existing tab:', targetTab); } - const newUrl = `/app?=${appName}&tab=${targetTab}`; + const newUrl = `/app/${appName}?tab=${targetTab}`; // console.log('🔍 Setting URL to:', newUrl); history.pushState({}, '', newUrl); @@ -369,7 +369,7 @@ class AppsManager { } // Set URL to target tab (config or tasks) - const newUrl = `/app?=${appName}&tab=${targetTab}`; + const newUrl = `/app/${appName}?tab=${targetTab}`; history.pushState({}, '', newUrl); // Update app-tabbed-manager. setCurrentApp clears stale disable state from @@ -509,7 +509,7 @@ class AppsManager { if (categoryId === 'all') { history.pushState({}, '', '/apps'); } else { - history.pushState({}, '', `/apps?=${categoryId}`); + history.pushState({}, '', `/apps/${categoryId}`); } // Direct view update without URL change to avoid conflicts @@ -2086,9 +2086,9 @@ class AppsManager { navigateToServiceApp(slug) { if (typeof window.navigateToApp === 'function') return window.navigateToApp(slug); - if (window.librePortalSPA?.navigate) return window.librePortalSPA.navigate(`/app?=${slug}`); - if (typeof window.navigateToRoute === 'function') return window.navigateToRoute(`app?=${slug}`); - window.location.href = `/app?=${slug}`; + if (window.librePortalSPA?.navigate) return window.librePortalSPA.navigate(`/app/${slug}`); + if (typeof window.navigateToRoute === 'function') return window.navigateToRoute(`app/${slug}`); + window.location.href = `/app/${slug}`; } // Find matching CFG_ key for a field (working method from app-config-original.js) @@ -3221,10 +3221,10 @@ class AppsManager { }, 500); } else if (window.librePortalSPA) { // Fallback: navigate to app with tasks tab - const taskUrl = task ? `/app?=${appName}&tab=tasks&task=${task.id}` : `/app?=${appName}&tab=tasks`; + const taskUrl = task ? `/app/${appName}?tab=tasks&task=${task.id}` : `/app/${appName}?tab=tasks`; window.librePortalSPA.navigateTo(taskUrl); } else if (window.navigateToRoute) { - window.navigateToRoute(`app?=${appName}&tab=tasks${task ? `&task=${task.id}` : ''}`); + window.navigateToRoute(`app/${appName}?tab=tasks${task ? `&task=${task.id}` : ''}`); } }, 1000); @@ -3466,11 +3466,11 @@ class AppsManager { }, 500); } else if (window.librePortalSPA) { // Fallback: navigate to app with tasks tab - const taskUrl = task ? `/app?=${appName}&tab=tasks&task=${task.id}` : `/app?=${appName}&tab=tasks`; + const taskUrl = task ? `/app/${appName}?tab=tasks&task=${task.id}` : `/app/${appName}?tab=tasks`; // console.log(`🔄 Navigating to app tasks with uninstall task: ${task?.id}`); window.librePortalSPA.navigateTo(taskUrl); } else if (window.navigateToRoute) { - window.navigateToRoute(`app?=${appName}&tab=tasks${task ? `&task=${task.id}` : ''}`); + window.navigateToRoute(`app/${appName}?tab=tasks${task ? `&task=${task.id}` : ''}`); } }, 1000); diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js index 27e7dd8..b62eb33 100644 --- a/containers/libreportal/frontend/js/components/backup/backup-page.js +++ b/containers/libreportal/frontend/js/components/backup/backup-page.js @@ -117,6 +117,10 @@ class BackupPage { either source resolve correctly. */ parseTabFromUrl() { const allowed = new Set(['dashboard', 'backups', 'locations', 'configuration']); + // Path-based: /backup/ (bare /backup → default tab). + const seg = window.location.pathname.replace(/^\/backup\/?/, '').split('/')[0]; + if (seg && allowed.has(seg)) return seg; + // Legacy ?= / ?backup= / ?tab= for old links. const search = window.location.search || ''; const legacy = search.match(/\?=([^&]+)/); if (legacy && allowed.has(legacy[1])) return legacy[1]; @@ -376,7 +380,7 @@ class BackupPage { } pushTabToUrl(tab) { - const url = `/backup?=${tab}`; + const url = `/backup/${tab}`; // Use replaceState for the *first* push (initial tab inferred from // URL); otherwise pushState so back/forward navigates between tabs. if (!this._pushedAnyTab) { diff --git a/containers/libreportal/frontend/js/components/config/config-form.js b/containers/libreportal/frontend/js/components/config/config-form.js index b2d6b8d..cc3a9d8 100755 --- a/containers/libreportal/frontend/js/components/config/config-form.js +++ b/containers/libreportal/frontend/js/components/config/config-form.js @@ -82,9 +82,9 @@ class ConfigForm { ); if (task && window.librePortalSPA && typeof window.librePortalSPA.navigate === 'function') { - setTimeout(() => window.librePortalSPA.navigate(`/tasks?=all&task=${task.id}`), 400); + setTimeout(() => window.librePortalSPA.navigate(`/tasks/all?task=${task.id}`), 400); } else if (task && window.navigateToRoute) { - setTimeout(() => window.navigateToRoute(`tasks?=all&task=${task.id}`), 400); + setTimeout(() => window.navigateToRoute(`tasks/all?task=${task.id}`), 400); } } catch (error) { console.error('ConfigForm: Error saving configuration:', error); diff --git a/containers/libreportal/frontend/js/components/dashboard.js b/containers/libreportal/frontend/js/components/dashboard.js index d358d02..dcf3b4a 100755 --- a/containers/libreportal/frontend/js/components/dashboard.js +++ b/containers/libreportal/frontend/js/components/dashboard.js @@ -39,7 +39,7 @@ function createInstalledAppCard(app) { const shortName = app.name.split(' - ')[0].trim(); return ` -
+
${shortName}
@@ -103,12 +103,12 @@ function setupEventListeners() { function navigateToApp(appName) { // Use proper SPA navigation to the app page if (window.librePortalSPA && typeof window.librePortalSPA.navigate === 'function') { - window.librePortalSPA.navigate(`/app?=${appName}`); + window.librePortalSPA.navigate(`/app/${appName}`); } else if (window.navigateToRoute && typeof window.navigateToRoute === 'function') { - window.navigateToRoute(`app?=${appName}`); + window.navigateToRoute(`app/${appName}`); } else { // Fallback to direct navigation - window.location.href = `/app?=${appName}`; + window.location.href = `/app/${appName}`; } } diff --git a/containers/libreportal/frontend/js/components/notifications.js b/containers/libreportal/frontend/js/components/notifications.js index d3714ff..43bda97 100755 --- a/containers/libreportal/frontend/js/components/notifications.js +++ b/containers/libreportal/frontend/js/components/notifications.js @@ -346,7 +346,7 @@ window.handleNotificationNavigation = (url) => { // We're on an app page - navigate to the specified app and tab if (window.appTabbedManager) { // Update the URL to the target app/tab/task - const newUrl = `/app?=${appName}&tab=${tab}&task=${taskId}`; + const newUrl = `/app/${appName}?tab=${tab}&task=${taskId}`; console.log('🔗 Pushing state to URL:', newUrl); window.history.pushState({}, '', newUrl); @@ -402,7 +402,7 @@ window.handleNotificationNavigation = (url) => { // We're on the tasks page, navigate to the specified task if (taskId) { console.log('🔗 On tasks page, opening task:', taskId); - window.history.pushState({}, '', `/tasks?=all&task=${taskId}`); + window.history.pushState({}, '', `/tasks/all?task=${taskId}`); setTimeout(() => { if (typeof window.toggleTaskDetails === 'function') { console.log('🔗 Opening task details for:', taskId); @@ -414,7 +414,7 @@ window.handleNotificationNavigation = (url) => { } else { // Not on app or tasks page - navigate to the app's tasks tab if (appName && tab) { - window.history.pushState({}, '', `/app?=${appName}&tab=${tab}&task=${taskId}`); + window.history.pushState({}, '', `/app/${appName}?tab=${tab}&task=${taskId}`); // Let the SPA handle the navigation if (window.appTabbedManager) { window.appTabbedManager.showAppDetail(appName); diff --git a/containers/libreportal/frontend/js/components/task/task-actions.js b/containers/libreportal/frontend/js/components/task/task-actions.js index 3720bb0..d2611b2 100755 --- a/containers/libreportal/frontend/js/components/task/task-actions.js +++ b/containers/libreportal/frontend/js/components/task/task-actions.js @@ -295,10 +295,10 @@ async configUpdate(changes) { let taskUrl; const currentUrl = window.location.href; - if (currentUrl.includes('/app?=') && appName) { - taskUrl = `/app?=${appName}&tab=tasks&task=${task.id}`; + if (currentUrl.includes('/app/') && appName) { + taskUrl = `/app/${appName}?tab=tasks&task=${task.id}`; } else { - taskUrl = `/tasks?=all&task=${task.id}`; + taskUrl = `/tasks/all?task=${task.id}`; } if (window.notificationSystem) { @@ -364,12 +364,12 @@ async configUpdate(changes) { // console.log('🔍 TaskActions: Current URL:', currentUrl); // Always generate URL with app name for proper navigation - if (currentUrl.includes('/app?=') && appName) { + if (currentUrl.includes('/app/') && appName) { // We're on an app page, maintain app context - taskUrl = `/app?=${appName}&tab=tasks&task=${task.id}`; + taskUrl = `/app/${appName}?tab=tasks&task=${task.id}`; } else { // We're on main tasks page, use normal URL - taskUrl = `/tasks?=all&task=${task.id}`; + taskUrl = `/tasks/all?task=${task.id}`; } // Show success notification with app icon and direct link diff --git a/containers/libreportal/frontend/js/components/tasks/tasks-manager.js b/containers/libreportal/frontend/js/components/tasks/tasks-manager.js index 8ec27a1..6a8d989 100755 --- a/containers/libreportal/frontend/js/components/tasks/tasks-manager.js +++ b/containers/libreportal/frontend/js/components/tasks/tasks-manager.js @@ -122,11 +122,12 @@ class TasksManager { const searchParams = currentUrl.searchParams; // Check if we're on the main tasks page (not app page) - const isMainTasksPage = currentUrl.pathname === '/tasks' || currentUrl.pathname === '/tasks.html'; - + const isMainTasksPage = currentUrl.pathname === '/tasks' || currentUrl.pathname.startsWith('/tasks/') || currentUrl.pathname === '/tasks.html'; + if (isMainTasksPage) { - // On main tasks page, get category from URL - this.currentCategory = searchParams.get('') || 'all'; + // Category from the path (/tasks/), else legacy ?=. + const seg = currentUrl.pathname.replace(/^\/tasks\/?/, '').split('/')[0]; + this.currentCategory = seg || searchParams.get('') || 'all'; // Only check for specific task parameter if we're not coming from an app page const taskParam = searchParams.get('task'); @@ -149,9 +150,9 @@ class TasksManager { updateURL(category, taskId = null) { // Update URL without page reload and without hash - let newURL = `/tasks?=${category}`; + let newURL = `/tasks/${category || 'all'}`; if (taskId) { - newURL += `&task=${taskId}`; + newURL += `?task=${taskId}`; } // Prevent the SPA from interfering @@ -619,13 +620,13 @@ class TasksManager {
- Task ID: ${task.id} + Task ID: ${task.id}
Type: ${task.type || 'unknown'}
- App: ${task.app ? `${task.app}` : 'system'} + App: ${task.app ? `${task.app}` : 'system'}
Created: ${new Date(task.createdAt).toLocaleString()} @@ -1334,7 +1335,7 @@ class TasksManager { // Show success notification with app icon and direct link if (window.notificationSystem) { - const taskUrl = `/tasks?=all&task=${task.id}`; + const taskUrl = `/tasks/all?task=${task.id}`; const typeIcon = this.getTaskTypeIcon ? this.getTaskTypeIcon({ type: action })?.icon : ''; const customIcon = typeIcon ? `${typeIcon}` : null; window.notificationSystem.show( @@ -1455,8 +1456,8 @@ class TasksManager { const subjectLabel = isSystemTask ? 'System' : 'App'; const onAppPage = window.location.pathname.startsWith('/app') && !window.location.pathname.startsWith('/apps'); const url = (onAppPage && appName) - ? `/app?=${appName}&tab=tasks&task=${taskId}` - : `/tasks?=all&task=${taskId}`; + ? `/app/${appName}?tab=tasks&task=${taskId}` + : `/tasks/all?task=${taskId}`; const icon = appName ? `icons/apps/${appName}.svg` : null; // Match the per-action emoji used in the task list rows (see diff --git a/containers/libreportal/frontend/js/components/topbar.js b/containers/libreportal/frontend/js/components/topbar.js index 329b9c8..0f3dbd8 100755 --- a/containers/libreportal/frontend/js/components/topbar.js +++ b/containers/libreportal/frontend/js/components/topbar.js @@ -145,7 +145,7 @@ class TopbarComponent { window.tasksManager.tasks = []; // Clear the task list // Clear URL parameters - window.history.pushState({ category: 'all', taskId: null }, '', '/tasks?=all'); + window.history.pushState({ category: 'all', taskId: null }, '', '/tasks/all'); // Clear any localStorage filters localStorage.removeItem('tasksDefaultFilter'); diff --git a/containers/libreportal/frontend/js/spa.js b/containers/libreportal/frontend/js/spa.js index b9dc866..2124d8a 100755 --- a/containers/libreportal/frontend/js/spa.js +++ b/containers/libreportal/frontend/js/spa.js @@ -66,8 +66,9 @@ class LibrePortalSPAClean { this.routes.set('/', () => this.handleDashboard()); this.routes.set('/dashboard', () => this.handleDashboard()); 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('/apps*', () => this.handleApps()); // /apps/ — MUST precede /app* (/apps startsWith /app) + this.routes.set('/app', () => this.handleAppDetail()); // /app/ + this.routes.set('/app*', () => this.handleAppDetail()); 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 @@ -279,17 +280,17 @@ class LibrePortalSPAClean { async handleApps() { //console.log('📱 Loading apps...'); - // Handle query parameters for apps - const path = window.location.pathname + window.location.search; - if (path.includes('?=')) { - const [basePath, query] = path.split('?='); - window.appsCategory = query || 'all'; - } else if (path.includes('?')) { - const url = new URL(path, window.location.origin); - const searchParams = url.searchParams; - window.appsCategory = searchParams.get('apps') || 'all'; + // Category from the path (/apps/), else legacy ?= / ?apps=. + const seg = window.location.pathname.replace(/^\/apps\/?/, '').split('/')[0]; + if (seg) { + window.appsCategory = decodeURIComponent(seg); } else { - window.appsCategory = 'all'; + 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'; + } } try { @@ -321,10 +322,13 @@ class LibrePortalSPAClean { async handleAppDetail() { //console.log('🔍 Loading app detail...'); - // Extract app name from URL + // Extract app name. Path-based /app/ first, then legacy ?app= / ?=name. const url = new URL(window.location); - let appName = url.searchParams.get('app'); - + let appName = url.pathname.replace(/^\/app\/?/, '').split('/')[0]; + appName = appName ? decodeURIComponent(appName) : ''; + + if (!appName) appName = url.searchParams.get('app'); + // Handle old format ?=appname&tab=tabname if (!appName && url.search.includes('?=')) { const queryPart = url.search.replace('?', ''); diff --git a/containers/libreportal/frontend/js/system/setup-completion-watcher.js b/containers/libreportal/frontend/js/system/setup-completion-watcher.js index d1375b0..d6377e0 100644 --- a/containers/libreportal/frontend/js/system/setup-completion-watcher.js +++ b/containers/libreportal/frontend/js/system/setup-completion-watcher.js @@ -142,13 +142,13 @@ } function navigateToRecommendedApps() { - const route = '/apps?=recommended'; + const route = '/apps/recommended'; if (window.spaClean && typeof window.spaClean.navigate === 'function') { window.spaClean.navigate(route); } else if (typeof window.navigateToRoute === 'function') { window.navigateToRoute(route); } else { - window.location.href = 'apps.html?=recommended'; + window.location.href = '/apps/recommended'; } } diff --git a/containers/libreportal/frontend/js/utils/data-loader.js b/containers/libreportal/frontend/js/utils/data-loader.js index ffe1d28..455a749 100755 --- a/containers/libreportal/frontend/js/utils/data-loader.js +++ b/containers/libreportal/frontend/js/utils/data-loader.js @@ -147,7 +147,7 @@ async function initializeData() { if (currentPath === '/dashboard' || currentPath === '/') { // Dashboard: Only installed apps + system info (no categories needed) await loadDashboardData(); - } else if (currentPath === '/apps' || currentPath === '/app' || searchParams.has('apps') || searchParams.has('app')) { + } else if (currentPath.startsWith('/apps') || currentPath.startsWith('/app') || searchParams.has('apps') || searchParams.has('app')) { // Apps page: All apps + categories (no system configs needed) await loadAppsPageData(); } else if (currentPath.startsWith('/admin') || currentPath.startsWith('/config') || currentPath.startsWith('/ssh') || searchParams.has('config')) {