/* Administração de dispositivos de RV — ESP-0009 (RF-DISPOSITIVOS-01, 08, 12, 13, 17). Vive dentro do shell, como `view_users.jsx`. Esconder o item de menu não é proteção — `/api/devices` recusa quem não é administrador. As rotas públicas de registro (`/registrations/*`) são do app de RV; esta tela só faz o lado administrativo. O painel "Conectar dispositivo" gera, client-side, um QR-Code com a URL do Portal (`window.NAZIDE_CONFIG.API_BASE`) para o headset ler na tela de configuração. O QR carrega **só a URL**, nunca o segredo. */ const STATUS_DO_DISPOSITIVO = { pendente: { rotulo: "Pendente", tag: "accent" }, ativo: { rotulo: "Ativo", tag: "go" }, inativo: { rotulo: "Inativo", tag: "danger" }, }; const ERROS_DE_DISPOSITIVO = { codigo_de_pareamento_invalido: "Código de pareamento incorreto. Confira o que o app exibe.", install_id_ja_registrado: null, // a mensagem do backend já orienta status_invalido: null, credencial_ja_resgatada: "A credencial deste dispositivo já foi resgatada.", dispositivo_nao_encontrado: "Dispositivo não encontrado. Atualize a listagem.", pendentes_no_limite: null, }; function mensagemDeErroDeDispositivo(erro) { if (!erro) return ""; const especifica = ERROS_DE_DISPOSITIVO[erro.code]; if (especifica) return especifica; return erro.message || "Não foi possível concluir a solicitação."; } function formatarInstante(iso) { if (!iso) return "—"; try { return new Date(iso).toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }); } catch (e) { return iso; } } /* --- QR-Code client-side (biblioteca qrcode-generator via CDN) ------------- */ function QrCode({ texto, tamanho = 200 }) { const dataUrl = React.useMemo(() => { if (!texto || typeof window.qrcode !== "function") return null; try { const qr = window.qrcode(0, "M"); // versão automática, correção média qr.addData(texto); qr.make(); return qr.createDataURL(5, 4); // cellSize (px), margin (módulos) } catch (e) { return null; } }, [texto]); if (!dataUrl) { return {texto || "URL não configurada"}; } return ( {"QR-Code ); } function PainelConectarDispositivo() { const apiBase = (window.NAZIDE_CONFIG && window.NAZIDE_CONFIG.API_BASE) || ""; return (
Conectar um dispositivo

Na primeira execução, o app de RV pede a URL do Portal. Aponte o headset para este QR-Code — ou digite a URL no teclado do visor. O código transporta apenas o endereço, nunca uma credencial.

{apiBase ? ( {apiBase} ) : ( Defina window.NAZIDE_CONFIG.API_BASE em config.js{" "} para gerar o QR-Code. )}
{apiBase && (
)}
); } /* --- Modais -------------------------------------------------------------- */ function ModalAprovar({ dispositivo, onConfirmar, onFechar, busy, erro }) { const [codigo, setCodigo] = React.useState(""); const [nome, setNome] = React.useState(dispositivo.nome || ""); const codigoCompleto = codigo.trim().length === 6; return ( codigoCompleto && onConfirmar({ pairing_code: codigo.trim().toUpperCase(), nome: nome.trim() || undefined })} onClose={onFechar} >

O app mostra um código de pareamento de 6 caracteres. Digite-o exatamente como aparece no visor — é a prova de que você está diante do aparelho certo.

setCodigo(v.toUpperCase().slice(0, 6))} placeholder="ABC234" />
{dispositivo.install_id}
); } function ModalSegredo({ dispositivo, onFechar }) { const [copiado, setCopiado] = React.useState(false); const copiar = () => { try { navigator.clipboard.writeText(dispositivo.secret); setCopiado(true); setTimeout(() => setCopiado(false), 2000); } catch (e) {} }; return ( Entendi, guardei a credencial} > Copie o segredo agora e provisione-o no dispositivo{" "} {dispositivo.nome}. Ele não fica salvo no Portal.
{dispositivo.secret} {copiado ? "Copiado" : "Copiar"}
); } function ModalEditarRotulo({ dispositivo, onConfirmar, onFechar, busy, erro }) { const [nome, setNome] = React.useState(dispositivo.nome || ""); return ( nome.trim() && onConfirmar(nome.trim())} onClose={onFechar} > ); } /* --- Vista ------------------------------------------------------------- */ const FILTROS = [ ["pendente", "Pendentes"], ["ativo", "Ativos"], ["inativo", "Inativos"], ["", "Todos"], ]; function DevicesView({ onToast }) { const [itens, setItens] = React.useState([]); const [filtro, setFiltro] = React.useState("pendente"); const [carregando, setCarregando] = React.useState(true); const [erroLista, setErroLista] = React.useState(null); const [aprovando, setAprovando] = React.useState(null); const [editando, setEditando] = React.useState(null); const [segredo, setSegredo] = React.useState(null); const [confirmando, setConfirmando] = React.useState(null); // {tipo, dispositivo} const [busy, setBusy] = React.useState(false); const [erroForm, setErroForm] = React.useState(null); const carregar = React.useCallback(() => { setCarregando(true); setErroLista(null); window.API.listDevices(filtro ? { status: filtro } : {}) .then(dados => { setItens(dados.items || []); setCarregando(false); }) .catch(erro => { setErroLista(erro); setItens([]); setCarregando(false); }); }, [filtro]); React.useEffect(() => { carregar(); }, [carregar]); const aprovar = (dados) => { setBusy(true); setErroForm(null); window.API.approveDevice(aprovando.id, dados) .then(() => { setBusy(false); setAprovando(null); onToast("Dispositivo aprovado. O app pode resgatar a credencial."); carregar(); }) .catch(erro => { setBusy(false); setErroForm(erro); }); }; const editarRotulo = (nome) => { setBusy(true); setErroForm(null); window.API.updateDevice(editando.id, { nome }) .then(() => { setBusy(false); setEditando(null); onToast("Rótulo atualizado."); carregar(); }) .catch(erro => { setBusy(false); setErroForm(erro); }); }; const executarConfirmacao = () => { const { tipo, dispositivo } = confirmando; setBusy(true); setErroForm(null); let promessa; if (tipo === "recusar") promessa = window.API.rejectDevice(dispositivo.id); else if (tipo === "desativar") promessa = window.API.setDeviceActive(dispositivo.id, false); else if (tipo === "ativar") promessa = window.API.setDeviceActive(dispositivo.id, true); else if (tipo === "rotacionar") promessa = window.API.rotateDeviceSecret(dispositivo.id); else return; promessa .then(resultado => { setBusy(false); setConfirmando(null); if (tipo === "rotacionar" && resultado && resultado.secret) { setSegredo(resultado); } else { onToast({ recusar: "Solicitação recusada.", desativar: "Dispositivo desativado.", ativar: "Dispositivo reativado.", }[tipo]); } carregar(); }) .catch(erro => { setBusy(false); setErroForm(erro); }); }; const TEXTO_CONFIRMACAO = { recusar: { titulo: "Recusar solicitação", label: "Recusar", corpo: "A solicitação pendente será apagada. Se o app tentar de novo, uma nova solicitação aparecerá aqui." }, desativar: { titulo: "Desativar dispositivo", label: "Desativar", corpo: "A próxima autenticação do dispositivo passa a falhar. Ele não é apagado — pode ser reativado depois." }, ativar: { titulo: "Reativar dispositivo", label: "Reativar", corpo: "O dispositivo volta a autenticar com a credencial que já tem." }, rotacionar: { titulo: "Rotacionar credencial", label: "Rotacionar", corpo: "Um novo segredo é gerado e o anterior deixa de valer imediatamente. O novo segredo aparece uma única vez — provisione-o no dispositivo." }, }; return (
Administração

Dispositivos

Headsets Meta Quest cadastrados. Cada um se registra pelo próprio app; aqui você aprova, ativa e desativa.

{erroLista && ( setErroLista(null)}> {mensagemDeErroDeDispositivo(erroLista)} )}
{FILTROS.map(([valor, rotulo]) => ( ))}
{carregando ? (
Carregando...
) : ( {itens.map(dispositivo => { const meta = STATUS_DO_DISPOSITIVO[dispositivo.status] || { rotulo: dispositivo.status, tag: "" }; const pendente = dispositivo.status === "pendente"; return ( ); })}
Dispositivo Situação Último acesso Ações
{dispositivo.nome}
{dispositivo.install_id} {dispositivo.app_version && · app {dispositivo.app_version}}
{meta.rotulo} {!pendente && !dispositivo.credencial_resgatada && ( aguardando resgate )} {formatarInstante(dispositivo.ultimo_acesso_em)}
{pendente ? ( <> { setErroForm(null); setAprovando(dispositivo); }} /> { setErroForm(null); setConfirmando({ tipo: "recusar", dispositivo }); }} /> ) : ( <> { setErroForm(null); setEditando(dispositivo); }} /> {dispositivo.status === "ativo" ? ( { setErroForm(null); setConfirmando({ tipo: "desativar", dispositivo }); }} /> ) : ( { setErroForm(null); setConfirmando({ tipo: "ativar", dispositivo }); }} /> )} {dispositivo.status === "ativo" && dispositivo.credencial_resgatada && ( { setErroForm(null); setConfirmando({ tipo: "rotacionar", dispositivo }); }} /> )} )}
)} {!carregando && !itens.length && (
{filtro === "pendente" ? "Nenhuma solicitação de registro pendente." : "Nenhum dispositivo nesta situação."}
)}
{aprovando && ( { setAprovando(null); setErroForm(null); }} /> )} {editando && ( { setEditando(null); setErroForm(null); }} /> )} {confirmando && ( { setConfirmando(null); setErroForm(null); }} >

{TEXTO_CONFIRMACAO[confirmando.tipo].corpo}

{confirmando.dispositivo.nome}
)} {segredo && ( setSegredo(null)} /> )}
); } Object.assign(window, { DevicesView });