feat(platform): Sistema > Usuários/Auditoria, Infraestrutura > Saúde
Três endpoints novos, todos platform-only: GET /platform/users (cross- tenant, users não tem RLS) + PATCH .../status (desabilitar tem efeito real — login() já checava status ACTIVE desde a PHASE 04); GET /platform/audit-log (últimos 200 eventos, audit_logs também sem RLS, linha imutável); GET /platform/health (Postgres/Redis + FreeSWITCH via conexão ESL avulsa, sem manter estado). Achado de arquitetura documentado explicitamente na própria tela: o check de FreeSWITCH sempre falha neste ambiente porque apps/api roda no host e a porta 8021 é deliberadamente não publicada (decisão da PHASE 01/05) — não é um bug, é a rede isolada do jeito certo. Frontend: /platform/sistema/usuarios, /auditoria, /platform/ infraestrutura/saude. Testado ponta a ponta contra dados reais (3 usuários da plataforma, audit log com eventos reais desta sessão, inclusive uma referência órfã tratada corretamente). Smoke test nas 19 telas anteriores, todas 200. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
10
apps/frontend/src/app/platform/infraestrutura/saude/page.tsx
Normal file
10
apps/frontend/src/app/platform/infraestrutura/saude/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { requireSession } from "@/lib/session";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import type { PlatformHealth } from "@/lib/platform-types";
|
||||
import { SaudeView } from "./saude-view";
|
||||
|
||||
export default async function SaudePage() {
|
||||
const session = await requireSession();
|
||||
const health = await apiFetch<PlatformHealth>("/platform/health", session.accessToken);
|
||||
return <SaudeView health={health} />;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CircleCheck, CircleX, Database, Phone, RefreshCw, Server } from "lucide-react";
|
||||
import { Panel, PanelHeader } from "@/components/ui/panel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import type { HealthCheck, PlatformHealth } from "@/lib/platform-types";
|
||||
|
||||
const CHECKS: { key: keyof Omit<PlatformHealth, "checkedAt">; label: string; icon: typeof Database }[] = [
|
||||
{ key: "postgres", label: "PostgreSQL", icon: Database },
|
||||
{ key: "redis", label: "Redis", icon: Server },
|
||||
{ key: "freeswitch", label: "FreeSWITCH (ESL)", icon: Phone },
|
||||
];
|
||||
|
||||
export function SaudeView({ health }: { health: PlatformHealth }) {
|
||||
const router = useRouter();
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-foreground">Saúde da infraestrutura</h1>
|
||||
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||
Verificação ao vivo (agente.md secao 187) — checada agora, não um monitoramento contínuo. Última
|
||||
checagem: {formatDateTime(health.checkedAt)}.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => startTransition(() => router.refresh())} disabled={pending}>
|
||||
<RefreshCw className={`h-3.5 w-3.5 ${pending ? "animate-spin" : ""}`} aria-hidden />
|
||||
{pending ? "Verificando…" : "Verificar de novo"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{CHECKS.map(({ key, label, icon: Icon }) => (
|
||||
<CheckCard key={key} label={label} icon={Icon} check={health[key]} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Panel>
|
||||
<PanelHeader title="Sobre o check de FreeSWITCH" />
|
||||
<p className="px-5 py-4 text-sm text-muted-foreground">
|
||||
<code className="font-mono text-xs">apps/api</code> roda direto no host desta VM, fora do Docker; a porta
|
||||
do Event Socket (8021) do FreeSWITCH é deliberadamente <strong>não publicada no host</strong> (agente.md
|
||||
secao 184: porta sensível, nunca exposta). Por isso este check falha mesmo com o FreeSWITCH saudável — o
|
||||
container está isolado do jeito certo. Os serviços que realmente falam com o FreeSWITCH
|
||||
(<code className="font-mono text-xs">fs-events</code>, <code className="font-mono text-xs">fs-config</code>
|
||||
, <code className="font-mono text-xs">predictive-dialer</code>) rodam dentro da mesma rede Docker e não
|
||||
têm esse problema.
|
||||
</p>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckCard({ label, icon: Icon, check }: { label: string; icon: typeof Database; check: HealthCheck }) {
|
||||
const ok = check.status === "ok";
|
||||
return (
|
||||
<div className="flex flex-col justify-between rounded-lg border border-border bg-surface p-5 shadow-panel">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||
{label}
|
||||
</span>
|
||||
{ok ? <CircleCheck className="h-5 w-5 text-status-green" aria-hidden /> : <CircleX className="h-5 w-5 text-status-red" aria-hidden />}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<span className={`text-2xl font-semibold ${ok ? "text-status-green" : "text-status-red"}`}>{ok ? "OK" : "Falhou"}</span>
|
||||
<p className="mt-1 font-mono text-xs text-muted-foreground">{check.latencyMs}ms</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user