// Peças comuns do console de gestão. const DSc = window.WiFi_eeb21e; const { Button: BtnC, Badge: BadgeC, Skeleton: SkeletonC } = DSc; // Caixa de estado de carregamento: Skeleton enquanto carrega, mensagem + "Tentar de novo" no // erro (com o motivo quando o servidor manda um), senão renderiza o conteúdo. function CargaBox({ carregando, erro, onTentar, linhas, children }) { if (carregando) { return
{Array.from({ length: linhas || 3 }).map((_, i) => )}
; } if (erro) { const motivo = (erro && erro.corpo && erro.corpo.detail) || (erro && erro.status === 401 ? 'sessão expirada — recarregue a página' : (erro && erro.status ? 'erro ' + erro.status : 'falha de rede')); return
Não foi possível carregar: {motivo}. {onTentar ?
Tentar de novo
: null}
; } return children; } const GESTAO_ICONES = { grid: '', lista: '', escudo: '', wifi: '', tela: '', mail: '', cpu: '', atividade: '' }; function Icone({ nome, size = 16 }) { return ; } const GESTAO_DOT = { online: '#22c55e', ativo: '#22c55e', offline: 'hsl(var(--destructive))', aviso: 'hsl(var(--ring))', dormente: 'hsl(var(--muted-foreground))' }; function Dot({ s }) { return {s} ; } function GTh({ children, right }) { return {children}; } function GTd({ children, mono, right, wrap }) { return {children}; } function ChipFiltro({ ativo, onClick, children }) { return ; } function BtnMini({ children, tom, ...p }) { const cor = tom === 'perigo' ? 'hsl(var(--destructive))' : 'var(--brand-orange-text)'; return ; } function TermoDica({ dica, children }) { return {children}; } function SelectG(p) { return ; } // Painel lateral: comando gerado para revisão — a interface NÃO executa escrita. function CmdPainel({ cmd, onClose }) { const [copiado, setCopiado] = React.useState(false); const [exec, setExec] = React.useState({ estado: 'idle' }); // idle | executando | ok | erro React.useEffect(() => { setCopiado(false); setExec({ estado: 'idle' }); }, [cmd]); if (!cmd) return null; const texto = cmd.linhas.join('\n'); const copiar = () => { try { navigator.clipboard.writeText(texto).then(() => setCopiado(true)); } catch (e) { setCopiado(false); } }; // Executa só quando o ponto está no piloto (GESTAO_ESCRITA) e o comando declara uma ação. const podeExecutar = !!(cmd.slug && window.GESTAO_ESCRITA[cmd.slug] && cmd.acao); const executar = async () => { setExec({ estado: 'executando' }); try { const API = window.GestaoAPI; let r; if (cmd.acao === 'binding-add') r = await API.bindingAdd(cmd.slug, cmd.mac, cmd.comment); else if (cmd.acao === 'binding-del') r = await API.bindingDel(cmd.slug, cmd.mac); else if (cmd.acao === 'ssid-add') r = await API.ssidAdd(cmd.slug, { nome: cmd.nome, banda: cmd.banda, seg: cmd.seg }); else if (cmd.acao === 'ssid-bridge') r = await API.ssidBridge(cmd.slug, { cfg: cmd.cfg, bridge: cmd.bridge }); else throw new Error('ação desconhecida'); setExec({ estado: 'ok', saida: (r && (r.saida || r.nota)) || 'Executado.' }); if (cmd.onSucesso) cmd.onSucesso(); } catch (e) { const msg = (e && e.corpo && e.corpo.detail) || (e && e.status ? 'erro ' + e.status : 'falha de rede'); setExec({ estado: 'erro', saida: msg }); } }; return
e.stopPropagation()} style={{ width: 480, maxWidth: '92vw', height: '100%', background: 'hsl(var(--card))', borderLeft: '1px solid hsl(var(--card-border))', boxShadow: 'var(--shadow-lg)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box' }}>
{cmd.titulo}
{podeExecutar ? `Alvo: ${cmd.alvo} · executa no roteador (piloto MF)` : `Alvo: ${cmd.alvo} · comando para o seu terminal`}
{cmd.linhas.map((l, i) =>
{l}
)} {cmd.nota ?
{cmd.nota}
: null} {exec.estado === 'ok' ?
✓ {exec.saida}
: null} {exec.estado === 'erro' ?
Não executado: {exec.saida}
: null}
{podeExecutar ? {exec.estado === 'executando' ? 'Executando…' : (exec.estado === 'ok' ? 'Feito ✓' : 'Executar no roteador')} : null} {copiado ? 'Copiado ✓' : 'Copiar comandos'} Fechar {!podeExecutar ?
Fora do piloto MF: execute no seu terminal.
: null}
; } function mkLinha(alvo, comando) { return `app/.venv/bin/python ops/mk.py ${alvo} cmd '${comando}'`; } function slugSsid(t) { return (t || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); } // Menu de seleção de pontos — um ou vários clientes ao mesmo tempo. function PontoMenu({ sel, setSel }) { const sites = window.GESTAO_META; const chaves = Object.keys(sites); const [aberto, setAberto] = React.useState(false); const todos = sel.length === chaves.length; const rotulo = todos ? 'Todos os pontos' : sel.length === 1 ? sites[sel[0]].label : `${sel.length} pontos`; const alterna = k => setSel(s => s.includes(k) ? (s.length > 1 ? s.filter(x => x !== k) : s) : [...s, k]); return
{aberto ?
setAberto(false)} style={{ position: 'fixed', inset: 0, zIndex: 40 }} />
{chaves.map(k => )}
: null}
; } Object.assign(window, { Icone, Dot, GTh, GTd, ChipFiltro, BtnMini, TermoDica, SelectG, CmdPainel, PontoMenu, mkLinha, slugSsid, CargaBox });