/* App entry — full-bleed Calm dashboard with dashboard ↔ pipeline routing. */ const VALID_VIEWS = ['dashboard', 'pipeline', 'inbox', 'candidates', 'reqs', 'interviews', 'reports', 'settings']; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "role": "exec", "theme": "light" }/*EDITMODE-END*/; /* ── Live data loader ──────────────────────────────────────────────────────── * When /api/dashboard is reachable (running behind FastAPI) we replace the * hardcoded mock constants in app.jsx with real Lever data. * Falls back to mock data silently so local dev without the backend still works. */ function useLiveData() { const [status, setStatus] = React.useState('loading'); // 'loading' | 'live' | 'mock' React.useEffect(() => { const API = window.location.hostname === 'localhost' && window.location.port === '8787' ? null // pure local dev (python http.server) — skip API call : '/api/dashboard'; if (!API) { setStatus('mock'); return; } fetch(API) .then(r => { if (r.status === 401) { // Not authenticated — preserve the current path + hash through OAuth. const target = window.location.pathname + window.location.search + window.location.hash; window.location.href = `/auth/login?next=${encodeURIComponent(target || '/')}`; throw new Error('redirecting to login'); } if (!r.ok) throw new Error(r.status); return r.json(); }) .then(data => { // Overwrite the global constants that all components reference if (data.pipeline && data.pipeline.length) window.PIPELINE = data.pipeline; if (data.candidates && data.candidates.length) { window.CANDIDATES = data.candidates.map((c, i) => ({ ...c, // ensure fields the UI expects always exist id: c.id || `c${i}`, role: c.role || c.id, expYrs: c.expYrs || 0, salary: c.salary || 0, langs: c.langs || ['EN'], reasons: c.reasons || [], })); } if (data.reqs && data.reqs.length) { window.REQS = data.reqs.map((r, i) => ({ ...r, role: r.role || r.id || `r${i}`, })); } if (data.updates && data.updates.length) window.UPDATES = data.updates; if (data.kpis) window.KPIS = data.kpis; setStatus('live'); }) .catch(() => setStatus('mock')); }, []); return status; } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const dataStatus = useLiveData(); const [view, setView] = React.useState(() => { const h = window.location.hash.replace('#', '').split('/')[0]; return VALID_VIEWS.includes(h) ? h : 'dashboard'; }); React.useEffect(() => { document.documentElement.dataset.theme = t.theme; }, [t.theme]); // Sync view ↔ hash (preserves any `/id` suffix the page added for deep links). React.useEffect(() => { const currentView = window.location.hash.replace('#','').split('/')[0]; if (currentView !== view) window.location.hash = view; }, [view]); // Hashchange listener — supports back/forward + pasted links. React.useEffect(() => { const fn = () => { const h = window.location.hash.replace('#','').split('/')[0]; if (VALID_VIEWS.includes(h) && h !== view) setView(h); }; window.addEventListener('hashchange', fn); return () => window.removeEventListener('hashchange', fn); }, [view]); const setRole = React.useCallback((r) => setTweak('role', r), [setTweak]); const roleVal = React.useMemo(() => ({ role: t.role, setRole }), [t.role, setRole]); const handleNav = React.useCallback((id) => { if (VALID_VIEWS.includes(id)) setView(id); }, []); const page = (() => { switch (view) { case 'pipeline': return ; case 'inbox': return ; case 'candidates': return ; case 'reqs': return ; case 'interviews': return ; case 'reports': return ; case 'settings': return ; default: return ; } })(); return (
{page} {/* data-source badge — bottom-left, unobtrusive */}
{dataStatus === 'live' ? 'Live · Database' : dataStatus === 'loading' ? 'Connecting…' : 'Demo data'}
setTweak('role', v)} /> setTweak('theme', v)} />
); } ReactDOM.createRoot(document.getElementById('root')).render();