import { v } from "convex/values"; import { mutation, query, internalMutation } from "./_generated/server"; import { internal } from "./_generated/api"; import { Id, Doc } from "./_generated/dataModel"; import type { QueryCtx } from "./_generated/server"; /** * Helper para obter usuário autenticado */ async function getUsuarioAutenticado(ctx: QueryCtx) { const usuariosOnline = await ctx.db.query("usuarios").collect(); const usuarioOnline = usuariosOnline.find( (u) => u.statusPresenca === "online" ); return usuarioOnline || null; } /** * Salvar métricas do sistema */ export const salvarMetricas = mutation({ args: { cpuUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()), networkLatency: v.optional(v.number()), storageUsed: v.optional(v.number()), usuariosOnline: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()), errosCount: v.optional(v.number()), }, returns: v.object({ success: v.boolean(), metricId: v.optional(v.id("systemMetrics")), }), handler: async (ctx, args) => { const timestamp = Date.now(); // Salvar métricas const metricId = await ctx.db.insert("systemMetrics", { timestamp, cpuUsage: args.cpuUsage, memoryUsage: args.memoryUsage, networkLatency: args.networkLatency, storageUsed: args.storageUsed, usuariosOnline: args.usuariosOnline, mensagensPorMinuto: args.mensagensPorMinuto, tempoRespostaMedio: args.tempoRespostaMedio, errosCount: args.errosCount, }); // Verificar alertas após salvar métricas await ctx.scheduler.runAfter(0, internal.monitoramento.verificarAlertasInternal, { metricId, }); // Limpar métricas antigas (mais de 30 dias) const dataLimite = Date.now() - 30 * 24 * 60 * 60 * 1000; const metricasAntigas = await ctx.db .query("systemMetrics") .withIndex("by_timestamp", (q) => q.lt("timestamp", dataLimite)) .collect(); for (const metrica of metricasAntigas) { await ctx.db.delete(metrica._id); } return { success: true, metricId, }; }, }); /** * Configurar ou atualizar alerta */ export const configurarAlerta = mutation({ args: { alertId: v.optional(v.id("alertConfigurations")), metricName: v.string(), threshold: v.number(), operator: v.union( v.literal(">"), v.literal("<"), v.literal(">="), v.literal("<="), v.literal("==") ), enabled: v.boolean(), notifyByEmail: v.boolean(), notifyByChat: v.boolean(), }, returns: v.object({ success: v.boolean(), alertId: v.id("alertConfigurations"), }), handler: async (ctx, args) => { const usuario = await getUsuarioAutenticado(ctx); if (!usuario) { throw new Error("Não autenticado"); } let alertId: Id<"alertConfigurations">; if (args.alertId) { // Atualizar alerta existente await ctx.db.patch(args.alertId, { metricName: args.metricName, threshold: args.threshold, operator: args.operator, enabled: args.enabled, notifyByEmail: args.notifyByEmail, notifyByChat: args.notifyByChat, lastModified: Date.now(), }); alertId = args.alertId; } else { // Criar novo alerta alertId = await ctx.db.insert("alertConfigurations", { metricName: args.metricName, threshold: args.threshold, operator: args.operator, enabled: args.enabled, notifyByEmail: args.notifyByEmail, notifyByChat: args.notifyByChat, createdBy: usuario._id, lastModified: Date.now(), }); } return { success: true, alertId, }; }, }); /** * Listar todas as configurações de alerta */ export const listarAlertas = query({ args: {}, returns: v.array( v.object({ _id: v.id("alertConfigurations"), metricName: v.string(), threshold: v.number(), operator: v.union( v.literal(">"), v.literal("<"), v.literal(">="), v.literal("<="), v.literal("==") ), enabled: v.boolean(), notifyByEmail: v.boolean(), notifyByChat: v.boolean(), createdBy: v.id("usuarios"), lastModified: v.number(), }) ), handler: async (ctx) => { const alertas = await ctx.db.query("alertConfigurations").collect(); return alertas; }, }); /** * Obter métricas com filtros */ export const obterMetricas = query({ args: { dataInicio: v.optional(v.number()), dataFim: v.optional(v.number()), metricName: v.optional(v.string()), limit: v.optional(v.number()), }, returns: v.array( v.object({ _id: v.id("systemMetrics"), timestamp: v.number(), cpuUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()), networkLatency: v.optional(v.number()), storageUsed: v.optional(v.number()), usuariosOnline: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()), errosCount: v.optional(v.number()), }) ), handler: async (ctx, args) => { let query = ctx.db.query("systemMetrics"); // Filtrar por data se fornecido if (args.dataInicio !== undefined || args.dataFim !== undefined) { query = query.withIndex("by_timestamp", (q) => { if (args.dataInicio !== undefined && args.dataFim !== undefined) { return q.gte("timestamp", args.dataInicio).lte("timestamp", args.dataFim); } else if (args.dataInicio !== undefined) { return q.gte("timestamp", args.dataInicio); } else { return q.lte("timestamp", args.dataFim!); } }); } let metricas = await query.order("desc").collect(); // Limitar resultados if (args.limit !== undefined && args.limit > 0) { metricas = metricas.slice(0, args.limit); } return metricas; }, }); /** * Obter métricas mais recentes (última hora) */ export const obterMetricasRecentes = query({ args: {}, returns: v.array( v.object({ _id: v.id("systemMetrics"), timestamp: v.number(), cpuUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()), networkLatency: v.optional(v.number()), storageUsed: v.optional(v.number()), usuariosOnline: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()), errosCount: v.optional(v.number()), }) ), handler: async (ctx) => { const umaHoraAtras = Date.now() - 60 * 60 * 1000; const metricas = await ctx.db .query("systemMetrics") .withIndex("by_timestamp", (q) => q.gte("timestamp", umaHoraAtras)) .order("desc") .take(100); return metricas; }, }); /** * Obter última métrica salva */ export const obterUltimaMetrica = query({ args: {}, returns: v.union( v.object({ _id: v.id("systemMetrics"), timestamp: v.number(), cpuUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()), networkLatency: v.optional(v.number()), storageUsed: v.optional(v.number()), usuariosOnline: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()), errosCount: v.optional(v.number()), }), v.null() ), handler: async (ctx) => { const metrica = await ctx.db .query("systemMetrics") .order("desc") .first(); return metrica || null; }, }); /** * Verificar alertas (internal) */ export const verificarAlertasInternal = internalMutation({ args: { metricId: v.id("systemMetrics"), }, returns: v.null(), handler: async (ctx, args) => { const metrica = await ctx.db.get(args.metricId); if (!metrica) return null; // Buscar configurações de alerta ativas const alertasAtivos = await ctx.db .query("alertConfigurations") .withIndex("by_enabled", (q) => q.eq("enabled", true)) .collect(); for (const alerta of alertasAtivos) { // Obter valor da métrica correspondente const metricValue = (metrica as Record)[alerta.metricName]; if (metricValue === undefined) continue; // Verificar se o alerta deve ser disparado let shouldTrigger = false; switch (alerta.operator) { case ">": shouldTrigger = metricValue > alerta.threshold; break; case "<": shouldTrigger = metricValue < alerta.threshold; break; case ">=": shouldTrigger = metricValue >= alerta.threshold; break; case "<=": shouldTrigger = metricValue <= alerta.threshold; break; case "==": shouldTrigger = metricValue === alerta.threshold; break; } if (shouldTrigger) { // Verificar se já existe um alerta triggered recente (últimos 5 minutos) const cincoMinutosAtras = Date.now() - 5 * 60 * 1000; const alertaRecente = await ctx.db .query("alertHistory") .withIndex("by_config", (q) => q.eq("configId", alerta._id).gte("timestamp", cincoMinutosAtras) ) .filter((q) => q.eq(q.field("status"), "triggered")) .first(); // Se já existe alerta recente, não disparar novamente if (alertaRecente) continue; // Registrar alerta no histórico await ctx.db.insert("alertHistory", { configId: alerta._id, metricName: alerta.metricName, metricValue, threshold: alerta.threshold, timestamp: Date.now(), status: "triggered", notificationsSent: { email: alerta.notifyByEmail, chat: alerta.notifyByChat, }, }); // Criar notificação no chat se configurado if (alerta.notifyByChat) { // Buscar usuários TI para notificar const usuarios = await ctx.db.query("usuarios").collect(); const usuariosTI = usuarios.filter( (u) => u.role?.nome === "ti" || u.role?.nivel === 0 ); for (const usuario of usuariosTI) { await ctx.db.insert("notificacoes", { usuarioId: usuario._id, tipo: "nova_mensagem", titulo: `⚠️ Alerta de Sistema: ${alerta.metricName}`, descricao: `Métrica ${alerta.metricName} está em ${metricValue.toFixed(2)}% (limite: ${alerta.threshold}%)`, lida: false, criadaEm: Date.now(), }); } } // TODO: Enviar email se configurado (integração com sistema de email) // if (alerta.notifyByEmail) { // await enviarEmailAlerta(alerta, metricValue); // } } } return null; }, }); /** * Gerar relatório de métricas */ export const gerarRelatorio = query({ args: { dataInicio: v.number(), dataFim: v.number(), metricNames: v.optional(v.array(v.string())), }, returns: v.object({ periodo: v.object({ inicio: v.number(), fim: v.number(), }), metricas: v.array( v.object({ _id: v.id("systemMetrics"), timestamp: v.number(), cpuUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()), networkLatency: v.optional(v.number()), storageUsed: v.optional(v.number()), usuariosOnline: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()), errosCount: v.optional(v.number()), }) ), estatisticas: v.object({ cpuUsage: v.optional(v.object({ min: v.number(), max: v.number(), avg: v.number(), })), memoryUsage: v.optional(v.object({ min: v.number(), max: v.number(), avg: v.number(), })), networkLatency: v.optional(v.object({ min: v.number(), max: v.number(), avg: v.number(), })), storageUsed: v.optional(v.object({ min: v.number(), max: v.number(), avg: v.number(), })), usuariosOnline: v.optional(v.object({ min: v.number(), max: v.number(), avg: v.number(), })), mensagensPorMinuto: v.optional(v.object({ min: v.number(), max: v.number(), avg: v.number(), })), tempoRespostaMedio: v.optional(v.object({ min: v.number(), max: v.number(), avg: v.number(), })), errosCount: v.optional(v.object({ min: v.number(), max: v.number(), avg: v.number(), })), }), }), handler: async (ctx, args) => { // Buscar métricas no período const metricas = await ctx.db .query("systemMetrics") .withIndex("by_timestamp", (q) => q.gte("timestamp", args.dataInicio).lte("timestamp", args.dataFim) ) .collect(); // Calcular estatísticas const calcularEstatisticas = ( valores: number[] ): { min: number; max: number; avg: number } | undefined => { if (valores.length === 0) return undefined; return { min: Math.min(...valores), max: Math.max(...valores), avg: valores.reduce((a, b) => a + b, 0) / valores.length, }; }; const estatisticas = { cpuUsage: calcularEstatisticas( metricas.map((m) => m.cpuUsage).filter((v) => v !== undefined) as number[] ), memoryUsage: calcularEstatisticas( metricas.map((m) => m.memoryUsage).filter((v) => v !== undefined) as number[] ), networkLatency: calcularEstatisticas( metricas.map((m) => m.networkLatency).filter((v) => v !== undefined) as number[] ), storageUsed: calcularEstatisticas( metricas.map((m) => m.storageUsed).filter((v) => v !== undefined) as number[] ), usuariosOnline: calcularEstatisticas( metricas.map((m) => m.usuariosOnline).filter((v) => v !== undefined) as number[] ), mensagensPorMinuto: calcularEstatisticas( metricas.map((m) => m.mensagensPorMinuto).filter((v) => v !== undefined) as number[] ), tempoRespostaMedio: calcularEstatisticas( metricas.map((m) => m.tempoRespostaMedio).filter((v) => v !== undefined) as number[] ), errosCount: calcularEstatisticas( metricas.map((m) => m.errosCount).filter((v) => v !== undefined) as number[] ), }; return { periodo: { inicio: args.dataInicio, fim: args.dataFim, }, metricas, estatisticas, }; }, }); /** * Deletar configuração de alerta */ export const deletarAlerta = mutation({ args: { alertId: v.id("alertConfigurations"), }, returns: v.object({ success: v.boolean(), }), handler: async (ctx, args) => { await ctx.db.delete(args.alertId); return { success: true }; }, }); /** * Obter histórico de alertas */ export const obterHistoricoAlertas = query({ args: { limit: v.optional(v.number()), }, returns: v.array( v.object({ _id: v.id("alertHistory"), configId: v.id("alertConfigurations"), metricName: v.string(), metricValue: v.number(), threshold: v.number(), timestamp: v.number(), status: v.union(v.literal("triggered"), v.literal("resolved")), notificationsSent: v.object({ email: v.boolean(), chat: v.boolean(), }), }) ), handler: async (ctx, args) => { const limit = args.limit || 50; const historico = await ctx.db .query("alertHistory") .order("desc") .take(limit); return historico; }, });