'use strict'; /* AdminRouter — hash-based router for admin panel. * Wraps the existing switchTab() flow without replacing it. * * Hash format: #[/[/...]] * #stats → { route: 'stats', params: [] } * #users → { route: 'users', params: [] } * #users/123 → { route: 'users', params: ['123'] } * #sessions/456/foo → { route: 'sessions', params: ['456','foo'] } * * Public API: window.AdminRouter * parse(hash) → route object * current() → route object for location.hash * navigate(hash, { replace, silent }) * on(event, fn) / off(event, fn) — 'change' event only * * Recursion guard: programmatic navigate() sets a flag so the hashchange * listener does not re-emit 'change' for our own writes. */ (function () { 'use strict'; const listeners = { change: new Set() }; let _navigating = false; function parse(hash) { const raw = String(hash || ''); const stripped = raw.charAt(0) === '#' ? raw.slice(1) : raw; const parts = stripped.split('/').filter(Boolean); const route = parts.length ? decodeURIComponent(parts[0]) : ''; const params = parts.slice(1).map(p => { try { return decodeURIComponent(p); } catch { return p; } }); return { route, params, raw: raw || '' }; } function current() { return parse(location.hash); } function normalizeHash(input) { const s = String(input || ''); if (!s) return ''; return s.charAt(0) === '#' ? s : '#' + s; } function emit(event, payload) { const set = listeners[event]; if (!set) return; set.forEach(fn => { try { fn(payload); } catch (e) { console.error('AdminRouter listener error', e); } }); } function navigate(routeOrHash, opts) { const options = opts || {}; const target = normalizeHash(routeOrHash); const currentHash = location.hash || ''; // Same hash → no-op (avoids spurious listener fires). if (target === currentHash) { if (!options.silent) emit('change', { ...parse(target), silent: false }); return; } _navigating = true; try { if (options.replace && typeof history !== 'undefined' && history.replaceState) { // Preserve current path/query, swap hash without adding history entry. const url = location.pathname + location.search + target; history.replaceState(history.state, '', url); } else { location.hash = target; } } finally { // hashchange fires async; clear flag after dispatch. setTimeout(() => { _navigating = false; }, 0); } if (!options.silent) { emit('change', { ...parse(target), silent: false }); } } function on(event, fn) { if (!listeners[event] || typeof fn !== 'function') return; listeners[event].add(fn); } function off(event, fn) { if (!listeners[event]) return; listeners[event].delete(fn); } window.addEventListener('hashchange', () => { if (_navigating) return; emit('change', { ...current(), silent: false }); }); window.AdminRouter = { parse, current, navigate, on, off }; })();