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
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/<name> 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/<app>?tab=<tab>&task=<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);
}

View File

@ -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);

View File

@ -117,6 +117,10 @@ class BackupPage {
either source resolve correctly. */
parseTabFromUrl() {
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 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) {

View File

@ -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);

View File

@ -39,7 +39,7 @@ function createInstalledAppCard(app) {
const shortName = app.name.split(' - ')[0].trim();
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">
<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>
@ -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}`;
}
}

View File

@ -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);

View File

@ -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

View File

@ -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/<category>), else legacy ?=<category>.
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 metadata -->
<div class="task-meta">
<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 class="meta-item">
<strong>Type:</strong> ${task.type || 'unknown'}
</div>
<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 class="meta-item">
<strong>Created:</strong> ${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 ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : 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

View File

@ -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');

View File

@ -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/<category> — MUST precede /app* (/apps startsWith /app)
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());
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/<category>), else legacy ?=<cat> / ?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/<name> 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('?', '');

View File

@ -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';
}
}

View File

@ -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')) {