import { v } from 'convex/values'; import { api, internal } from './_generated/api'; import type { Id } from './_generated/dataModel'; import type { QueryCtx } from './_generated/server'; import { internalMutation, mutation, query } 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: {}, handler: async (ctx) => { const alertas = await ctx.db.query('alertConfigurations').collect(); return alertas; } }); /** * Verificar configuração do sistema de alertas (diagnóstico) */ export const verificarConfiguracaoAlertas = query({ args: { _refresh: v.optional(v.number()) // Parâmetro ignorado, usado apenas para forçar refresh no frontend }, returns: v.object({ templateExiste: v.boolean(), templateInfo: v.union( v.object({ _id: v.id('templatesMensagens'), codigo: v.string(), nome: v.string(), htmlCorpo: v.optional(v.string()) }), v.null() ), todosTemplatesCodigos: v.optional(v.array(v.string())), // Para debug roleTiMasterExiste: v.boolean(), usuariosTiMaster: v.array( v.object({ _id: v.id('usuarios'), nome: v.string(), email: v.optional(v.string()), temEmail: v.boolean() }) ), configSmtpAtiva: v.boolean(), configSmtpInfo: v.union( v.object({ _id: v.id('configuracaoEmail'), servidor: v.string(), porta: v.number(), emailRemetente: v.string(), ativo: v.boolean() }), v.null() ), emailsPendentes: v.number(), emailsFalha: v.number(), alertasAtivos: v.number(), alertasComEmail: v.number() }), handler: async (ctx) => { try { // 1. Verificar template let template = null; try { // Tentar buscar usando índice template = await ctx.db .query('templatesMensagens') .withIndex('by_codigo', (q) => q.eq('codigo', 'monitoramento_alerta_sistema')) .first(); // Se não encontrou com índice, tentar busca direta if (!template) { const todosTemplates = await ctx.db.query('templatesMensagens').collect(); template = todosTemplates.find( (t) => t.codigo?.toLowerCase() === 'monitoramento_alerta_sistema'.toLowerCase() ) || null; } if (template) { console.log('✅ Template encontrado:', template.codigo, template.nome); } else { console.warn('⚠️ Template monitoramento_alerta_sistema não encontrado no banco'); } } catch (error) { console.error('❌ Erro ao buscar template:', error); // Tentar busca alternativa sem índice try { const todosTemplates = await ctx.db.query('templatesMensagens').collect(); template = todosTemplates.find( (t) => t.codigo?.toLowerCase() === 'monitoramento_alerta_sistema'.toLowerCase() ) || null; } catch (fallbackError) { console.error('❌ Erro na busca alternativa:', fallbackError); } } // 2. Verificar role TI_MASTER let roleTiMaster = null; try { roleTiMaster = await ctx.db .query('roles') .withIndex('by_nome', (q) => q.eq('nome', 'ti_master')) .first(); } catch (error) { console.warn('Erro ao buscar role TI_MASTER:', error); } // 3. Verificar usuários TI_MASTER let usuariosTiMaster: Array<{ _id: Id<'usuarios'>; nome: string; email?: string; temEmail: boolean; }> = []; if (roleTiMaster) { try { const usuarios = await ctx.db .query('usuarios') .withIndex('by_role', (q) => q.eq('roleId', roleTiMaster!._id)) .collect(); usuariosTiMaster = usuarios.map((u) => ({ _id: u._id, nome: u.nome, email: u.email, temEmail: !!u.email })); } catch (error) { console.warn('Erro ao buscar usuários TI_MASTER:', error); } } // 4. Verificar configuração SMTP let configSmtp = null; try { configSmtp = await ctx.db .query('configuracaoEmail') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); } catch (error) { console.warn('Erro ao buscar configuração SMTP:', error); } // 5. Listar todos os templates para debug (opcional) let todosTemplatesCodigos: string[] = []; try { const todosTemplates = await ctx.db.query('templatesMensagens').collect(); todosTemplatesCodigos = todosTemplates.map((t) => t.codigo || '').filter(Boolean); console.log('📋 Templates encontrados no banco:', todosTemplatesCodigos); } catch (error) { console.warn('Erro ao listar templates para debug:', error); } // 6. Verificar fila de emails let emailsPendentes = 0; let emailsFalha = 0; try { const todosEmails = await ctx.db.query('notificacoesEmail').collect(); emailsPendentes = todosEmails.filter((e) => e.status === 'pendente').length; emailsFalha = todosEmails.filter((e) => e.status === 'falha').length; } catch (error) { console.warn('Erro ao buscar emails:', error); } // 7. Verificar alertas let alertasAtivos = 0; let alertasComEmail = 0; try { const todosAlertas = await ctx.db.query('alertConfigurations').collect(); alertasAtivos = todosAlertas.filter((a) => a.enabled).length; alertasComEmail = todosAlertas.filter((a) => a.enabled && a.notifyByEmail).length; } catch (error) { console.warn('Erro ao buscar alertas:', error); } return { templateExiste: !!template, templateInfo: template ? { _id: template._id, codigo: template.codigo, nome: template.nome, htmlCorpo: template.htmlCorpo } : null, todosTemplatesCodigos: todosTemplatesCodigos.length > 0 ? todosTemplatesCodigos : undefined, roleTiMasterExiste: !!roleTiMaster, usuariosTiMaster, configSmtpAtiva: !!configSmtp, configSmtpInfo: configSmtp ? { _id: configSmtp._id, servidor: configSmtp.servidor, porta: configSmtp.porta, emailRemetente: configSmtp.emailRemetente, ativo: configSmtp.ativo } : null, emailsPendentes, emailsFalha, alertasAtivos, alertasComEmail }; } catch (error) { console.error('Erro ao verificar configuração de alertas:', error); // Retornar valores padrão em caso de erro return { templateExiste: false, templateInfo: null, todosTemplatesCodigos: undefined, roleTiMasterExiste: false, usuariosTiMaster: [], configSmtpAtiva: false, configSmtpInfo: null, emailsPendentes: 0, emailsFalha: 0, alertasAtivos: 0, alertasComEmail: 0 }; } } }); /** * 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) => { // Construir consulta respeitando tipos sem reatribuições let metricas; if (args.dataInicio !== undefined && args.dataFim !== undefined) { const inicio: number = args.dataInicio as number; const fim: number = args.dataFim as number; metricas = await ctx.db .query('systemMetrics') .withIndex('by_timestamp', (q) => q.gte('timestamp', inicio).lte('timestamp', fim)) .order('desc') .collect(); } else if (args.dataInicio !== undefined) { const inicio: number = args.dataInicio as number; metricas = await ctx.db .query('systemMetrics') .withIndex('by_timestamp', (q) => q.gte('timestamp', inicio)) .order('desc') .collect(); } else if (args.dataFim !== undefined) { const fim: number = args.dataFim as number; metricas = await ctx.db .query('systemMetrics') .withIndex('by_timestamp', (q) => q.lte('timestamp', fim)) .order('desc') .collect(); } else { metricas = await ctx.db.query('systemMetrics').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, validando tipo número const rawValue = (metrica as Record)[alerta.metricName]; if (typeof rawValue !== 'number') continue; const metricValue = rawValue; // 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 roles administrativas (admin === true) e filtrar usuários por roleId const rolesAdminOuTi = await ctx.db .query('roles') .filter((q) => q.eq(q.field('admin'), true)) .collect(); if (!rolesAdminOuTi) { console.warn('Role TI_MASTER não encontrada. Notificações de chat não serão enviadas.'); } else { // Buscar usuários com role TI_MASTER const usuarios = await ctx.db .query('usuarios') .withIndex('by_role', (q) => q.eq('roleId', rolesAdminOuTi[0]._id)) .collect(); for (const usuario of usuarios) { 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() }); } } } // Enviar email se configurado (usar template HTML padronizado) if (alerta.notifyByEmail) { // Buscar apenas a role TI_MASTER const roleTiMaster = await ctx.db .query('roles') .filter((q) => q.eq(q.field('admin'), true)) .collect(); if (!roleTiMaster) { console.warn( '⚠️ [Monitoramento] Role TI_MASTER não encontrada. Emails de alerta não serão enviados.' ); } else { // Buscar usuários com role TI_MASTER que possuem email const usuarios = await ctx.db .query('usuarios') .withIndex('by_role', (q) => q.eq('roleId', roleTiMaster[0]._id)) .collect(); if (usuarios.length === 0) { console.warn( '⚠️ [Monitoramento] Nenhum usuário TI_MASTER encontrado para receber alertas por email.' ); } else { // Usar o createdBy do alerta como enviadoPor (quem criou o alerta) const enviadoPorId = alerta.createdBy; for (const usuario of usuarios) { const email = usuario.email; if (!email) { console.warn( `⚠️ [Monitoramento] Usuário ${usuario._id} (TI_MASTER) não possui email cadastrado.` ); continue; } // Montar variáveis para template de alerta de sistema const variaveisEmail = { destinatarioNome: usuario.nome, metricName: alerta.metricName, metricValue: metricValue.toFixed(2), threshold: alerta.threshold.toString() }; try { // Importante: usar api.email.enviarEmailComTemplate (action pública), // e não internal.email, para corresponder à tipagem gerada em ./_generated/api. await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: email, destinatarioId: usuario._id, templateCodigo: 'monitoramento_alerta_sistema', variaveis: variaveisEmail, enviadoPor: enviadoPorId // ✅ CORRIGIDO: usar createdBy do alerta }); console.log( `✅ [Monitoramento] Email de alerta agendado para ${email} (${usuario.nome})` ); } catch (error) { console.error( `❌ [Monitoramento] Erro ao agendar email de alerta para ${email}:`, error ); // Continuar tentando enviar para outros usuários mesmo se um falhar } } } } } } } 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; } });