// Clean SPA Router - Unified routing system for LibrePortal class LibrePortalSPAClean { constructor() { this.routes = new Map(); this.currentRoute = null; this.isLoading = false; this.dataLoaded = false; this.apps = []; this.categories = []; this.init(); } async init() { //console.log('🚀 Clean SPA: Initializing...'); // Setup routes from the feature manifest (falls back to the built-in table) await this.setupRoutesFromManifest(); // Wait for DOM to be ready if (document.readyState === 'loading') { await new Promise(resolve => { document.addEventListener('DOMContentLoaded', resolve); }); } // Wait for topbar to load first await this.waitForTopbar(); // Load data first await this.loadCoreData(); // Handle initial route this.handleInitialRoute(); //console.log('✅ Clean SPA: Initialization complete'); } async waitForTopbar() { //console.log('⏳ Waiting for topbar to load...'); // Wait for topbar component to be available and loaded let attempts = 0; const maxAttempts = 20; // 2 seconds max (reduced from 5 seconds) while (attempts < maxAttempts) { if (typeof TopbarComponent !== 'undefined' && TopbarComponent.loadTopbar) { try { await TopbarComponent.loadTopbar(); //console.log('✅ Topbar loaded successfully'); return; } catch (error) { console.warn('⚠️ Topbar loading failed, retrying...', error); } } await new Promise(resolve => setTimeout(resolve, 100)); attempts++; } console.warn('⚠️ Topbar failed to load after 2 seconds, continuing without topbar'); // Don't block the entire app if topbar fails } setupRoutes() { // Clean route definitions with explicit handlers this.routes.set('/', () => this.handleDashboard()); this.routes.set('/dashboard', () => this.handleDashboard()); this.routes.set('/apps', () => this.handleApps()); 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 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('/peers', () => this.handlePeers()); // legacy → /admin/tools/peers this.routes.set('/peers*', () => this.handlePeers()); 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())); } // Build the route table from the feature manifest (window.LP.features) so // "what pages exist" lives in one declarative place (features/manifest.dev.json) // instead of being hardcoded here. Route insertion order is preserved from the // manifest, which keeps the wildcard precedence findRouteHandler() relies on // (e.g. '/apps*' must be inserted before '/app*'). Falls back to the built-in // setupRoutes() table if the manifest is missing, empty, or names a handler // this class doesn't define — routing must never be left half-wired. async setupRoutesFromManifest() { try { const manifest = (window.LP && window.LP.features) ? await window.LP.features.loadManifest() : null; const entries = manifest && manifest.features; if (!entries || !entries.length) { this.setupRoutes(); return; } // Load each feature's self-registering module (if declared) BEFORE building // routes, so LP.features.get(id) sees the registered mount(). This is what // lets index.html drop its per-feature