/* ── App bootstrap & routing ── */ const PAGES = { tracker: { container: 'page-tracker', init: (el) => TrackerPage.init(el) }, bills: { container: 'page-bills', init: (el) => BillsPage.init(el) }, categories: { container: 'page-categories', init: (el) => CategoriesPage.init(el) }, settings: { container: 'page-settings', init: (el) => SettingsPage.init(el) }, status: { container: 'page-status', init: (el) => StatusPage.init(el) }, }; let activePage = null; let currentUser = null; // ── Auth gate ────────────────────────────────────────────────────────────── async function boot() { let res; try { res = await fetch('/api/auth/me'); } catch { location.href = '/login.html'; return; } if (!res.ok) { location.href = '/login.html'; return; } const data = await res.json(); currentUser = data.user; if (currentUser.role === 'admin') { location.href = '/admin.html'; return; } document.getElementById('sidebar-username').textContent = currentUser.username; if (data.single_user_mode) { document.getElementById('sidebar-logout').style.display = 'none'; startApp(); return; } if (currentUser.must_change_password) { showChangePasswordOverlay(); return; } if (currentUser.first_login) { showPrivacyNotice(); return; } startApp(); } // ── Password change overlay ──────────────────────────────────────────────── function showChangePasswordOverlay() { document.getElementById('change-password-overlay').classList.remove('hidden'); document.getElementById('change-password-form').onsubmit = async (e) => { e.preventDefault(); const np = document.getElementById('cp-new').value; const cnf = document.getElementById('cp-confirm').value; const err = document.getElementById('cp-error'); if (np !== cnf) { err.textContent = 'Passwords do not match.'; return; } if (np.length < 8) { err.textContent = 'Password must be at least 8 characters.'; return; } err.textContent = ''; try { const res = await fetch('/api/auth/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_password: np }), }); const body = await res.json(); if (!res.ok) throw new Error(body.error); document.getElementById('change-password-overlay').classList.add('hidden'); currentUser.must_change_password = false; currentUser.first_login ? showPrivacyNotice() : startApp(); } catch (ex) { document.getElementById('cp-error').textContent = 'Error: ' + ex.message; } }; } // ── Privacy notice overlay ───────────────────────────────────────────────── function showPrivacyNotice() { document.getElementById('privacy-overlay').classList.remove('hidden'); document.getElementById('privacy-ack-btn').onclick = async () => { await fetch('/api/auth/acknowledge-privacy', { method: 'POST' }); document.getElementById('privacy-overlay').classList.add('hidden'); currentUser.first_login = false; startApp(); }; } // ── Main app ─────────────────────────────────────────────────────────────── function startApp() { document.getElementById('app').style.visibility = 'visible'; setupLogout(); setupNavLinks(); handleHash(); } function setupLogout() { document.getElementById('sidebar-logout').addEventListener('click', async (e) => { e.preventDefault(); await fetch('/api/auth/logout', { method: 'POST' }); location.href = '/login.html'; }); } function setupNavLinks() { document.querySelectorAll('.nav-link[data-page]').forEach(link => { link.addEventListener('click', () => { activePage = null; navigate(link.dataset.page); }); }); window.addEventListener('hashchange', () => { activePage = null; handleHash(); }); } function handleHash() { navigate(location.hash.replace('#', '') || 'tracker'); } function navigate(page) { if (!PAGES[page]) page = 'tracker'; document.querySelectorAll('.nav-link[data-page]').forEach(a => { a.classList.toggle('active', a.dataset.page === page); }); document.querySelectorAll('.page').forEach(el => el.classList.remove('active')); document.getElementById(PAGES[page].container).classList.add('active'); if (activePage !== page) { activePage = page; PAGES[page].init(document.getElementById(PAGES[page].container)); } } // ── Toast system ─────────────────────────────────────────────────────────── const TOAST_ICONS = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ', }; let toastContainer = null; function getToastContainer() { if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'toast-container'; document.body.appendChild(toastContainer); } return toastContainer; } function showToast(msg, type = 'info', duration = 3500) { const container = getToastContainer(); const toast = document.createElement('div'); const icon = TOAST_ICONS[type] || TOAST_ICONS.info; toast.className = `toast toast-${type}`; toast.innerHTML = ` ${icon} ${msg}
`; container.appendChild(toast); // Trigger animation requestAnimationFrame(() => toast.classList.add('toast-show')); const dismiss = () => { toast.classList.remove('toast-show'); toast.classList.add('toast-hide'); toast.addEventListener('transitionend', () => toast.remove(), { once: true }); }; const timer = setTimeout(dismiss, duration); toast.querySelector('.toast-close').onclick = () => { clearTimeout(timer); dismiss(); }; } // Hide app shell until auth check completes (prevents flash) document.getElementById('app').style.visibility = 'hidden'; boot();