201 lines
9.9 KiB
TypeScript
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>
|
|
)
|
|
}
|