diff --git a/convex/machines.ts b/convex/machines.ts index 0863694..9194caa 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -251,6 +251,10 @@ function isObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value) } +// Campos do inventory que sao muito grandes e nao devem ser persistidos +// para evitar OOM no Convex (documentos de ~100KB cada) +const INVENTORY_BLOCKLIST = new Set(["software", "extended"]) + function mergeInventory(current: unknown, patch: unknown): unknown { if (!isObject(patch)) { return patch @@ -258,6 +262,8 @@ function mergeInventory(current: unknown, patch: unknown): unknown { const base: Record = isObject(current) ? { ...(current as Record) } : {} for (const [key, value] of Object.entries(patch)) { if (value === undefined) continue + // Filtrar campos volumosos que causam OOM + if (INVENTORY_BLOCKLIST.has(key)) continue if (isObject(value) && isObject(base[key])) { base[key] = mergeInventory(base[key], value) } else { diff --git a/convex/migrations.ts b/convex/migrations.ts index c90f580..9ea35e6 100644 --- a/convex/migrations.ts +++ b/convex/migrations.ts @@ -870,6 +870,122 @@ export const syncMachineCompanyReferences = mutation({ }, }) +/** + * Migracao para compactar metadata de machines, removendo dados volumosos + * como inventory.software (lista de programas instalados) que podem ter + * centenas de KBs por maquina. + * + * Esta migracao preserva: + * - metrics (metricas de sistema) + * - postureAlerts (alertas de postura) + * - lastPostureAt (timestamp) + * - collaborator (email do colaborador) + * - inventory.os, inventory.cpu, inventory.memory, inventory.disks, inventory.network + * - inventory.services (lista de servicos - usado para alertas de postura) + * + * Remove: + * - inventory.software (lista completa de programas instalados - muito grande) + * - inventory.extended (dados estendidos do Linux - muito grande) + * - cpuWindow (janela de CPU - pode ser reconstruida) + */ +export const compactMachineMetadata = mutation({ + args: { + tenantId: v.optional(v.string()), + limit: v.optional(v.number()), + dryRun: v.optional(v.boolean()), + }, + handler: async (ctx, { tenantId, limit, dryRun }) => { + const effectiveDryRun = Boolean(dryRun) + const effectiveLimit = limit && limit > 0 ? Math.min(limit, 200) : 200 + + // Busca maquinas em lotes pequenos para evitar OOM + const machines = tenantId && tenantId.trim().length > 0 + ? await ctx.db + .query("machines") + .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) + .take(effectiveLimit) + : await ctx.db.query("machines").take(effectiveLimit) + + let processed = 0 + let compacted = 0 + let bytesSavedEstimate = 0 + + for (const machine of machines) { + processed += 1 + const metadata = machine.metadata + if (!metadata || typeof metadata !== "object") continue + + const meta = metadata as Record + const inventory = meta["inventory"] + const cpuWindow = meta["cpuWindow"] + + // Verificar se precisa compactar + let needsCompact = false + const sizeBefore = JSON.stringify(meta).length + + const newMeta: Record = {} + + // Preservar campos essenciais + if (meta["metrics"]) newMeta["metrics"] = meta["metrics"] + if (meta["postureAlerts"]) newMeta["postureAlerts"] = meta["postureAlerts"] + if (meta["lastPostureAt"]) newMeta["lastPostureAt"] = meta["lastPostureAt"] + if (meta["collaborator"]) newMeta["collaborator"] = meta["collaborator"] + if (meta["remoteAccessSnapshot"]) newMeta["remoteAccessSnapshot"] = meta["remoteAccessSnapshot"] + + // Compactar cpuWindow para apenas ultimas 30 amostras (em vez de 120) + if (Array.isArray(cpuWindow) && cpuWindow.length > 30) { + newMeta["cpuWindow"] = cpuWindow.slice(-30) + needsCompact = true + } else if (cpuWindow) { + newMeta["cpuWindow"] = cpuWindow + } + + // Compactar inventory - remover software e extended + if (inventory && typeof inventory === "object") { + const inv = inventory as Record + const newInv: Record = {} + + // Preservar apenas campos essenciais do inventory + if (inv["os"]) newInv["os"] = inv["os"] + if (inv["cpu"]) newInv["cpu"] = inv["cpu"] + if (inv["memory"]) newInv["memory"] = inv["memory"] + if (inv["disks"]) newInv["disks"] = inv["disks"] + if (inv["network"]) newInv["network"] = inv["network"] + if (inv["services"]) newInv["services"] = inv["services"] + if (inv["bios"]) newInv["bios"] = inv["bios"] + if (inv["motherboard"]) newInv["motherboard"] = inv["motherboard"] + + // Verificar se tinha software ou extended (campos volumosos) + if (inv["software"] || inv["extended"]) { + needsCompact = true + } + + if (Object.keys(newInv).length > 0) { + newMeta["inventory"] = newInv + } + } + + if (!needsCompact) continue + + const sizeAfter = JSON.stringify(newMeta).length + bytesSavedEstimate += sizeBefore - sizeAfter + + if (!effectiveDryRun) { + await ctx.db.patch(machine._id, { metadata: newMeta, updatedAt: Date.now() }) + } + compacted += 1 + } + + return { + dryRun: effectiveDryRun, + processed, + compacted, + bytesSavedEstimate, + bytesSavedMB: Math.round(bytesSavedEstimate / 1024 / 1024 * 100) / 100, + } + }, +}) + export const backfillTicketSnapshots = mutation({ args: { tenantId: v.string(), limit: v.optional(v.number()) }, handler: async (ctx, { tenantId, limit }) => {