// 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}
Tentar novamente
>
) : (
<>
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 || (
setCmdkOpen(true)} className="row" style={{
gap: 6, height: 28, padding: '0 8px',
background: 'var(--bg-2)', border: '1px solid var(--line)', borderRadius: 6,
fontSize: 12, color: 'var(--text-muted)', cursor: 'pointer',
}}>
Buscar… ⌘K
)}
/>
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' },
]}
/>
⌘K Abrir paleta de comandos
/ Focar busca
N Novo cliente
g c Ir para Clientes
g t Ir para Times
g s Ir para Servidores
g y Ir para Sincronização
g d Ir para Dashboard
g h Ir para Histórico
esc Fechar painel/drawer
Sair
) : null}
setCmdkOpen(false)} gotoView={go} />
setConfirm(null)} />
setToast(null)} />
);
}
ReactDOM.createRoot(document.getElementById('root')).render( );