refactor: quality workflow, docs, tests
This commit is contained in:
parent
a9caf36b01
commit
68ace0a858
27 changed files with 758 additions and 330 deletions
|
|
@ -107,7 +107,7 @@ async function getActiveToken(
|
|||
const tokenHash = hashToken(tokenValue)
|
||||
const token = await ctx.db
|
||||
.query("machineTokens")
|
||||
.withIndex("by_token_hash", (q: any) => q.eq("tokenHash", tokenHash))
|
||||
.withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
|
||||
.unique()
|
||||
|
||||
if (!token) {
|
||||
|
|
@ -163,6 +163,42 @@ function mergeMetadata(current: unknown, patch: Record<string, unknown>) {
|
|||
return base
|
||||
}
|
||||
|
||||
type JsonRecord = Record<string, unknown>
|
||||
|
||||
function ensureRecord(value: unknown): JsonRecord | null {
|
||||
return isObject(value) ? (value as JsonRecord) : null
|
||||
}
|
||||
|
||||
function ensureRecordArray(value: unknown): JsonRecord[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.filter(isObject) as JsonRecord[]
|
||||
}
|
||||
|
||||
function ensureFiniteNumber(value: unknown): number | null {
|
||||
const num = typeof value === "number" ? value : Number(value)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
function ensureString(value: unknown): string | null {
|
||||
return typeof value === "string" ? value : null
|
||||
}
|
||||
|
||||
function getNestedRecord(root: JsonRecord | null, ...keys: string[]): JsonRecord | null {
|
||||
let current: JsonRecord | null = root
|
||||
for (const key of keys) {
|
||||
if (!current) return null
|
||||
current = ensureRecord(current[key])
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
function getNestedRecordArray(root: JsonRecord | null, ...keys: string[]): JsonRecord[] {
|
||||
if (keys.length === 0) return []
|
||||
const parent = getNestedRecord(root, ...keys.slice(0, -1))
|
||||
if (!parent) return []
|
||||
return ensureRecordArray(parent[keys[keys.length - 1]])
|
||||
}
|
||||
|
||||
type PostureFinding = {
|
||||
kind: "CPU_HIGH" | "SERVICE_DOWN" | "SMART_FAIL"
|
||||
message: string
|
||||
|
|
@ -179,16 +215,16 @@ async function createTicketForAlert(
|
|||
const actorEmail = process.env["MACHINE_ALERTS_TICKET_REQUESTER_EMAIL"] ?? "admin@sistema.dev"
|
||||
const actor = await ctx.db
|
||||
.query("users")
|
||||
.withIndex("by_tenant_email", (q: any) => q.eq("tenantId", tenantId).eq("email", actorEmail))
|
||||
.withIndex("by_tenant_email", (q) => q.eq("tenantId", tenantId).eq("email", actorEmail))
|
||||
.unique()
|
||||
if (!actor) return null
|
||||
|
||||
// pick first category/subcategory if not configured
|
||||
const category = await ctx.db.query("ticketCategories").withIndex("by_tenant", (q: any) => q.eq("tenantId", tenantId)).first()
|
||||
const category = await ctx.db.query("ticketCategories").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).first()
|
||||
if (!category) return null
|
||||
const subcategory = await ctx.db
|
||||
.query("ticketSubcategories")
|
||||
.withIndex("by_category_order", (q: any) => q.eq("categoryId", category._id))
|
||||
.withIndex("by_category_order", (q) => q.eq("categoryId", category._id))
|
||||
.first()
|
||||
if (!subcategory) return null
|
||||
|
||||
|
|
@ -217,20 +253,32 @@ async function createTicketForAlert(
|
|||
async function evaluatePostureAndMaybeRaise(
|
||||
ctx: MutationCtx,
|
||||
machine: Doc<"machines">,
|
||||
args: { metrics?: any; inventory?: any; metadata?: any }
|
||||
args: {
|
||||
metrics?: JsonRecord | null
|
||||
inventory?: JsonRecord | null
|
||||
metadata?: JsonRecord | null
|
||||
}
|
||||
) {
|
||||
const findings: PostureFinding[] = []
|
||||
|
||||
// Janela temporal de CPU (5 minutos)
|
||||
const now = Date.now()
|
||||
const metrics = args.metrics ?? (args.metadata?.metrics ?? null)
|
||||
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 metadataPatch = ensureRecord(args.metadata)
|
||||
const metrics = ensureRecord(args.metrics) ?? ensureRecord(metadataPatch?.["metrics"])
|
||||
const metaObj: JsonRecord = ensureRecord(machine.metadata) ?? {}
|
||||
const prevWindowRecords = ensureRecordArray(metaObj["cpuWindow"])
|
||||
const prevWindow: Array<{ ts: number; usage: number }> = prevWindowRecords
|
||||
.map((entry) => {
|
||||
const ts = ensureFiniteNumber(entry["ts"])
|
||||
const usage = ensureFiniteNumber(entry["usage"])
|
||||
if (ts === null || usage === null) return null
|
||||
return { ts, usage }
|
||||
})
|
||||
.filter((entry): entry is { ts: number; usage: number } => entry !== null)
|
||||
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)) {
|
||||
const usage =
|
||||
ensureFiniteNumber(metrics?.["cpuUsagePercent"]) ?? ensureFiniteNumber(metrics?.["cpu_usage_percent"])
|
||||
if (usage !== null) {
|
||||
window.push({ ts: now, usage })
|
||||
}
|
||||
if (window.length > 0) {
|
||||
|
|
@ -240,30 +288,48 @@ async function evaluatePostureAndMaybeRaise(
|
|||
}
|
||||
}
|
||||
|
||||
const inventory = args.inventory ?? (args.metadata?.inventory ?? null)
|
||||
if (inventory && typeof inventory === "object") {
|
||||
const services = (inventory as any).services
|
||||
if (Array.isArray(services)) {
|
||||
const inventory = ensureRecord(args.inventory) ?? ensureRecord(metadataPatch?.["inventory"])
|
||||
if (inventory) {
|
||||
const services = ensureRecordArray(inventory["services"])
|
||||
if (services.length > 0) {
|
||||
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")
|
||||
const firstDown = services.find((service) => {
|
||||
const status = ensureString(service["status"]) ?? ensureString(service["Status"]) ?? ""
|
||||
const name = ensureString(service["name"]) ?? ensureString(service["Name"]) ?? ""
|
||||
return Boolean(name) && status.toLowerCase() !== "running"
|
||||
})
|
||||
if (firstDown) {
|
||||
const name = String(firstDown.name ?? firstDown.Name ?? "serviço")
|
||||
const name = ensureString(firstDown["name"]) ?? ensureString(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) {
|
||||
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})`
|
||||
const smartEntries = getNestedRecordArray(inventory, "extended", "linux", "smart")
|
||||
if (smartEntries.length > 0) {
|
||||
const firstFail = smartEntries.find((disk) => {
|
||||
const status = ensureString(disk["smart_status"]) ?? ensureString(disk["status"]) ?? ""
|
||||
return status.toLowerCase() !== "ok"
|
||||
})
|
||||
if (firstFail) {
|
||||
const model =
|
||||
ensureString(firstFail["model_name"]) ??
|
||||
ensureString(firstFail["model_family"]) ??
|
||||
ensureString(firstFail["model"]) ??
|
||||
"Disco"
|
||||
const deviceRecord = getNestedRecord(firstFail, "device")
|
||||
const serial =
|
||||
ensureString(firstFail["serial_number"]) ??
|
||||
ensureString(deviceRecord?.["name"]) ??
|
||||
"—"
|
||||
const temperatureRecord = getNestedRecord(firstFail, "temperature")
|
||||
const temp =
|
||||
ensureFiniteNumber(temperatureRecord?.["current"]) ??
|
||||
ensureFiniteNumber(temperatureRecord?.["value"])
|
||||
const details = temp !== null ? `${model} (${serial}) · ${temp}ºC` : `${model} (${serial})`
|
||||
findings.push({ kind: "SMART_FAIL", message: `SMART em falha: ${details}`, severity: "critical" })
|
||||
}
|
||||
}
|
||||
|
|
@ -279,8 +345,8 @@ async function evaluatePostureAndMaybeRaise(
|
|||
postureAlerts: findings,
|
||||
lastPostureAt: now,
|
||||
}
|
||||
const prevMeta = (machine.metadata && typeof machine.metadata === "object") ? (machine.metadata as Record<string, unknown>) : null
|
||||
const lastAtPrev = typeof prevMeta?.lastPostureAt === "number" ? (prevMeta!.lastPostureAt as number) : 0
|
||||
const prevMeta = ensureRecord(machine.metadata)
|
||||
const lastAtPrev = ensureFiniteNumber(prevMeta?.["lastPostureAt"]) ?? 0
|
||||
await ctx.db.patch(machine._id, { metadata: mergeMetadata(machine.metadata, record), updatedAt: now })
|
||||
|
||||
if ((process.env["MACHINE_ALERTS_CREATE_TICKETS"] ?? "true").toLowerCase() !== "true") return
|
||||
|
|
@ -934,10 +1000,9 @@ export const rename = mutation({
|
|||
args: {
|
||||
machineId: v.id("machines"),
|
||||
actorId: v.id("users"),
|
||||
tenantId: v.optional(v.string()),
|
||||
hostname: v.string(),
|
||||
},
|
||||
handler: async (ctx, { machineId, actorId, tenantId, hostname }) => {
|
||||
handler: async (ctx, { machineId, actorId, hostname }) => {
|
||||
// Reutiliza requireStaff através de tickets.ts helpers
|
||||
const machine = await ctx.db.get(machineId)
|
||||
if (!machine) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue