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:
parent
e682c6773a
commit
0556502685
4 changed files with 308 additions and 46 deletions
|
|
@ -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}`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue