refactor: quality workflow, docs, tests

This commit is contained in:
Esdras Renan 2025-10-16 19:14:46 -03:00
parent a9caf36b01
commit 68ace0a858
27 changed files with 758 additions and 330 deletions

View file

@ -1,7 +1,5 @@
import { action, mutation, query } from "./_generated/server"
import { api } from "./_generated/api"
import { mutation, query } from "./_generated/server"
import { v } from "convex/values"
import type { Id } from "./_generated/dataModel"
export const log = mutation({
args: {

View file

@ -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) {

View file

@ -204,24 +204,32 @@ async function ensureCompany(
let id: Id<"companies">
if (existing) {
const existingIsAvulso = existing.isAvulso ?? undefined
const targetIsAvulso = payload.isAvulso ?? existingIsAvulso
const targetCnpj = payload.cnpj ?? undefined
const targetDomain = payload.domain ?? undefined
const targetPhone = payload.phone ?? undefined
const targetDescription = payload.description ?? undefined
const targetAddress = payload.address ?? undefined
const needsPatch =
existing.name !== payload.name ||
(existing as any).isAvulso !== (payload.isAvulso ?? (existing as any).isAvulso) ||
existing.cnpj !== (payload.cnpj ?? undefined) ||
existing.domain !== (payload.domain ?? undefined) ||
existing.phone !== (payload.phone ?? undefined) ||
existing.description !== (payload.description ?? undefined) ||
existing.address !== (payload.address ?? undefined) ||
existingIsAvulso !== targetIsAvulso ||
(existing.cnpj ?? undefined) !== targetCnpj ||
(existing.domain ?? undefined) !== targetDomain ||
(existing.phone ?? undefined) !== targetPhone ||
(existing.description ?? undefined) !== targetDescription ||
(existing.address ?? undefined) !== targetAddress ||
existing.provisioningCode !== payload.provisioningCode
if (needsPatch) {
await ctx.db.patch(existing._id, {
name: payload.name,
isAvulso: payload.isAvulso,
cnpj: payload.cnpj,
domain: payload.domain,
phone: payload.phone,
description: payload.description,
address: payload.address,
isAvulso: targetIsAvulso,
cnpj: targetCnpj,
domain: targetDomain,
phone: targetPhone,
description: targetDescription,
address: targetAddress,
provisioningCode: payload.provisioningCode,
updatedAt: Date.now(),
})
@ -359,7 +367,7 @@ export const exportTenantSnapshot = query({
companies: companies.map((company) => ({
slug: company.slug,
name: company.name,
isAvulso: (company as any).isAvulso ?? false,
isAvulso: company.isAvulso ?? false,
cnpj: company.cnpj ?? null,
domain: company.domain ?? null,
phone: company.phone ?? null,

View file

@ -347,7 +347,7 @@ export const list = query({
priority: t.priority,
channel: t.channel,
queue: queueName,
company: company ? { id: company._id, name: company.name, isAvulso: (company as any).isAvulso ?? false } : null,
company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null,
requester: requester && {
id: requester._id,
name: requester.name,
@ -377,14 +377,14 @@ export const list = query({
subcategory: subcategorySummary,
workSummary: {
totalWorkedMs: t.totalWorkedMs ?? 0,
internalWorkedMs: (t as any).internalWorkedMs ?? 0,
externalWorkedMs: (t as any).externalWorkedMs ?? 0,
internalWorkedMs: t.internalWorkedMs ?? 0,
externalWorkedMs: t.externalWorkedMs ?? 0,
activeSession: activeSession
? {
id: activeSession._id,
agentId: activeSession.agentId,
startedAt: activeSession.startedAt,
workType: (activeSession as any).workType ?? "INTERNAL",
workType: activeSession.workType ?? "INTERNAL",
}
: null,
},
@ -525,7 +525,7 @@ export const getById = query({
priority: t.priority,
channel: t.channel,
queue: queueName,
company: company ? { id: company._id, name: company.name, isAvulso: (company as any).isAvulso ?? false } : null,
company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null,
requester: requester && {
id: requester._id,
name: requester.name,
@ -566,14 +566,14 @@ export const getById = query({
: null,
workSummary: {
totalWorkedMs: t.totalWorkedMs ?? 0,
internalWorkedMs: (t as any).internalWorkedMs ?? 0,
externalWorkedMs: (t as any).externalWorkedMs ?? 0,
internalWorkedMs: t.internalWorkedMs ?? 0,
externalWorkedMs: t.externalWorkedMs ?? 0,
activeSession: activeSession
? {
id: activeSession._id,
agentId: activeSession.agentId,
startedAt: activeSession.startedAt,
workType: (activeSession as any).workType ?? "INTERNAL",
workType: activeSession.workType ?? "INTERNAL",
}
: null,
},
@ -1130,14 +1130,14 @@ export const workSummary = query({
return {
ticketId,
totalWorkedMs: ticket.totalWorkedMs ?? 0,
internalWorkedMs: (ticket as any).internalWorkedMs ?? 0,
externalWorkedMs: (ticket as any).externalWorkedMs ?? 0,
internalWorkedMs: ticket.internalWorkedMs ?? 0,
externalWorkedMs: ticket.externalWorkedMs ?? 0,
activeSession: activeSession
? {
id: activeSession._id,
agentId: activeSession.agentId,
startedAt: activeSession.startedAt,
workType: (activeSession as any).workType ?? "INTERNAL",
workType: activeSession.workType ?? "INTERNAL",
}
: null,
}
@ -1275,7 +1275,7 @@ export const pauseWork = mutation({
pauseNote: note ?? "",
})
const sessionType = ((session as any).workType ?? "INTERNAL").toUpperCase()
const sessionType = (session.workType ?? "INTERNAL").toUpperCase()
const deltaInternal = sessionType === "INTERNAL" ? durationMs : 0
const deltaExternal = sessionType === "EXTERNAL" ? durationMs : 0
@ -1283,8 +1283,8 @@ export const pauseWork = mutation({
working: false,
activeSessionId: undefined,
totalWorkedMs: (ticket.totalWorkedMs ?? 0) + durationMs,
internalWorkedMs: ((ticket as any).internalWorkedMs ?? 0) + deltaInternal,
externalWorkedMs: ((ticket as any).externalWorkedMs ?? 0) + deltaExternal,
internalWorkedMs: (ticket.internalWorkedMs ?? 0) + deltaInternal,
externalWorkedMs: (ticket.externalWorkedMs ?? 0) + deltaExternal,
updatedAt: now,
})