feat(admin/ui): filters + badges + full inventory dialog with search; CSV export; types tightened; feat(desktop): charts in diagnostics and heartbeat interval settings; feat(agent): normalized software/services; linux lspci/lsusb parsed

This commit is contained in:
Esdras Renan 2025-10-09 22:29:59 -03:00
parent e682c6773a
commit 0556502685
4 changed files with 308 additions and 46 deletions

View file

@ -175,11 +175,22 @@ async function evaluatePostureAndMaybeRaise(
) {
const findings: PostureFinding[] = []
// Janela temporal de CPU (5 minutos)
const now = Date.now()
const metrics = args.metrics ?? (args.metadata?.metrics ?? null)
if (metrics && typeof metrics === "object") {
const usage = Number((metrics as any).cpuUsagePercent ?? (metrics as any).cpu_usage_percent)
if (Number.isFinite(usage) && usage >= 90) {
findings.push({ kind: "CPU_HIGH", message: `CPU acima de ${usage.toFixed(0)}%`, severity: "warning" })
const metaObj = machine.metadata && typeof machine.metadata === "object" ? (machine.metadata as Record<string, unknown>) : {}
const prevWindow: Array<{ ts: number; usage: number }> = Array.isArray((metaObj as any).cpuWindow)
? (((metaObj as any).cpuWindow as Array<any>).map((p) => ({ ts: Number(p.ts ?? 0), usage: Number(p.usage ?? NaN) })).filter((p) => Number.isFinite(p.ts) && Number.isFinite(p.usage)))
: []
const window = prevWindow.filter((p) => now - p.ts <= 5 * 60 * 1000)
const usage = Number((metrics as any)?.cpuUsagePercent ?? (metrics as any)?.cpu_usage_percent ?? NaN)
if (Number.isFinite(usage)) {
window.push({ ts: now, usage })
}
if (window.length > 0) {
const avg = window.reduce((acc, p) => acc + p.usage, 0) / window.length
if (avg >= 90) {
findings.push({ kind: "CPU_HIGH", message: `CPU média ${avg.toFixed(0)}% em 5 min`, severity: "warning" })
}
}
@ -187,23 +198,37 @@ async function evaluatePostureAndMaybeRaise(
if (inventory && typeof inventory === "object") {
const services = (inventory as any).services
if (Array.isArray(services)) {
const criticalDown = services.find((s: any) => typeof s?.name === "string" && String(s.status ?? "").toLowerCase() !== "running")
if (criticalDown) {
findings.push({ kind: "SERVICE_DOWN", message: `Serviço em falha: ${criticalDown.name}`, severity: "warning" })
const criticalList = (process.env["MACHINE_CRITICAL_SERVICES"] ?? "")
.split(/[\s,]+/)
.map((s) => s.trim().toLowerCase())
.filter(Boolean)
const criticalSet = new Set(criticalList)
const firstDown = services.find((s: any) => typeof s?.name === "string" && String(s.status ?? s?.Status ?? "").toLowerCase() !== "running")
if (firstDown) {
const name = String(firstDown.name ?? firstDown.Name ?? "serviço")
const sev: "warning" | "critical" = criticalSet.has(name.toLowerCase()) ? "critical" : "warning"
findings.push({ kind: "SERVICE_DOWN", message: `Serviço em falha: ${name}`, severity: sev })
}
}
const smart = (inventory as any).extended?.linux?.smart
if (Array.isArray(smart)) {
const failing = smart.find((e: any) => e?.smart_status && e.smart_status.passed === false)
if (failing) {
findings.push({ kind: "SMART_FAIL", message: `Disco com SMART em falha`, severity: "critical" })
const model = failing?.model_name ?? failing?.model_family ?? "Disco"
const serial = failing?.serial_number ?? failing?.device?.name ?? "—"
const temp = failing?.temperature?.current ?? failing?.temperature?.value ?? null
const details = temp ? `${model} (${serial}) · ${temp}ºC` : `${model} (${serial})`
findings.push({ kind: "SMART_FAIL", message: `SMART em falha: ${details}`, severity: "critical" })
}
}
}
// Persistir janela de CPU (limite de 120 amostras)
const cpuWindowCapped = window.slice(-120)
await ctx.db.patch(machine._id, { metadata: mergeMetadata(machine.metadata, { cpuWindow: cpuWindowCapped }) })
if (!findings.length) return
const now = Date.now()
const record = {
postureAlerts: findings,
lastPostureAt: now,
@ -213,7 +238,6 @@ async function evaluatePostureAndMaybeRaise(
await ctx.db.patch(machine._id, { metadata: mergeMetadata(machine.metadata, record), updatedAt: now })
if ((process.env["MACHINE_ALERTS_CREATE_TICKETS"] ?? "true").toLowerCase() !== "true") return
// Evita excesso: não cria ticket se já houve alerta nos últimos 30 minutos
if (lastAtPrev && now - lastAtPrev < 30 * 60 * 1000) return
const subject = `Alerta de máquina: ${machine.hostname}`