// 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 (
);
})}
{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}
);
}
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 (
HRC em Uso
{dados.total} instancias · em uso = HRC consumindo {'≥'} {fmtCores(dados.limiar_cpu_core_pct)} · atualiza a cada 60s
{temporarios.length ? (