Merge claude/1

This commit is contained in:
librelad 2026-05-23 19:03:54 +01:00
commit 7e7a7f524c
12 changed files with 98 additions and 143 deletions

View File

@ -97,15 +97,15 @@ class AppTabbedManager {
// Get app name from URL parameter // Get app name from URL parameter
getAppFromURL() { getAppFromURL() {
const urlParams = new URLSearchParams(window.location.search); // Path-based /app/<name> first, then legacy ?app= / ?=name.
let appName = urlParams.get('app'); let appName = window.location.pathname.replace(/^\/app\/?/, '').split('/')[0];
appName = appName ? decodeURIComponent(appName) : '';
// Fallback to old format if app param not found
if (!appName) { if (!appName) {
const fullPath = window.location.search; const urlParams = new URLSearchParams(window.location.search);
if (fullPath.includes('?=')) { appName = urlParams.get('app');
const [basePath, query] = fullPath.split('?='); if (!appName && window.location.search.includes('?=')) {
appName = query.split('&')[0]; // Get only the app name, ignore other params appName = window.location.search.split('?=')[1].split('&')[0];
} }
} }
@ -163,88 +163,34 @@ class AppTabbedManager {
search.includes('?=')); // Old format app pages search.includes('?=')); // Old format app pages
} }
// Update URL with app and tab // Update URL with app and tab — path-based: /app/<app>?tab=<tab>&task=<task>.
updateURL(app = null, tab = null) { updateURL(app = null, tab = null) {
// console.log('🔍 updateURL called with:', { app, tab }); // Only update URLs on app pages - prevent interference with other pages.
// console.log('🔍 Current URL before update:', window.location.href); if (!this.isAppPage()) return;
// Only update URLs on app pages - prevent interference with other pages const params = new URLSearchParams(window.location.search);
if (!this.isAppPage()) { const fromPath = window.location.pathname.replace(/^\/app\/?/, '').split('/')[0];
// console.log('🚫 Not on app page, skipping URL update'); const currentApp = app || (fromPath ? decodeURIComponent(fromPath) : '') || this.currentApp;
return; 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 qs = q.toString();
const params = new URLSearchParams(url.search); window.history.replaceState({}, '', `/app/${encodeURIComponent(currentApp)}${qs ? '?' + qs : ''}`);
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}`);
}
}
} }
// Update current app and refresh content // Update current app and refresh content
updateApp(newAppName) { updateApp(newAppName) {
this.setCurrentApp(newAppName); this.setCurrentApp(newAppName);
// Reset to the config tab on the path-based app URL.
// Reset URL to config tab history.replaceState({}, '', `/app/${encodeURIComponent(newAppName)}?tab=config`);
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);
this.switchTab('config'); this.switchTab('config');
} }
@ -593,7 +539,7 @@ class AppTabbedManager {
const currentApp = this.currentApp || ''; const currentApp = this.currentApp || '';
// Construct proper URL with correct parameter order // 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); // console.log('🔍 Updating URL with task parameter:', newUrl);
history.pushState({}, '', newUrl); history.pushState({}, '', newUrl);
} }

View File

@ -55,7 +55,7 @@ class AppsManager {
// reset password) show without needing a page refresh. Don't // reset password) show without needing a page refresh. Don't
// switch tabs — they may be reading the tool's task log. // switch tabs — they may be reading the tool's task log.
const url = new URL(window.location.href); 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/'); const onAppDetail = window.location.pathname === '/app' || window.location.pathname.startsWith('/app/');
if (onAppDetail && appName && currentAppFromUrl === appName) { if (onAppDetail && appName && currentAppFromUrl === appName) {
this.displayConfigForm?.((window.apps || []).find(a => this.displayConfigForm?.((window.apps || []).find(a =>
@ -115,7 +115,7 @@ class AppsManager {
} }
const currentUrl = new URL(window.location.href); 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 pathname = window.location.pathname;
const isAppsPage = pathname === '/apps' || pathname.startsWith('/apps/'); const isAppsPage = pathname === '/apps' || pathname.startsWith('/apps/');
const isAppDetailPage = pathname === '/app' || pathname.startsWith('/app/'); const isAppDetailPage = pathname === '/app' || pathname.startsWith('/app/');
@ -251,7 +251,7 @@ class AppsManager {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
if (path === '/app' || path.startsWith('/app/') || searchParams.has('app')) { 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); this.showAppDetail(appName);
} else { } else {
// Use the category parsed by SPA // Use the category parsed by SPA
@ -266,7 +266,7 @@ class AppsManager {
// Update URL only for specific categories, not for 'all' // Update URL only for specific categories, not for 'all'
if (category && category !== 'all') { if (category && category !== 'all') {
history.pushState({}, '', `/apps?=${category}`); history.pushState({}, '', `/apps/${category}`);
} }
// For 'all' category, keep URL as /apps to avoid redirect loops // For 'all' category, keep URL as /apps to avoid redirect loops
@ -311,7 +311,7 @@ class AppsManager {
// console.log('🔍 Preserving existing tab:', targetTab); // console.log('🔍 Preserving existing tab:', targetTab);
} }
const newUrl = `/app?=${appName}&tab=${targetTab}`; const newUrl = `/app/${appName}?tab=${targetTab}`;
// console.log('🔍 Setting URL to:', newUrl); // console.log('🔍 Setting URL to:', newUrl);
history.pushState({}, '', newUrl); history.pushState({}, '', newUrl);
@ -369,7 +369,7 @@ class AppsManager {
} }
// Set URL to target tab (config or tasks) // Set URL to target tab (config or tasks)
const newUrl = `/app?=${appName}&tab=${targetTab}`; const newUrl = `/app/${appName}?tab=${targetTab}`;
history.pushState({}, '', newUrl); history.pushState({}, '', newUrl);
// Update app-tabbed-manager. setCurrentApp clears stale disable state from // Update app-tabbed-manager. setCurrentApp clears stale disable state from
@ -509,7 +509,7 @@ class AppsManager {
if (categoryId === 'all') { if (categoryId === 'all') {
history.pushState({}, '', '/apps'); history.pushState({}, '', '/apps');
} else { } else {
history.pushState({}, '', `/apps?=${categoryId}`); history.pushState({}, '', `/apps/${categoryId}`);
} }
// Direct view update without URL change to avoid conflicts // Direct view update without URL change to avoid conflicts
@ -2086,9 +2086,9 @@ class AppsManager {
navigateToServiceApp(slug) { navigateToServiceApp(slug) {
if (typeof window.navigateToApp === 'function') return window.navigateToApp(slug); if (typeof window.navigateToApp === 'function') return window.navigateToApp(slug);
if (window.librePortalSPA?.navigate) return window.librePortalSPA.navigate(`/app?=${slug}`); if (window.librePortalSPA?.navigate) return window.librePortalSPA.navigate(`/app/${slug}`);
if (typeof window.navigateToRoute === 'function') return window.navigateToRoute(`app?=${slug}`); if (typeof window.navigateToRoute === 'function') return window.navigateToRoute(`app/${slug}`);
window.location.href = `/app?=${slug}`; window.location.href = `/app/${slug}`;
} }
// Find matching CFG_ key for a field (working method from app-config-original.js) // Find matching CFG_ key for a field (working method from app-config-original.js)
@ -3221,10 +3221,10 @@ class AppsManager {
}, 500); }, 500);
} else if (window.librePortalSPA) { } else if (window.librePortalSPA) {
// Fallback: navigate to app with tasks tab // 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); window.librePortalSPA.navigateTo(taskUrl);
} else if (window.navigateToRoute) { } 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); }, 1000);
@ -3466,11 +3466,11 @@ class AppsManager {
}, 500); }, 500);
} else if (window.librePortalSPA) { } else if (window.librePortalSPA) {
// Fallback: navigate to app with tasks tab // 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}`); // console.log(`🔄 Navigating to app tasks with uninstall task: ${task?.id}`);
window.librePortalSPA.navigateTo(taskUrl); window.librePortalSPA.navigateTo(taskUrl);
} else if (window.navigateToRoute) { } 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); }, 1000);

