refactor: quality workflow, docs, tests
This commit is contained in:
parent
a9caf36b01
commit
68ace0a858
27 changed files with 758 additions and 330 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue