import { mutation, query, internalMutation } from "./_generated/server" import { v } from "convex/values" import type { Id } from "./_generated/dataModel" // Tipo para software recebido do agente type SoftwareInput = { name: string version?: string publisher?: string source?: string } // Upsert de softwares de uma maquina (chamado pelo heartbeat) export const syncFromHeartbeat = internalMutation({ args: { tenantId: v.string(), machineId: v.id("machines"), software: v.array( v.object({ name: v.string(), version: v.optional(v.string()), publisher: v.optional(v.string()), source: v.optional(v.string()), }) ), }, handler: async (ctx, { tenantId, machineId, software }) => { const now = Date.now() // Busca softwares existentes da maquina const existing = await ctx.db .query("machineSoftware") .withIndex("by_machine", (q) => q.eq("machineId", machineId)) .collect() const existingMap = new Map(existing.map((s) => [`${s.nameLower}|${s.version ?? ""}`, s])) // Processa cada software recebido const seenKeys = new Set() for (const item of software) { if (!item.name || item.name.trim().length === 0) continue const nameLower = item.name.toLowerCase().trim() const key = `${nameLower}|${item.version ?? ""}` seenKeys.add(key) const existingDoc = existingMap.get(key) if (existingDoc) { // Atualiza lastSeenAt se ja existe await ctx.db.patch(existingDoc._id, { lastSeenAt: now, publisher: item.publisher || existingDoc.publisher, source: item.source || existingDoc.source, }) } else { // Cria novo registro await ctx.db.insert("machineSoftware", { tenantId, machineId, name: item.name.trim(), nameLower, version: item.version?.trim() || undefined, publisher: item.publisher?.trim() || undefined, source: item.source?.trim() || undefined, detectedAt: now, lastSeenAt: now, }) } } // Remove softwares que nao foram vistos (desinstalados) // So remove se o software nao foi visto nas ultimas 24 horas const staleThreshold = now - 24 * 60 * 60 * 1000 for (const doc of existing) { const key = `${doc.nameLower}|${doc.version ?? ""}` if (!seenKeys.has(key) && doc.lastSeenAt < staleThreshold) { await ctx.db.delete(doc._id) } } return { processed: software.length } }, }) // Lista softwares de uma maquina com paginacao e filtros export const listByMachine = query({ args: { tenantId: v.string(), viewerId: v.id("users"), machineId: v.id("machines"), search: v.optional(v.string()), limit: v.optional(v.number()), cursor: v.optional(v.string()), }, handler: async (ctx, { machineId, search, limit = 50, cursor }) => { const pageLimit = Math.min(limit, 100) let query = ctx.db .query("machineSoftware") .withIndex("by_machine", (q) => q.eq("machineId", machineId)) // Coleta todos e filtra em memoria (Convex nao suporta LIKE) const all = await query.collect() // Filtra por search se fornecido let filtered = all if (search && search.trim().length > 0) { const searchLower = search.toLowerCase().trim() filtered = all.filter( (s) => s.nameLower.includes(searchLower) || (s.publisher && s.publisher.toLowerCase().includes(searchLower)) || (s.version && s.version.toLowerCase().includes(searchLower)) ) } // Ordena por nome filtered.sort((a, b) => a.nameLower.localeCompare(b.nameLower)) // Paginacao manual let startIndex = 0 if (cursor) { const cursorIndex = filtered.findIndex((s) => s._id === cursor) if (cursorIndex >= 0) { startIndex = cursorIndex + 1 } } const page = filtered.slice(startIndex, startIndex + pageLimit) const nextCursor = page.length === pageLimit ? page[page.length - 1]._id : null return { items: page.map((s) => ({ id: s._id, name: s.name, version: s.version ?? null, publisher: s.publisher ?? null, source: s.source ?? null, detectedAt: s.detectedAt, lastSeenAt: s.lastSeenAt, })), total: filtered.length, nextCursor, } }, }) // Lista softwares de todas as maquinas de um tenant (para admin) export const listByTenant = query({ args: { tenantId: v.string(), viewerId: v.id("users"), search: v.optional(v.string()), machineId: v.optional(v.id("machines")), limit: v.optional(v.number()), cursor: v.optional(v.string()), }, handler: async (ctx, { tenantId, search, machineId, limit = 50, cursor }) => { const pageLimit = Math.min(limit, 100) // Busca por tenant ou por maquina especifica let all: Array<{ _id: Id<"machineSoftware"> tenantId: string machineId: Id<"machines"> name: string nameLower: string version?: string publisher?: string source?: string detectedAt: number lastSeenAt: number }> if (machineId) { all = await ctx.db .query("machineSoftware") .withIndex("by_tenant_machine", (q) => q.eq("tenantId", tenantId).eq("machineId", machineId)) .collect() } else { // Busca por tenant - pode ser grande, limita all = await ctx.db .query("machineSoftware") .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId)) .take(5000) } // Filtra por search let filtered = all if (search && search.trim().length > 0) { const searchLower = search.toLowerCase().trim() filtered = all.filter( (s) => s.nameLower.includes(searchLower) || (s.publisher && s.publisher.toLowerCase().includes(searchLower)) || (s.version && s.version.toLowerCase().includes(searchLower)) ) } // Ordena por nome filtered.sort((a, b) => a.nameLower.localeCompare(b.nameLower)) // Paginacao let startIndex = 0 if (cursor) { const cursorIndex = filtered.findIndex((s) => s._id === cursor) if (cursorIndex >= 0) { startIndex = cursorIndex + 1 } } const page = filtered.slice(startIndex, startIndex + pageLimit) const nextCursor = page.length === pageLimit ? page[page.length - 1]._id : null // Busca nomes das maquinas const machineIds = [...new Set(page.map((s) => s.machineId))] const machines = await Promise.all(machineIds.map((id) => ctx.db.get(id))) const machineNames = new Map( machines.filter(Boolean).map((m) => [m!._id, m!.displayName || m!.hostname]) ) return { items: page.map((s) => ({ id: s._id, machineId: s.machineId, machineName: machineNames.get(s.machineId) ?? "Desconhecido", name: s.name, version: s.version ?? null, publisher: s.publisher ?? null, source: s.source ?? null, detectedAt: s.detectedAt, lastSeenAt: s.lastSeenAt, })), total: filtered.length, nextCursor, } }, }) // Conta softwares de uma maquina export const countByMachine = query({ args: { machineId: v.id("machines"), }, handler: async (ctx, { machineId }) => { const software = await ctx.db .query("machineSoftware") .withIndex("by_machine", (q) => q.eq("machineId", machineId)) .collect() return { count: software.length } }, }) // Conta softwares unicos por tenant (para relatorios) export const stats = query({ args: { tenantId: v.string(), viewerId: v.id("users"), }, handler: async (ctx, { tenantId }) => { const software = await ctx.db .query("machineSoftware") .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId)) .take(10000) const uniqueNames = new Set(software.map((s) => s.nameLower)) const machineIds = new Set(software.map((s) => s.machineId)) return { totalInstances: software.length, uniqueSoftware: uniqueNames.size, machinesWithSoftware: machineIds.size, } }, })