View File

@ -117,6 +117,10 @@ class BackupPage {
either source resolve correctly. */ either source resolve correctly. */
parseTabFromUrl() { parseTabFromUrl() {
const allowed = new Set(['dashboard', 'backups', 'locations', 'configuration']); const allowed = new Set(['dashboard', 'backups', 'locations', 'configuration']);
// Path-based: /backup/<tab> (bare /backup → default tab).
const seg = window.location.pathname.replace(/^\/backup\/?/, '').split('/')[0];
if (seg && allowed.has(seg)) return seg;
// Legacy ?=<tab> / ?backup=<tab> / ?tab=<tab> for old links.
const search = window.location.search || ''; const search = window.location.search || '';
const legacy = search.match(/\?=([^&]+)/); const legacy = search.match(/\?=([^&]+)/);
if (legacy && allowed.has(legacy[1])) return legacy[1]; if (legacy && allowed.has(legacy[1])) return legacy[1];
@ -376,7 +380,7 @@ class BackupPage {
} }
pushTabToUrl(tab) { pushTabToUrl(tab) {
const url = `/backup?=${tab}`; const url = `/backup/${tab}`;
// Use replaceState for the *first* push (initial tab inferred from // Use replaceState for the *first* push (initial tab inferred from
// URL); otherwise pushState so back/forward navigates between tabs. // URL); otherwise pushState so back/forward navigates between tabs.
if (!this._pushedAnyTab) { if (!this._pushedAnyTab) {

View File

@ -82,9 +82,9 @@ class ConfigForm {
); );
if (task && window.librePortalSPA && typeof window.librePortalSPA.navigate === 'function') { 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) { } 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) { } catch (error) {
console.error('ConfigForm: Error saving configuration:', error); console.error('ConfigForm: Error saving configuration:', error);

View File

@ -39,7 +39,7 @@ function createInstalledAppCard(app) {
const shortName = app.name.split(' - ')[0].trim(); const shortName = app.name.split(' - ')[0].trim();
return ` return `
<div class="frontpage-app-tile" onclick="window.location.href='/app?=${appName}'"> <div class="frontpage-app-tile" onclick="window.location.href='/app/${appName}'">
<div class="frontpage-app-icon-wrap"> <div class="frontpage-app-icon-wrap">
<img src="${icon}" alt="${shortName}" onerror="this.src='/icons/apps/default.svg'"> <img src="${icon}" alt="${shortName}" onerror="this.src='/icons/apps/default.svg'">
<div class="frontpage-app-overlay" id="frontpage-overlay-${appName}" onclick="event.stopPropagation()"></div> <div class="frontpage-app-overlay" id="frontpage-overlay-${appName}" onclick="event.stopPropagation()"></div>
@ -103,12 +103,12 @@ function setupEventListeners() {
function navigateToApp(appName) { function navigateToApp(appName) {
// Use proper SPA navigation to the app page // Use proper SPA navigation to the app page
if (window.librePortalSPA && typeof window.librePortalSPA.navigate === 'function') { 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') { } else if (window.navigateToRoute && typeof window.navigateToRoute === 'function') {
window.navigateToRoute(`app?=${appName}`); window.navigateToRoute(`app/${appName}`);
} else { } else {
// Fallback to direct navigation // Fallback to direct navigation
window.location.href = `/app?=${appName}`; window.location.href = `/app/${appName}`;
} }
} }

View File

@ -346,7 +346,7 @@ window.handleNotificationNavigation = (url) => {
// We're on an app page - navigate to the specified app and tab // We're on an app page - navigate to the specified app and tab
if (window.appTabbedManager) { if (window.appTabbedManager) {
// Update the URL to the target app/tab/task // 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); console.log('🔗 Pushing state to URL:', newUrl);
window.history.pushState({}, '', newUrl); window.history.pushState({}, '', newUrl);
@ -402,7 +402,7 @@ window.handleNotificationNavigation = (url) => {
// We're on the tasks page, navigate to the specified task // We're on the tasks page, navigate to the specified task
if (taskId) { if (taskId) {
console.log('🔗 On tasks page, opening task:', taskId); console.log('🔗 On tasks page, opening task:', taskId);
window.history.pushState({}, '', `/tasks?=all&task=${taskId}`); window.history.pushState({}, '', `/tasks/all?task=${taskId}`);
setTimeout(() => { setTimeout(() => {
if (typeof window.toggleTaskDetails === 'function') { if (typeof window.toggleTaskDetails === 'function') {
console.log('🔗 Opening task details for:', taskId); console.log('🔗 Opening task details for:', taskId);
@ -414,7 +414,7 @@ window.handleNotificationNavigation = (url) => {
} else { } else {
// Not on app or tasks page - navigate to the app's tasks tab // Not on app or tasks page - navigate to the app's tasks tab
if (appName && 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 // Let the SPA handle the navigation
if (window.appTabbedManager) { if (window.appTabbedManager) {
window.appTabbedManager.showAppDetail(appName); window.appTabbedManager.showAppDetail(appName);

View File

@ -295,10 +295,10 @@ async configUpdate(changes) {
let taskUrl; let taskUrl;
const currentUrl = window.location.href; const currentUrl = window.location.href;
if (currentUrl.includes('/app?=') && appName) { if (currentUrl.includes('/app/') && appName) {
taskUrl = `/app?=${appName}&tab=tasks&task=${task.id}`; taskUrl = `/app/${appName}?tab=tasks&task=${task.id}`;
} else { } else {
taskUrl = `/tasks?=all&task=${task.id}`; taskUrl = `/tasks/all?task=${task.id}`;
} }
if (window.notificationSystem) { if (window.notificationSystem) {
@ -364,12 +364,12 @@ async configUpdate(changes) {
// console.log('🔍 TaskActions: Current URL:', currentUrl); // console.log('🔍 TaskActions: Current URL:', currentUrl);
// Always generate URL with app name for proper navigation // 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 // 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 { } else {
// We're on main tasks page, use normal URL // 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 // Show success notification with app icon and direct link

View File

@ -122,11 +122,12 @@ class TasksManager {
const searchParams = currentUrl.searchParams; const searchParams = currentUrl.searchParams;
// Check if we're on the main tasks page (not app page) // 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) { if (isMainTasksPage) {
// On main tasks page, get category from URL // Category from the path (/tasks/<category>), else legacy ?=<category>.
this.currentCategory = searchParams.get('') || 'all'; 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 // Only check for specific task parameter if we're not coming from an app page
const taskParam = searchParams.get('task'); const taskParam = searchParams.get('task');
@ -149,9 +150,9 @@ class TasksManager {
updateURL(category, taskId = null) { updateURL(category, taskId = null) {
// Update URL without page reload and without hash // Update URL without page reload and without hash
let newURL = `/tasks?=${category}`; let newURL = `/tasks/${category || 'all'}`;
if (taskId) { if (taskId) {
newURL += `&task=${taskId}`; newURL += `?task=${taskId}`;
} }
// Prevent the SPA from interfering // Prevent the SPA from interfering
@ -619,13 +620,13 @@ class TasksManager {
<!-- Task metadata --> <!-- Task metadata -->
<div class="task-meta"> <div class="task-meta">
<div class="meta-item"> <div class="meta-item">
<strong>Task ID:</strong> <a href="/tasks?=all&task=${task.id}" class="task-id-link" data-task-id="${task.id}">${task.id}</a> <strong>Task ID:</strong> <a href="/tasks/all?task=${task.id}" class="task-id-link" data-task-id="${task.id}">${task.id}</a>
</div> </div>
<div class="meta-item"> <div class="meta-item">
<strong>Type:</strong> ${task.type || 'unknown'} <strong>Type:</strong> ${task.type || 'unknown'}
</div> </div>
<div class="meta-item"> <div class="meta-item">
<strong>App:</strong> ${task.app ? `<a href="/app?=${task.app}" class="task-app-link" data-app-name="${task.app}">${task.app}</a>` : 'system'} <strong>App:</strong> ${task.app ? `<a href="/app/${task.app}" class="task-app-link" data-app-name="${task.app}">${task.app}</a>` : 'system'}
</div> </div>
<div class="meta-item"> <div class="meta-item">
<strong>Created:</strong> ${new Date(task.createdAt).toLocaleString()} <strong>Created:</strong> ${new Date(task.createdAt).toLocaleString()}
@ -1334,7 +1335,7 @@ class TasksManager {
// Show success notification with app icon and direct link // Show success notification with app icon and direct link
if (window.notificationSystem) { 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 typeIcon = this.getTaskTypeIcon ? this.getTaskTypeIcon({ type: action })?.icon : '';
const customIcon = typeIcon ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : null; const customIcon = typeIcon ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : null;
window.notificationSystem.show( window.notificationSystem.show(
@ -1455,8 +1456,8 @@ class TasksManager {
const subjectLabel = isSystemTask ? 'System' : 'App'; const subjectLabel = isSystemTask ? 'System' : 'App';
const onAppPage = window.location.pathname.startsWith('/app') && !window.location.pathname.startsWith('/apps'); const onAppPage = window.location.pathname.startsWith('/app') && !window.location.pathname.startsWith('/apps');
const url = (onAppPage && appName) const url = (onAppPage && appName)
? `/app?=${appName}&tab=tasks&task=${taskId}` ? `/app/${appName}?tab=tasks&task=${taskId}`
: `/tasks?=all&task=${taskId}`; : `/tasks/all?task=${taskId}`;
const icon = appName ? `icons/apps/${appName}.svg` : null; const icon = appName ? `icons/apps/${appName}.svg` : null;
// Match the per-action emoji used in the task list rows (see // Match the per-action emoji used in the task list rows (see

View File

@ -145,7 +145,7 @@ class TopbarComponent {
window.tasksManager.tasks = []; // Clear the task list window.tasksManager.tasks = []; // Clear the task list
// Clear URL parameters // 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 // Clear any localStorage filters
localStorage.removeItem('tasksDefaultFilter'); localStorage.removeItem('tasksDefaultFilter');

View File

@ -66,8 +66,9 @@ class LibrePortalSPAClean {
this.routes.set('/', () => this.handleDashboard()); this.routes.set('/', () => this.handleDashboard());
this.routes.set('/dashboard', () => this.handleDashboard()); this.routes.set('/dashboard', () => this.handleDashboard());
this.routes.set('/apps', () => this.handleApps()); this.routes.set('/apps', () => this.handleApps());
this.routes.set('/app', () => this.handleAppDetail()); // Handle /app without query this.routes.set('/apps*', () => this.handleApps()); // /apps/<category> — MUST precede /app* (/apps startsWith /app)
this.routes.set('/app*', () => this.handleAppDetail()); // Handle /app with query this.routes.set('/app', () => this.handleAppDetail()); // /app/<name>
this.routes.set('/app*', () => this.handleAppDetail());
this.routes.set('/admin', () => this.handleAdmin()); // Admin area (path-based: /admin/config/<x>, /admin/tools/<x>) this.routes.set('/admin', () => this.handleAdmin()); // Admin area (path-based: /admin/config/<x>, /admin/tools/<x>)
this.routes.set('/admin*', () => this.handleAdmin()); this.routes.set('/admin*', () => this.handleAdmin());
this.routes.set('/config', () => this.handleConfigRedirect()); // legacy → /admin this.routes.set('/config', () => this.handleConfigRedirect()); // legacy → /admin
@ -279,17 +280,17 @@ class LibrePortalSPAClean {
async handleApps() { async handleApps() {
//console.log('📱 Loading apps...'); //console.log('📱 Loading apps...');
// Handle query parameters for apps // Category from the path (/apps/<category>), else legacy ?=<cat> / ?apps=.
const path = window.location.pathname + window.location.search; const seg = window.location.pathname.replace(/^\/apps\/?/, '').split('/')[0];
if (path.includes('?=')) { if (seg) {
const [basePath, query] = path.split('?='); window.appsCategory = decodeURIComponent(seg);
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';
} else { } 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 { try {
@ -321,10 +322,13 @@ class LibrePortalSPAClean {
async handleAppDetail() { async handleAppDetail() {
//console.log('🔍 Loading app detail...'); //console.log('🔍 Loading app detail...');
// Extract app name from URL // Extract app name. Path-based /app/<name> first, then legacy ?app= / ?=name.
const url = new URL(window.location); 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 // Handle old format ?=appname&tab=tabname
if (!appName && url.search.includes('?=')) { if (!appName && url.search.includes('?=')) {
const queryPart = url.search.replace('?', ''); const queryPart = url.search.replace('?', '');

View File

@ -142,13 +142,13 @@
} }
function navigateToRecommendedApps() { function navigateToRecommendedApps() {
const route = '/apps?=recommended'; const route = '/apps/recommended';
if (window.spaClean && typeof window.spaClean.navigate === 'function') { if (window.spaClean && typeof window.spaClean.navigate === 'function') {
window.spaClean.navigate(route); window.spaClean.navigate(route);
} else if (typeof window.navigateToRoute === 'function') { } else if (typeof window.navigateToRoute === 'function') {
window.navigateToRoute(route); window.navigateToRoute(route);
} else { } else {
window.location.href = 'apps.html?=recommended'; window.location.href = '/apps/recommended';
} }
} }

View File

@ -147,7 +147,7 @@ async function initializeData() {
if (currentPath === '/dashboard' || currentPath === '/') { if (currentPath === '/dashboard' || currentPath === '/') {
// Dashboard: Only installed apps + system info (no categories needed) // Dashboard: Only installed apps + system info (no categories needed)
await loadDashboardData(); 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) // Apps page: All apps + categories (no system configs needed)
await loadAppsPageData(); await loadAppsPageData();
} else if (currentPath.startsWith('/admin') || currentPath.startsWith('/config') || currentPath.startsWith('/ssh') || searchParams.has('config')) { } else if (currentPath.startsWith('/admin') || currentPath.startsWith('/config') || currentPath.startsWith('/ssh') || searchParams.has('config')) {