// Instancias HRC — panorama 4 servidores x 4 usuarios. // // Arquivo proprio em vez de mais 400 linhas em views.jsx (que ja passa de 4000). // O estado vem de /api/instancias-hrc, alimentado pelo scheduler de 60s. const { useState: useStateI, useEffect: useEffectI, useCallback: useCallbackI } = React; const RC_ESTADOS = { em_uso: { label: 'Em uso', cor: 'var(--ok)', soft: 'var(--ok-soft)' }, ocioso: { label: 'Ociosa', cor: 'var(--warn)', soft: 'var(--warn-soft)' }, parado: { label: 'Parada', cor: 'var(--err)', soft: 'var(--err-soft)' }, offline: { label: 'Offline', cor: 'var(--text-dim)', soft: 'transparent' }, }; const RC_ORDEM_ESTADOS = ['em_uso', 'ocioso', 'parado', 'offline']; // 2190 % de 1 core nao diz nada a ninguem; 21,9 cores diz. function fmtCores(cpuCorePct) { if (cpuCorePct === null || cpuCorePct === undefined) return '—'; const cores = cpuCorePct / 100; if (cores < 0.1) return '0 cores'; if (cores < 10) return `${cores.toFixed(1).replace('.', ',')} cores`; return `${Math.round(cores)} cores`; } function fmtDuracao(segundos) { if (segundos === null || segundos === undefined) return '—'; const s = Math.max(0, Math.floor(segundos)); if (s < 60) return `${s}s`; const min = Math.floor(s / 60); if (min < 60) return `${min}min`; const h = Math.floor(min / 60); if (h < 24) return `${h}h ${String(min % 60).padStart(2, '0')}min`; const d = Math.floor(h / 24); return `${d}d ${h % 24}h`; } function fmtRam(rssKb) { if (!rssKb) return '—'; const gb = rssKb / 1048576; if (gb >= 1) return `${gb.toFixed(1).replace('.', ',')} GB`; return `${Math.round(rssKb / 1024)} MB`; } function fmtMinutos(min) { if (min === null || min === undefined) return '—'; const h = Math.floor(min / 60); const m = Math.round(min % 60); if (h <= 0) return `${m}min`; return `${h}h ${String(m).padStart(2, '0')}min`; } // Cor determinística por id — mesmo operador sempre com a mesma cor, sem precisar // que o cadastro escolha uma manualmente. function corPorId(id) { const hue = (Number(id) * 47) % 360; return `hsl(${hue}, 62%, 45%)`; } function iniciaisOperador(nome) { const partes = (nome || '').trim().split(/\s+/).filter(Boolean); if (!partes.length) return '?'; if (partes.length === 1) return partes[0].slice(0, 2).toUpperCase(); return (partes[0][0] + partes[partes.length - 1][0]).toUpperCase(); } function ChipOperador({ operador, size = 20 }) { if (!operador) { return ( ); } const cor = operador.cor || corPorId(operador.id); const label = operador.apelido || operador.nome; return ( {iniciaisOperador(label)} ); } // Faixa em destaque no card: quem esta ocupando a maquina hoje sem ser o titular. // Fica na sobra ao lado do nome, onde antes nao havia nada. function BadgeTemp({ operador, size = 'sm' }) { if (!operador) return null; const label = operador.apelido || operador.nome; const compacto = size === 'sm'; return ( TEMP · {label} ); } function ResumoTile({ estadoId, valor, total }) { const meta = RC_ESTADOS[estadoId]; const pct = total > 0 ? Math.round((valor / total) * 100) : 0; return (
{meta.label}
{valor} de {total} · {pct}%
); } function Celula({ inst, ativa, onClick }) { const meta = RC_ESTADOS[inst.estado] || RC_ESTADOS.offline; const emUso = inst.estado === 'em_uso'; // Durante a histerese o estado ainda nao mudou, mas a CPU ja mudou. Sem sinalizar // isso a celula fica contraditoria ("24 cores" + "OCIOSA"). const pendente = inst.pendente_estado && inst.pendente_estado !== inst.estado ? RC_ESTADOS[inst.pendente_estado] : null; const temp = inst.operador_temporario || null; return ( ); } function BarrasHistorico({ amostras }) { if (!amostras || !amostras.length) { return
Sem amostras no periodo.
; } const largura = 100 / amostras.length; // Marcadores de horario ao longo do eixo, por indice (nao por relogio) — assim // continuam alinhados com as barras mesmo se houver buraco na coleta. const N_MARCADORES = Math.min(7, amostras.length); const indicesMarcadores = N_MARCADORES > 1 ? Array.from({ length: N_MARCADORES }, (_, i) => Math.round(i * (amostras.length - 1) / (N_MARCADORES - 1))) : [0]; return (
{amostras.map((a, i) => { const meta = RC_ESTADOS[a.estado] || RC_ESTADOS.offline; // altura proporcional aos cores usados, com piso visivel para o estado const cores = (a.cpu_core_pct || 0) / 100; const alt = Math.max(6, Math.min(100, (cores / 32) * 100)); return (
); })}
{indicesMarcadores.map((idx, i) => ( {i === 0 ? new Date(amostras[idx].coletado_em).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : new Date(amostras[idx].coletado_em).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} ))}
{amostras.length} amostras
); } function LinhaOcupacao({ linha }) { const pct = linha.pct_em_uso === null || linha.pct_em_uso === undefined ? 0 : linha.pct_em_uso; const dia = new Date(`${linha.dia}T12:00:00`).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' }); return (
{dia}
{pct}% {fmtMinutos(linha.min_em_uso)}
); } const NOVO_OPERADOR = '__novo__'; // Uma secao por faixa do slot: o titular e o analista temporario. Mesma mecanica, // rotulos e destaque diferentes — o temporario e o que explica uma maquina "de // alguem" aparecer ocupada por outra pessoa hoje. function SecaoOperador({ inst, podeAlocar, operadores, onChanged, onOperadorCriado, temporario = false }) { const operador = (temporario ? inst.operador_temporario : inst.operador) || null; const rotulos = temporario ? { titulo: 'Analista temporario', vazio: 'Nenhum analista temporario', alocar: 'Alocar temporario', trocar: 'Trocar temporario', liberar: 'Encerrar', novo: 'Nome do analista temporario' } : { titulo: 'Operador titular', vazio: 'Sem operador alocado', alocar: 'Alocar operador', trocar: 'Trocar', liberar: 'Liberar', novo: 'Nome do novo operador' }; const [trocando, setTrocando] = useStateI(false); const [escolhido, setEscolhido] = useStateI(''); const [novoNome, setNovoNome] = useStateI(''); const [salvando, setSalvando] = useStateI(false); const [erro, setErro] = useStateI(''); useEffectI(() => { setTrocando(false); setErro(''); setNovoNome(''); }, [inst.servidor_id, inst.usuario]); const alocarOperadorId = async (operadorId) => { const r = await window.rcFetch('/api/operadores/alocar', { method: 'POST', body: JSON.stringify({ servidor_id: inst.servidor_id, usuario: inst.usuario, operador_id: operadorId, temporario }), }); if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.detail || 'Falha ao alocar operador'); } }; const confirmarTroca = async () => { if (!escolhido) return; setSalvando(true); setErro(''); try { if (escolhido === NOVO_OPERADOR) { if (!novoNome.trim()) { setErro('Informe o nome do operador'); return; } const rc = await window.rcFetch('/api/operadores', { method: 'POST', body: JSON.stringify({ nome: novoNome.trim() }), }); const criado = await rc.json().catch(() => ({})); if (!rc.ok) { setErro(criado.detail || 'Falha ao criar operador'); return; } await alocarOperadorId(criado.id); await onOperadorCriado?.(); } else { await alocarOperadorId(Number(escolhido)); } setTrocando(false); setNovoNome(''); await onChanged(); } catch (e) { setErro(e?.message || 'Erro de conexao'); } finally { setSalvando(false); } }; const liberar = async () => { setSalvando(true); setErro(''); try { const r = await window.rcFetch('/api/operadores/liberar', { method: 'POST', body: JSON.stringify({ servidor_id: inst.servidor_id, usuario: inst.usuario, temporario }), }); if (!r.ok) { const d = await r.json().catch(() => ({})); setErro(d.detail || 'Falha ao liberar slot'); return; } await onChanged(); } catch (_) { setErro('Erro de conexao'); } finally { setSalvando(false); } }; return (
{rotulos.titulo}
{operador ? operador.nome : rotulos.vazio} {operador ? ( desde {new Date(operador.desde).toLocaleString('pt-BR')} ) : null}
{podeAlocar && !trocando ? (
{ setEscolhido(operador ? String(operador.id) : ''); setTrocando(true); }}> {operador ? rotulos.trocar : rotulos.alocar} {operador ? {rotulos.liberar} : null}
) : null}
{trocando ? (
{escolhido !== NOVO_OPERADOR ? ( <> {salvando ? 'Salvando…' : 'Confirmar'} setTrocando(false)} disabled={salvando}>Cancelar ) : null}
{escolhido === NOVO_OPERADOR ? (
{salvando ? 'Criando…' : 'Criar e alocar'} setTrocando(false)} disabled={salvando}>Cancelar
) : null}
) : null} {erro ?
{erro}
: null}
); } // Cor por modalidade extraida da legenda do .hrcz (ver hrcz_legend_parser.py). // Puramente cosmetico — sem "MODALIDADE" nenhuma aqui influencia dado de negocio. const RC_MODALIDADE_COR = { PKO: 'var(--accent)', MYSTERY: 'var(--warn)', VANILLA: 'var(--text-dim)', SATELITE: 'var(--ok)', }; function BadgeSpot({ spot, destaque }) { if (!spot) return null; const cor = RC_MODALIDADE_COR[spot.modalidade_legenda] || 'var(--text-dim)'; const detalheEstagio = spot.estagio && spot.estagio !== 'UNKNOWN' ? `${spot.estagio}${spot.jogadores_restantes ? ` · ${spot.jogadores_restantes} left` : ''}` : null; return (
{destaque ? ▶ : null} {/* Nome real do arquivo .hrcz -- trunca com "…" quando nao cabe, o titulo do bloco (tooltip do navegador) mostra o nome inteiro. */} {spot.legenda} {destaque ? ( rodando agora ) : null}
{spot.modalidade_legenda || '—'} {detalheEstagio ? ( {detalheEstagio} ) : null}
); } function SecaoSpots({ inst }) { const abas = inst.abas_abertas || []; if (!abas.length) return null; const rodandoLegenda = inst.spot_rodando ? inst.spot_rodando.legenda : null; // O spot rodando agora sempre primeiro; os demais mantem a ordem em que o // HRC tem as abas abertas. const ordenadas = [...abas].sort((a, b) => { if (a.legenda === rodandoLegenda) return -1; if (b.legenda === rodandoLegenda) return 1; return 0; }); return (
Spots abertos ({abas.length})
{ordenadas.map((spot) => ( ))}
); } function PainelDetalhe({ inst, onClose, podeAlocar, operadores, onOperadorChanged, onOperadorCriado }) { const [historico, setHistorico] = useStateI(null); const [ocupacao, setOcupacao] = useStateI(null); const [erro, setErro] = useStateI(''); useEffectI(() => { let vivo = true; setHistorico(null); setOcupacao(null); setErro(''); (async () => { try { const qs = `servidor_id=${inst.servidor_id}&usuario=${encodeURIComponent(inst.usuario)}`; const [h, o] = await Promise.all([ window.rcFetch(`/api/instancias-hrc/historico?${qs}&horas=24`), window.rcFetch(`/api/instancias-hrc/ocupacao?dias=7&servidor_id=${inst.servidor_id}`), ]); if (!h.ok || !o.ok) throw new Error('Falha ao carregar detalhe'); const [hj, oj] = await Promise.all([h.json(), o.json()]); if (!vivo) return; setHistorico(hj); setOcupacao(oj.filter(l => l.usuario === inst.usuario)); } catch (e) { if (vivo) setErro(e?.message || String(e)); } })(); return () => { vivo = false; }; }, [inst.servidor_id, inst.usuario]); const meta = RC_ESTADOS[inst.estado] || RC_ESTADOS.offline; return (
{inst.servidor} · {inst.usuario} {meta.label} ha {fmtDuracao(inst.segundos_no_estado)}
Fechar
CPU agora{fmtCores(inst.cpu_core_pct)}
% do servidor{inst.cpu_host_pct === null ? '—' : `${inst.cpu_host_pct}%`}
RAM{fmtRam(inst.rss_kb)}
Processos{inst.procs}
HRC de pe ha{fmtDuracao(inst.proc_uptime_s)}
NoMachine{inst.nx_sessao ? 'sessao ativa' : 'sem sessao'}
{erro ?
{erro}
: null}
Ultimas 24 horas
{historico === null && !erro ?
Carregando…
: }
Ocupacao dos ultimos 7 dias
{ocupacao === null && !erro ? (
Carregando…
) : ocupacao && ocupacao.length ? ( ocupacao.map(l => ) ) : (
Ainda sem historico diario consolidado.
)}
); } function InstanciasView({ readOnly, podeAlocar }) { const [dados, setDados] = useStateI(null); const [erro, setErro] = useStateI(''); const [selecionada, setSelecionada] = useStateI(null); const [forcando, setForcando] = useStateI(false); const [operadores, setOperadores] = useStateI([]); const carregar = useCallbackI(async () => { try { const r = await window.rcFetch('/api/instancias-hrc'); if (!r.ok) throw new Error('Falha ao carregar instancias'); setDados(await r.json()); setErro(''); } catch (e) { setErro(e?.message || String(e)); } }, []); useEffectI(() => { carregar(); const t = setInterval(carregar, 60000); // alinhado ao scheduler return () => clearInterval(t); }, [carregar]); const carregarOperadores = useCallbackI(async () => { if (!podeAlocar) return; try { const r = await window.rcFetch('/api/operadores'); if (r.ok) setOperadores(await r.json()); } catch (_) {} }, [podeAlocar]); useEffectI(() => { carregarOperadores(); }, [carregarOperadores]); const forcar = async () => { setForcando(true); try { await window.rcFetch('/api/instancias-hrc/verificar-agora', { method: 'POST' }); await carregar(); } finally { setForcando(false); } }; if (erro && !dados) { return
{erro}
; } if (!dados) { return
Carregando instancias…
; } const porServidor = {}; dados.instancias.forEach(i => { (porServidor[i.servidor] = porServidor[i.servidor] || []).push(i); }); const servidores = Object.keys(porServidor); const usuarios = dados.instancias.length ? [...new Set(dados.instancias.map(i => i.usuario))] : []; const temporarios = dados.instancias.filter(i => i.operador_temporario); return (
HRC em Uso {dados.total} instancias · em uso = HRC consumindo {'≥'} {fmtCores(dados.limiar_cpu_core_pct)} · atualiza a cada 60s {temporarios.length ? (
Analistas temporarios agora: {temporarios.map(i => ( {i.servidor} · {i.usuario} ))}
) : null}
{!readOnly ? ( {forcando ? 'Verificando…' : 'Verificar agora'} ) : null}
{RC_ORDEM_ESTADOS.map(id => ( ))}
{usuarios.map(u =>
{u}
)} {servidores.map(nome => (
{nome} {porServidor[nome][0]?.host || ''}
{porServidor[nome].map(inst => ( setSelecionada( selecionada && selecionada.servidor_id === inst.servidor_id && selecionada.usuario === inst.usuario ? null : inst )} /> ))}
))}
{selecionada ? ( i.servidor_id === selecionada.servidor_id && i.usuario === selecionada.usuario ) || selecionada} onClose={() => setSelecionada(null)} podeAlocar={podeAlocar} operadores={operadores} onOperadorChanged={carregar} onOperadorCriado={carregarOperadores} /> ) : null}
); } // ── Resumo para o Dashboard ──────────────────────────────────────────────── // Mini-matriz 4x4: uma olhada responde "quantas das 16 estao trabalhando agora // e quais estao paradas", sem sair da tela inicial. function InstanciasResumoCard({ go }) { const [dados, setDados] = useStateI(null); const [erro, setErro] = useStateI(''); useEffectI(() => { let vivo = true; const carregar = async () => { try { const r = await window.rcFetch('/api/instancias-hrc'); if (!r.ok) throw new Error('indisponivel'); const j = await r.json(); if (vivo) { setDados(j); setErro(''); } } catch (e) { if (vivo) setErro('indisponivel'); } }; carregar(); const t = setInterval(carregar, 60000); return () => { vivo = false; clearInterval(t); }; }, []); const resumo = dados?.resumo || {}; const total = dados?.total || 0; const emUso = resumo.em_uso || 0; const ociosas = resumo.ocioso || 0; const pct = total > 0 ? Math.round((emUso / total) * 100) : 0; const porServidor = {}; (dados?.instancias || []).forEach(i => { (porServidor[i.servidor] = porServidor[i.servidor] || []).push(i); }); const temporarios = (dados?.instancias || []).filter(i => i.operador_temporario); // cores sendo usados agora na frota inteira const coresTotal = (dados?.instancias || []) .reduce((acc, i) => acc + (i.cpu_core_pct || 0) / 100, 0); return (
HRC em Uso
{total ? `${total} instâncias · ${Math.round(coresTotal)} cores em uso agora` : 'carregando…'}
go('instancias')}>Ver matriz
{erro && !dados ? (
Dados de instâncias indisponíveis.
) : (
Em uso agora
{dados ? `${emUso}/${total}` : '—'} {dados ? `${pct}%` : ''}
{dados ? (ociosas ? `${ociosas} ociosa${ociosas > 1 ? 's' : ''}` : 'nenhuma ociosa') : ''}
{temporarios.length ? (
{temporarios.length} temporario{temporarios.length > 1 ? 's' : ''}: {temporarios.map(i => (i.operador_temporario.apelido || i.operador_temporario.nome)).join(', ')}
) : null}
{Object.keys(porServidor).map(nome => (
{nome}
{porServidor[nome].map(i => { const meta = RC_ESTADOS[i.estado] || RC_ESTADOS.offline; return ( {i.usuario.replace('user', '')} ); })}
))}
)}
); } Object.assign(window, { InstanciasView, InstanciasResumoCard });