/** * NovaCPX — Shared JS utilities */ window.Nova = (() => { // ── Activity bar (thin top-of-page progress stripe for every API call) ──── let _barEl = null, _barPct = 0, _barTimer = null, _barActive = 0; function _barShow() { _barActive++; if (!_barEl) { _barEl = document.createElement('div'); _barEl.style.cssText = [ 'position:fixed;top:0;left:0;z-index:999999', 'height:3px;width:0%;background:var(--primary,#6366f1)', 'transition:width .2s ease,opacity .3s ease', 'box-shadow:0 0 8px var(--primary,#6366f1)', 'pointer-events:none', ].join(';'); document.body.appendChild(_barEl); } _barEl.style.opacity = '1'; _barPct = 10; _barEl.style.width = _barPct + '%'; clearInterval(_barTimer); _barTimer = setInterval(() => { if (_barPct < 85) { _barPct += (_barPct < 50 ? 8 : _barPct < 70 ? 4 : 1); _barEl.style.width = _barPct + '%'; } }, 200); } function _barDone() { _barActive = Math.max(0, _barActive - 1); if (_barActive > 0) return; clearInterval(_barTimer); if (_barEl) { _barEl.style.width = '100%'; setTimeout(() => { if (_barEl) { _barEl.style.opacity = '0'; setTimeout(() => { _barEl?.remove(); _barEl = null; }, 300); } }, 200); } } // ── API ─────────────────────────────────────────────────────────────────── async function api(endpoint, action, opts = {}) { const { method = 'GET', body, params } = opts; let url = `/api/${endpoint}/${action}`; if (params) url += '?' + new URLSearchParams(params); _barShow(); let res; try { res = await fetch(url, { method, credentials: 'include', headers: body ? { 'Content-Type': 'application/json' } : {}, body: body ? JSON.stringify(body) : undefined, }); } catch (e) { _barDone(); console.error(`Nova.api network error [${endpoint}/${action}]:`, e); return { success: false, message: 'Network error — check your connection' }; } _barDone(); if (res.status === 401) { return { success: false, message: 'Session expired — please log in again' }; } if (res.status === 429) { const reset = res.headers.get('X-RateLimit-Reset'); const wait = reset ? Math.max(0, Math.ceil(Number(reset) - Date.now() / 1000)) : 60; return { success: false, message: `Rate limited — try again in ${wait}s` }; } const text = await res.text(); try { return JSON.parse(text); } catch { console.error(`Nova.api non-JSON from [${endpoint}/${action}] (HTTP ${res.status}):`, text.slice(0, 500)); return { success: false, message: `Server error (HTTP ${res.status}) — see browser console` }; } } // ── Toast ───────────────────────────────────────────────────────────────── let toastEl = null; function toast(msg, type = 'info', duration = 3500) { if (!toastEl) { toastEl = document.createElement('div'); toastEl.style.cssText = 'position:fixed;bottom:1.5rem;right:1.5rem;z-index:9999;display:flex;flex-direction:column;gap:.5rem;max-width:380px'; document.body.appendChild(toastEl); } const el = document.createElement('div'); el.className = `alert alert-${type}`; el.style.cssText = 'animation:fadeIn .2s;cursor:pointer;box-shadow:var(--shadow)'; el.textContent = msg; el.addEventListener('click', () => el.remove()); toastEl.appendChild(el); setTimeout(() => el.remove(), duration); } // ── Modal ───────────────────────────────────────────────────────────────── function modal(title, bodyHtml, footerHtml = '') { const ov = document.createElement('div'); ov.className = 'modal-overlay open'; ov.innerHTML = `
${msg}
`, ` ` ); ov.querySelector('#confirm-yes').onclick = () => { ov.remove(); onYes(); }; } // ── Sidebar navigation ──────────────────────────────────────────────────── function initNav(pages) { document.querySelectorAll('[data-page]').forEach(link => { link.addEventListener('click', e => { e.preventDefault(); const page = link.dataset.page; document.querySelectorAll('[data-page]').forEach(l => l.classList.remove('active')); link.classList.add('active'); const titleEl = document.getElementById('page-title'); if (titleEl) titleEl.textContent = link.textContent.trim(); loadPage(page, pages); }); }); } function loadPage(page, pages) { const content = document.getElementById('page-content'); if (!content) return; const fn = pages[page]; if (fn) { content.innerHTML = 'Page "${page}" coming soon.