/** * NovaCPX — Shared JS utilities */ window.Nova = (() => { // ── API ─────────────────────────────────────────────────────────────────── async function api(endpoint, action, opts = {}) { const { method = 'GET', body, params } = opts; let url = `/api/${endpoint}/${action}`; if (params) url += '?' + new URLSearchParams(params); let res; try { res = await fetch(url, { method, credentials: 'include', headers: body ? { 'Content-Type': 'application/json' } : {}, body: body ? JSON.stringify(body) : undefined, }); } catch (e) { console.error(`Nova.api network error [${endpoint}/${action}]:`, e); return { success: false, message: 'Network error — check your connection' }; } if (res.status === 401) { location.href = '/?redirect=' + encodeURIComponent(location.pathname); return null; } 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.