// Main app shell with view routing, tweaks, cmdk, drawer const { useState: useS, useEffect: useE, useCallback: useC, useRef: useR } = React; // ── Auth helpers ───────────────────────────────────────────────────────────── const _TOKEN_KEY = 'rc_token'; function getToken() { return localStorage.getItem(_TOKEN_KEY); } function saveToken(t) { t ? localStorage.setItem(_TOKEN_KEY, t) : localStorage.removeItem(_TOKEN_KEY); } window.rcFetch = async function(url, opts = {}) { const tok = getToken(); const headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) }; if (tok) headers['Authorization'] = 'Bearer ' + tok; const r = await fetch(url, { ...opts, headers }); if (r.status === 401) { saveToken(null); window.location.reload(); } return r; }; function useDocAttrs(theme, density) { useE(() => { document.body.setAttribute('data-theme', theme); document.body.setAttribute('data-density', density); }, [theme, density]); } const TWEAK_DEFAULS = /*EDITMODE-BEGIN*/{ "theme": "dark", "density": "normal" }/*EDITMODE-END*/; function LoadingScreen({ error, onRetry }) { return (
{error ? ( <> Falha ao carregar dados: {error} ) : ( <>
Carregando dados… )}
); } function App() { const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULS); const [logged, setLogged] = useS(!!getToken()); const [rcLoaded, setRcLoaded] = useS(false); const [loadError, setLoadError] = useS(null); const _VALID_VIEWS = ['dashboard', 'clientes', 'times', 'servidores', 'sync', 'historico', 'criar', 'operacao']; const [view, setView] = useS(() => { const hash = window.location.hash.replace('#', ''); return _VALID_VIEWS.includes(hash) ? hash : 'dashboard'; }); const [openClient, setOpenClient] = useS(null); const [cmdkOpen, setCmdkOpen] = useS(false); const [confirm, setConfirm] = useS(null); const [toast, setToast] = useS(null); useDocAttrs(tweaks.theme, tweaks.density); const doLoad = useC(async () => { setLoadError(null); try { await window.RC.load(); setRcLoaded(true); } catch (err) { setLoadError(err?.message || String(err)); } }, []); // Load/reload RC data from API const reloadRC = useC(async () => { await window.RC.load(); setRcLoaded(true); setOpenClient(cur => cur ? (window.RC.clients.find(c => c.id === cur.id) || null) : cur); }, []); window.RC.reload = reloadRC; useE(() => { if (!logged) { setRcLoaded(false); setLoadError(null); return; } doLoad(); }, [logged]); const showToast = useC((title, body, kind = 'ok') => { setToast({ title, body, kind }); setTimeout(() => setToast(null), 3500); }, []); const go = useC((v, opts) => { setView(v); window.location.hash = v; setOpenClient(null); if (opts?.selectId) { const c = window.RC.clients.find(x => x.id === opts.selectId); if (c) setOpenClient(c); } }, []); // Back/forward browser navigation useE(() => { const onHash = () => { const hash = window.location.hash.replace('#', ''); if (_VALID_VIEWS.includes(hash)) setView(hash); }; window.addEventListener('hashchange', onHash); return () => window.removeEventListener('hashchange', onHash); }, []); // global keyboard shortcuts useE(() => { let lastG = 0; const onKey = (e) => { const tag = (e.target.tagName || '').toLowerCase(); const inField = tag === 'input' || tag === 'textarea' || e.target.isContentEditable; // cmdk if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setCmdkOpen(o => !o); return; } if (inField) return; // sequences starting with g if (e.key === 'g') { lastG = Date.now(); return; } if (Date.now() - lastG < 1500) { const map = { c: 'clientes', t: 'times', s: 'servidores', y: 'sync', d: 'dashboard', h: 'historico' }; if (map[e.key]) { e.preventDefault(); go(map[e.key]); lastG = 0; return; } } if (e.key === '/') { e.preventDefault(); // best effort: focus first input on screen const f = document.querySelector('main .input input'); if (f) f.focus(); } if (e.key === 'n') { e.preventDefault(); go('criar'); } if (e.key === '?') { setCmdkOpen(true); } }; window.addEventListener('keydown', onKey); const onCmdk = () => setCmdkOpen(true); window.addEventListener('rc-cmdk', onCmdk); return () => { window.removeEventListener('keydown', onKey); window.removeEventListener('rc-cmdk', onCmdk); }; }, [go]); // titles per view const titles = { dashboard: { title: 'Dashboard', crumbs: ['RodaiClientes', 'Dashboard'] }, clientes: { title: 'Clientes', crumbs: ['RodaiClientes', 'Clientes'] }, times: { title: 'Times', crumbs: ['RodaiClientes', 'Times'] }, servidores: { title: 'Servidores', crumbs: ['RodaiClientes', 'Servidores'] }, sync: { title: 'Sincronização', crumbs: ['RodaiClientes', 'Sincronização'] }, historico: { title: 'Histórico de operações', crumbs: ['RodaiClientes', 'Histórico'] }, criar: { title: 'Novo cliente', crumbs: ['RodaiClientes', 'Clientes', 'Novo'] }, operacao: { title: 'Operação op-2412', crumbs: ['RodaiClientes', 'Operações', 'op-2412'] }, }; const topbarActions = (() => { if (view === 'clientes') return null; if (view === 'dashboard') { return (
window.dispatchEvent(new CustomEvent('rc-recheck-all'))}>Verificar tudo go('criar')}>Novo cliente
); } if (view === 'servidores') { return ( window.dispatchEvent(new CustomEvent('rc-recheck-all'))}> Re-checar todos ); } if (view === 'historico') { return (
Exportar CSV
); } return null; })(); const handleLogin = useC((token) => { saveToken(token); setLogged(true); }, []); const handleLogout = useC(() => { saveToken(null); setLogged(false); }, []); if (!logged) return ; if (!rcLoaded) return ; const t1 = titles[view]; return (
{window.RC.syncDiff.length} divergente{window.RC.syncDiff.length !== 1 ? 's' : ''} ) : null} actions={topbarActions || (
)} /> go('operacao')} />
{view === 'dashboard' && } {view === 'clientes' && setOpenClient(c)} />} {view === 'times' && setOpenClient(c)} go={go} />} {view === 'servidores' && } {view === 'sync' && } {view === 'historico' && } {view === 'criar' && showToast('Cliente criado', `${name} propagado para 3/4 servidores`)} />} {view === 'operacao' && go('clientes')} />}
{openClient ? ( setOpenClient(null)} go={go} /> ) : null}
{/* Tweaks panel — self-manages open/close via host protocol */} {window.TweaksPanel ? ( setTweak('theme', v)} options={[ { value: 'dark', label: 'Dark' }, { value: 'light', label: 'Light' }, ]} /> setTweak('density', v)} options={[ { value: 'compact', label: 'Compacta' }, { value: 'normal', label: 'Normal' }, { value: 'relax', label: 'Confortável' }, ]} />
⌘KAbrir paleta de comandos /Focar busca NNovo cliente g cIr para Clientes g tIr para Times g sIr para Servidores g yIr para Sincronização g dIr para Dashboard g hIr para Histórico escFechar painel/drawer
Sair
) : null} setCmdkOpen(false)} gotoView={go} /> setConfirm(null)} /> setToast(null)} />
); } ReactDOM.createRoot(document.getElementById('root')).render();