diff --git a/containers/libreportal/frontend/components/admin/config/js/config-form.js b/containers/libreportal/frontend/components/admin/config/js/config-form.js index cc3a9d8..12d1c95 100755 --- a/containers/libreportal/frontend/components/admin/config/js/config-form.js +++ b/containers/libreportal/frontend/components/admin/config/js/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(window.taskPath('all', task.id)), 400); } else if (task && window.navigateToRoute) { - setTimeout(() => window.navigateToRoute(`tasks/all?task=${task.id}`), 400); + setTimeout(() => window.navigateToRoute(window.taskPath('all', task.id)), 400); } } catch (error) { console.error('ConfigForm: Error saving configuration:', error); diff --git a/containers/libreportal/frontend/components/tasks/js/tasks-list-render.js b/containers/libreportal/frontend/components/tasks/js/tasks-list-render.js index a0438cc..3194195 100644 --- a/containers/libreportal/frontend/components/tasks/js/tasks-list-render.js +++ b/containers/libreportal/frontend/components/tasks/js/tasks-list-render.js @@ -213,7 +213,7 @@ Object.assign(TasksManager.prototype, {
- Task ID: ${task.id} + Task ID: ${task.id}
Type: ${task.type || 'unknown'} diff --git a/containers/libreportal/frontend/components/tasks/js/tasks-manager.js b/containers/libreportal/frontend/components/tasks/js/tasks-manager.js index f2886a2..4ccbbf8 100755 --- a/containers/libreportal/frontend/components/tasks/js/tasks-manager.js +++ b/containers/libreportal/frontend/components/tasks/js/tasks-manager.js @@ -142,18 +142,13 @@ class TasksManager { const isMainTasksPage = currentUrl.pathname === '/tasks' || currentUrl.pathname.startsWith('/tasks/') || currentUrl.pathname === '/tasks.html'; if (isMainTasksPage) { - // 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'); - if (taskParam) { - this.highlightedTaskId = taskParam; - } else { - // Clear any existing highlighted task when on main tasks page without task param - this.highlightedTaskId = null; - } + // Category + single-task deep link from the path (/tasks//), + // with legacy ?= and ?task= queries still honoured. + const parts = (typeof window.taskPartsFromPath === 'function') + ? window.taskPartsFromPath(currentUrl.pathname, currentUrl.search) + : { category: '', taskId: searchParams.get('task') || '' }; + this.currentCategory = parts.category || searchParams.get('') || 'all'; + this.highlightedTaskId = parts.taskId || null; } else { // Not on main tasks page, get default filter from localStorage this.currentCategory = localStorage.getItem('tasksDefaultFilter') || 'all'; @@ -163,11 +158,11 @@ class TasksManager { } updateURL(category, taskId = null) { - // Update URL without page reload and without hash - let newURL = `/tasks/${category || 'all'}`; - if (taskId) { - newURL += `?task=${taskId}`; - } + // Update URL without page reload and without hash. The task deep link is a + // path segment (/tasks//) — see window.taskPath. + const newURL = (typeof window.taskPath === 'function') + ? window.taskPath(category, taskId) + : `/tasks/${category || 'all'}${taskId ? '/' + String(taskId).replace(/^task_/, '') : ''}`; // Prevent the SPA from interfering if (window.librePortalSPA) { @@ -184,7 +179,7 @@ class TasksManager { //// // console.log('🔧 Initializing TasksManager...'); // Re-read the URL on every (re)mount. The SPA reuses this singleton, so a - // navigation to /tasks/?task=X must refresh the category + deep-link + // navigation to /tasks// must refresh the category + deep-link // state — a constructor-only read goes stale after the first visit. this.initializeFromURL(); @@ -415,7 +410,7 @@ class TasksManager { const onAppPage = window.location.pathname.startsWith('/app') && !window.location.pathname.startsWith('/apps'); const url = (onAppPage && appName) ? window.appPath(appName, 'tasks', null, taskId) - : `/tasks/all?task=${taskId}`; + : window.taskPath('all', taskId); // Per-action emoji (install ✅, backup 💾, restore 📦, …) in the // notification's leftmost icon slot, mirroring task-list rows. const customIcon = typeIcon ? `${typeIcon}` : null; diff --git a/containers/libreportal/frontend/core/kernel/js/spa.js b/containers/libreportal/frontend/core/kernel/js/spa.js index 096c0dd..f074258 100755 --- a/containers/libreportal/frontend/core/kernel/js/spa.js +++ b/containers/libreportal/frontend/core/kernel/js/spa.js @@ -657,6 +657,27 @@ window.appPartsFromPath = function (pathname) { return { app, tab, sub }; }; +// Global tasks page path helpers — bring the single-task deep link in line with +// /app/ and /admin/<…>: it's a path segment (/tasks//), not +// a ?task= query. Task ids are guaranteed `task__`, so the +// `task_` prefix is dropped in the URL for brevity and restored on read. A +// legacy ?task= query is still parsed so older links and bookmarks resolve. +window.taskPath = function (category, taskId) { + let p = '/tasks/' + encodeURIComponent(category || 'all'); + if (taskId) p += '/' + encodeURIComponent(String(taskId).replace(/^task_/, '')); + return p; +}; +window.taskPartsFromPath = function (pathname, search) { + const segs = String(pathname || '').replace(/^\/tasks\/?/, '').split('/').filter(Boolean); + const category = segs[0] || ''; + let taskId = segs[1] ? decodeURIComponent(segs[1]) : ''; + if (!taskId && search) { + try { taskId = new URLSearchParams(search).get('task') || ''; } catch (_) {} + } + if (taskId && !/^task_/.test(taskId)) taskId = 'task_' + taskId; + return { category, taskId }; +}; + // Global navigation function for click handlers window.navigateToRoute = function(href) { if (window.spaClean) { diff --git a/containers/libreportal/frontend/core/notifications/js/notifications.js b/containers/libreportal/frontend/core/notifications/js/notifications.js index ae6d3cf..3e2fa42 100755 --- a/containers/libreportal/frontend/core/notifications/js/notifications.js +++ b/containers/libreportal/frontend/core/notifications/js/notifications.js @@ -114,7 +114,7 @@ class NotificationSystem { // Action button (context-aware) if (appName && appUrl) { let buttonText = 'Manage'; - if (appUrl.includes('task=')) { + if (appUrl.includes('task=') || /\/tasks\/[^/]+\/[^/?]+/.test(appUrl)) { buttonText = 'View Task'; } else if (type === 'success' && message.includes('install')) { buttonText = 'Configure'; @@ -399,14 +399,17 @@ window.handleNotificationNavigation = (url) => { console.warn('⚠️ appTabbedManager not available'); } } else if (currentPath.includes('/tasks')) { - // 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}`); + // We're on the tasks page, navigate to the specified task. The target may + // carry the id as a /tasks// path segment or a legacy ?task=. + const tasksTaskId = taskId + || (window.taskPartsFromPath ? window.taskPartsFromPath(urlObj.pathname, urlObj.search).taskId : ''); + if (tasksTaskId) { + console.log('🔗 On tasks page, opening task:', tasksTaskId); + window.history.pushState({}, '', window.taskPath ? window.taskPath('all', tasksTaskId) : url); setTimeout(() => { if (typeof window.toggleTaskDetails === 'function') { - console.log('🔗 Opening task details for:', taskId); - window.toggleTaskDetails(taskId); + console.log('🔗 Opening task details for:', tasksTaskId); + window.toggleTaskDetails(tasksTaskId); } }, 300); return true; diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js index 1d57e1c..6e1e75d 100755 --- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js +++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js @@ -784,10 +784,11 @@ class SetupWizard { this.container.classList.add('setup-launched'); setTimeout(() => { - // Path-based route (the app uses /… URLs); the specific task is still - // selected via ?task=. Navigate via the SPA helper, with an absolute-path - // full-load fallback. - const target = `/tasks/all?task=${encodeURIComponent(firstTaskId)}&from=setup`; + // Path-based route (the app uses /… URLs); the specific task is a path + // segment (/tasks/all/) and ?from=setup flags the install handoff so + // the page follows the running queue. Navigate via the SPA helper, with + // an absolute-path full-load fallback. + const target = `${window.taskPath('all', firstTaskId)}?from=setup`; if (typeof window.navigateToRoute === 'function' && window.spaClean) { window.navigateToRoute(target); this.hide(); diff --git a/containers/libreportal/frontend/core/tasks/js/task-actions.js b/containers/libreportal/frontend/core/tasks/js/task-actions.js index 8742670..465bcee 100755 --- a/containers/libreportal/frontend/core/tasks/js/task-actions.js +++ b/containers/libreportal/frontend/core/tasks/js/task-actions.js @@ -406,7 +406,7 @@ async configUpdate(changes) { if (currentUrl.includes('/app/') && appName) { taskUrl = window.appPath(appName, 'tasks', null, task.id); } else { - taskUrl = `/tasks/all?task=${task.id}`; + taskUrl = window.taskPath('all', task.id); } if (window.notificationSystem) {