// Reusable RodaiClientes components
const { useState, useEffect, useRef, useMemo, useCallback } = React;
function Btn({ children, kind = 'ghost', size = 'md', icon, iconRight, onClick, disabled, title, type = 'button' }) {
const cls = ['btn', `btn-${kind}`, size === 'sm' ? 'btn-sm' : ''].join(' ');
return (
{icon ? : null}
{children}
{iconRight ? : null}
);
}
function IconBtn({ name, onClick, title, size = 'md', kind = 'ghost' }) {
const cls = ['btn', `btn-${kind}`, 'btn-icon', size === 'sm' ? 'btn-sm' : ''].join(' ');
return (
);
}
function Chip({ children, kind = 'default', icon }) {
const cls = ['chip', kind !== 'default' ? `chip-${kind}` : ''].join(' ');
return (
{icon ? : null}
{children}
);
}
function StatusDot({ kind = 'mut', pulse = false }) {
return ;
}
// Mini bars showing 4 servers' sync status for a client
function MiniBars({ status }) {
// status is array of 0=ok 1=syncing 2=diverged 3=missing
const cls = (s) => {
if (s === 0) return 's-ok';
if (s === 1) return 's-syncing';
if (s === 2) return 's-diverged';
if (s === 3) return 's-missing';
return 's-unknown';
};
return (
{status.map((s, i) => )}
);
}
function Input({ value, onChange, placeholder, icon, kbd, autoFocus, onKeyDown, type='text', style={}, maxLength }) {
return (
{icon ? : null}
onChange?.(e.target.value)}
placeholder={placeholder}
autoFocus={autoFocus}
onKeyDown={onKeyDown}
maxLength={maxLength}
/>
{kbd ? {kbd} : null}
);
}
function Segmented({ value, onChange, options }) {
return (
{options.map(o => (
onChange(o.value)}
role="tab"
aria-selected={value === o.value}
>
{o.label}
))}
);
}
function Sidebar({ view, setView, opsBadge }) {
const items = [
{ id: 'dashboard', label: 'Dashboard', icon: 'dashboard', kbd: 'g d' },
{ id: 'clientes', label: 'Clientes', icon: 'users', kbd: 'g c' },
{ id: 'times', label: 'Times', icon: 'tree', kbd: 'g t' },
{ id: 'servidores',label: 'Servidores',icon: 'server', kbd: 'g s' },
{ id: 'sync', label: 'Sincronização', icon: 'refresh', kbd: 'g y' },
{ id: 'historico', label: 'Histórico', icon: 'history', kbd: 'g h' },
];
return (
{/* brand */}
RodaiClientes
RodaiLAB · v0.5.0
window.dispatchEvent(new CustomEvent('rc-cmdk'))}
style={{
width: '100%', height: 28, padding: '0 8px',
background: 'var(--bg-2)', color: 'var(--text-muted)',
border: '1px solid var(--line)', borderRadius: 6,
display: 'flex', alignItems: 'center', gap: 8,
fontSize: 12, cursor: 'pointer'
}}
>
Pular para…
⌘K
{items.map(it => (
setView(it.id)}
>
{it.label}
{it.id === 'sync' ? : null}
{it.id === 'historico' && opsBadge ? {opsBadge} : null}
))}
{window.RC?.clients && (
Acesso rápido
{(() => {
const active = window.RC.clients.filter(c => !c.archived);
const divergentes = window.RC.syncDiff.length;
const emTransicao = active.filter(c => c.running).length;
const sincronizados = active.filter(c => c.status && c.status.every(s => s === 0)).length;
return (
<>
setView('sync')} style={{ height: 24 }}>
Divergentes
{divergentes > 0 && {divergentes} }
setView('clientes')} style={{ height: 24 }}>
Em transição
{emTransicao > 0 && {emTransicao} }
setView('clientes')} style={{ height: 24 }}>
Sincronizados
{sincronizados}
>
);
})()}
)}
{/* footer */}
window.postMessage({ type: '__activate_edit_mode' }, '*')}
title="Configurações e sessão"
>
AD
admin
operador principal
);
}
function Topbar({ title, crumbs, actions, info }) {
return (
{crumbs ? (
{crumbs.map((c, i) => (
{i > 0 ? / : null}
{i === crumbs.length - 1
? {c}
: {c} }
))}
) : null}
{title}
{info}
{actions}
);
}
// Live operation banner for top of any screen when an op is running
function LiveOpBanner({ op, onOpen }) {
if (!op) return null;
const total = op.servers.length;
const done = op.servers.filter(s => s.state === 'done').length;
const failed = op.servers.filter(s => s.state === 'fail').length;
const running = op.servers.filter(s => s.state === 'running').length;
return (
{op.kind} {' '}
· {' '}
{op.target} {' '}
em andamento — {done}/{total} servidores prontos · {running} rodando · {failed > 0 ? {failed} falhou : '0 falhou'}
Ver progresso
);
}
// Floating toast manager
function Toast({ toast, onClose }) {
if (!toast) return null;
return (
{toast.title}
{toast.body ? {toast.body} : null}
);
}
function ConfirmModal({ open, title, body, danger, confirmLabel = 'Confirmar', onConfirm, onCancel, children }) {
useEffect(() => {
if (!open) return;
const onKey = (e) => { if (e.key === 'Escape') onCancel?.(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onCancel]);
if (!open) return null;
return (
e.stopPropagation()}>
{danger ? : null}
{title}
{body ?
{body}
: null}
{children ?
{children}
: null}
Cancelar
{confirmLabel}
);
}
function CmdK({ open, onClose, gotoView }) {
const [q, setQ] = useState('');
const inputRef = useRef(null);
useEffect(() => {
if (open) {
setQ('');
setTimeout(() => inputRef.current?.focus(), 0);
}
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
if (open) window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]);
const items = useMemo(() => {
const navItems = [
{ id: 'go-dashboard', label: 'Ir para Dashboard', icon: 'dashboard', section: 'Navegação', run: () => gotoView('dashboard') },
{ id: 'go-clientes', label: 'Ir para Clientes', icon: 'users', section: 'Navegação', run: () => gotoView('clientes') },
{ id: 'go-times', label: 'Ir para Times', icon: 'tree', section: 'Navegação', run: () => gotoView('times') },
{ id: 'go-servidores',label: 'Ir para Servidores', icon: 'server', section: 'Navegação', run: () => gotoView('servidores') },
{ id: 'go-sync', label: 'Ir para Sincronização', icon: 'refresh', section: 'Navegação', run: () => gotoView('sync') },
];
const actions = [
{ id: 'a-new', label: 'Criar cliente novo', icon: 'plus', section: 'Ações', shortcut: 'N', run: () => gotoView('criar') },
{ id: 'a-sync', label: 'Rodar verificação completa', icon: 'refresh', section: 'Ações', run: () => gotoView('sync') },
{ id: 'a-op', label: 'Ver operação em andamento', icon: 'bolt', section: 'Ações', run: () => gotoView('operacao') },
];
const clientItems = window.RC.clients.slice(0, 12).map(c => ({
id: 'c-' + c.id,
label: c.name,
sub: (window.RC.teamById(c.team)?.name || String(c.team)).toUpperCase(),
icon: 'user',
section: 'Clientes',
run: () => gotoView('clientes', { selectId: c.id }),
}));
const all = [...navItems, ...actions, ...clientItems];
if (!q) return all;
const ql = q.toLowerCase();
return all.filter(it => it.label.toLowerCase().includes(ql) || (it.sub || '').toLowerCase().includes(ql));
}, [q, gotoView]);
const [active, setActive] = useState(0);
useEffect(() => { setActive(0); }, [q, open]);
if (!open) return null;
const onKey = (e) => {
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(a => Math.min(items.length - 1, a + 1)); }
if (e.key === 'ArrowUp') { e.preventDefault(); setActive(a => Math.max(0, a - 1)); }
if (e.key === 'Enter') { e.preventDefault(); items[active]?.run(); onClose(); }
};
// group by section
const groups = {};
items.forEach((it, i) => { (groups[it.section] = groups[it.section] || []).push({ ...it, _i: i }); });
return (
e.stopPropagation()}
>
setQ(e.target.value)}
onKeyDown={onKey}
placeholder="Buscar comandos, clientes, times…"
style={{ all: 'unset', flex: 1, fontSize: 14, color: 'var(--text)' }}
/>
esc
{Object.keys(groups).length === 0 ? (
Sem resultados.
) : Object.entries(groups).map(([sec, list]) => (
{sec}
{list.map(it => (
setActive(it._i)}
onClick={() => { it.run(); onClose(); }}
className="row"
style={{
padding: '6px 12px', gap: 10, cursor: 'pointer', height: 30,
background: active === it._i ? 'var(--bg-active)' : 'transparent',
}}
>
{it.label}
{it.sub ? {it.sub} : null}
{it.shortcut ? {it.shortcut} : null}
))}
))}
↑ ↓ navegar
↵ selecionar
esc fechar
);
}
Object.assign(window, { Btn, IconBtn, Chip, StatusDot, MiniBars, Input, Segmented, Sidebar, Topbar, LiveOpBanner, Toast, ConfirmModal, CmdK });