sistema-de-chamados/src/app/admin/health/page.tsx
2025-12-10 14:43:13 -03:00

201 lines
9.9 KiB
TypeScript

import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { AppShell } from "@/components/app-shell"
import { SiteHeader } from "@/components/site-header"
import { getHealthSnapshot } from "@/server/health"
export const runtime = "nodejs"
export const dynamic = "force-dynamic"
function formatDuration(ms: number | null): string {
if (ms === null) return "N/A"
const totalSeconds = Math.floor(ms / 1000)
const minutes = Math.floor(totalSeconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
if (days > 0) return `${days}d ${hours % 24}h`
if (hours > 0) return `${hours}h ${minutes % 60}m`
if (minutes > 0) return `${minutes}m`
return `${totalSeconds}s`
}
function formatNumber(value: number) {
return value.toLocaleString("pt-BR")
}
export default async function AdminHealthPage() {
const snapshot = await getHealthSnapshot()
const devices = snapshot.devices
const retention = snapshot.retention
const strategy = snapshot.retentionStrategy
return (
<AppShell
header={
<SiteHeader
title="Saude da plataforma"
lead="Visao consolidada de tickets, dispositivos e limites operacionais sem cron de limpeza automatica."
/>
}
>
<div className="mx-auto w-full max-w-6xl space-y-8 px-6 lg:px-8">
<div className="grid gap-4 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>Tickets</CardTitle>
<CardDescription>Sem expiracao automatica; acompanhar volume diario.</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-baseline justify-between">
<span className="text-sm text-muted-foreground">Total</span>
<span className="text-3xl font-semibold">{formatNumber(snapshot.tickets.total)}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Abertos</span>
<Badge variant="secondary">{formatNumber(snapshot.tickets.open)}</Badge>
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>Ultimos 7 dias</span>
<span>{formatNumber(snapshot.tickets.last7d)}</span>
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>Ultimas 24h</span>
<span>{formatNumber(snapshot.tickets.last24h)}</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div>
<CardTitle>Dispositivos</CardTitle>
<CardDescription>Heartbeats e conectividade (Convex)</CardDescription>
</div>
<Badge>{devices ? "Ativo" : "Sem resposta"}</Badge>
</CardHeader>
<CardContent className="space-y-3">
{devices ? (
<>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Cadastrados</span>
<span className="font-semibold">{formatNumber(devices.machines)}</span>
</div>
<div className="grid grid-cols-3 gap-2 text-sm">
<div className="rounded-md bg-emerald-50 px-3 py-2 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-200">
<div className="text-xs uppercase tracking-wide text-emerald-700 dark:text-emerald-300">Online</div>
<div className="text-lg font-semibold">{formatNumber(devices.online)}</div>
</div>
<div className="rounded-md bg-amber-50 px-3 py-2 text-amber-800 dark:bg-amber-950/40 dark:text-amber-100">
<div className="text-xs uppercase tracking-wide text-amber-700 dark:text-amber-200">Atencao</div>
<div className="text-lg font-semibold">{formatNumber(devices.warning)}</div>
</div>
<div className="rounded-md bg-rose-50 px-3 py-2 text-rose-800 dark:bg-rose-950/40 dark:text-rose-100">
<div className="text-xs uppercase tracking-wide text-rose-700 dark:text-rose-200">Offline</div>
<div className="text-lg font-semibold">{formatNumber(devices.offline)}</div>
</div>
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>Sem heartbeat</span>
<span>{formatNumber(devices.withoutHeartbeat)}</span>
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>Ultimo heartbeat</span>
<span>{formatDuration(devices.newestHeartbeatAgeMs)}</span>
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>Mais antigo</span>
<span>{formatDuration(devices.oldestHeartbeatAgeMs)}</span>
</div>
<div className="text-xs text-muted-foreground">
Offline apos {Math.round(devices.thresholds.offlineMs / 60000)} min | stale apos{" "}
{Math.round(devices.thresholds.staleMs / 60000)} min
{devices.truncated ? " (amostra limitada)" : ""}
</div>
</>
) : (
<p className="text-sm text-muted-foreground">
Nao foi possivel ler o estado do Convex. Confirme token interno e conectividade do backend.
</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Cadastros</CardTitle>
<CardDescription>Usuarios e empresas ativos na base SQL.</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Usuarios</span>
<span className="font-semibold">{formatNumber(snapshot.accounts.users)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Empresas</span>
<span className="font-semibold">{formatNumber(snapshot.accounts.companies)}</span>
</div>
<div className="rounded-md border border-dashed p-3 text-xs text-muted-foreground">
Tickets antigos continuam consultaveis; planejar arquivamento frio antes de qualquer offload massivo.
</div>
</CardContent>
</Card>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Retencao</CardTitle>
<CardDescription>Duracao alvo por tipo de dado (sem cron de exclusao ligado).</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Tickets</span>
<span className="font-semibold">Sem expiracao</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Telemetria inventory/metrics</span>
<span className="font-semibold">{retention.machineTelemetry.inventoryDays} dias</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Alertas de dispositivo</span>
<span className="font-semibold">{retention.machineTelemetry.alertsDays} dias</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Historico de exports</span>
<span className="font-semibold">{retention.reportExports.runsDays} dias</span>
</div>
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground">
{strategy.archivalPlan} Sem exclusao automatica; GC preguicoso pode ser habilitado futuramente em handlers de
alto volume.
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Check rapido</CardTitle>
<CardDescription>Comandos manuais recomendados (sem tocar em dados).</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<div className="rounded-md border border-dashed p-3 font-mono text-xs">
ssh -i ~/.ssh/codex_ed25519 root@154.12.253.40 "ls -lh /var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3"
</div>
<div className="rounded-md border border-dashed p-3 font-mono text-xs">
ssh -i ~/.ssh/codex_ed25519 root@154.12.253.40 "docker stats --no-stream | grep convex"
</div>
<p className="text-muted-foreground">
Objetivo: acompanhar tamanho do SQLite e memoria do Convex por 2-4 semanas. Se subir alem de 200 MB / 5 GB,
abrir janela de manutencao com backup antes de limpar/arquivar.
</p>
</CardContent>
</Card>
</div>
<p className="text-xs text-muted-foreground">
Atualizado em {new Date(snapshot.generatedAt).toLocaleString("pt-BR")}{" "}
{snapshot.notes ? `- Observacao: ${snapshot.notes}` : ""}
</p>
</div>
</AppShell>
)
}