import { v } from 'convex/values'; import { mutation, query, internalMutation } from './_generated/server'; import { internal } from './_generated/api'; import { Id } 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: {}, 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) => { // 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 (nível <= 1) e filtrar usuários por roleId const rolesAdminOuTi = await ctx.db .query('roles') .filter((q) => q.lte(q.field('nivel'), 1)) .collect(); const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id)); const usuarios = await ctx.db.query('usuarios').collect(); const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId)); 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() }); } } // Enviar email se configurado (usar template HTML padronizado) if (alerta.notifyByEmail) { // Buscar usuários administradores/TI para receber o alerta por email const rolesAdminOuTi = await ctx.db .query('roles') .filter((q) => q.lte(q.field('nivel'), 1)) .collect(); const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id)); const usuarios = await ctx.db.query('usuarios').collect(); const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId) && !!u.email); for (const usuario of usuariosTI) { const email = usuario.email; if (!email) 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() }; await ctx.scheduler.runAfter(0, internal.email.enviarEmailComTemplate, { destinatario: email, destinatarioId: usuario._id, templateCodigo: 'monitoramento_alerta_sistema', variaveis: variaveisEmail, enviadoPor: usuario._id }); } } } } 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; } }); /** * Status consolidado do sistema para o dashboard */ export const getStatusSistema = query({ args: {}, returns: v.object({ usuariosOnline: v.number(), totalRegistros: v.number(), tempoMedioResposta: v.number(), cpuUsada: v.number(), memoriaUsada: v.number(), ultimaAtualizacao: v.number() }), handler: async (ctx) => { // Última métrica, se existir const ultimaMetrica = (await ctx.db.query('systemMetrics').order('desc').first()) ?? null; // Usuários online: usar métrica se disponível, senão derivar de usuários let usuariosOnline = 0; if (ultimaMetrica?.usuariosOnline !== undefined) { usuariosOnline = ultimaMetrica.usuariosOnline; } else { const usuarios = await ctx.db.query('usuarios').collect(); usuariosOnline = usuarios.filter((u) => u.statusPresenca === 'online').length; } // Total de registros (estimativa baseada em tabelas principais) const [usuarios, funcionarios, simbolos, alertas, metricas] = await Promise.all([ ctx.db.query('usuarios').collect(), ctx.db.query('funcionarios').collect(), ctx.db.query('simbolos').collect(), ctx.db.query('alertConfigurations').collect(), ctx.db.query('systemMetrics').take(100) // não precisa contar tudo ]); const totalRegistros = usuarios.length + funcionarios.length + simbolos.length + alertas.length + metricas.length; // Métricas de performance com fallbacks seguros const tempoMedioResposta = ultimaMetrica?.tempoRespostaMedio ?? 0; const cpuUsada = Math.max( 0, Math.min(100, Math.round((ultimaMetrica?.cpuUsage ?? 0) * 100) / 100) ); const memoriaUsada = Math.max( 0, Math.min(100, Math.round((ultimaMetrica?.memoryUsage ?? 0) * 100) / 100) ); const ultimaAtualizacao = ultimaMetrica?.timestamp ?? Date.now(); return { usuariosOnline, totalRegistros, tempoMedioResposta, cpuUsada, memoriaUsada, ultimaAtualizacao }; } }); /** * Atividade do banco no último minuto (agregada em buckets) * Usa logsAtividades e systemMetrics para calcular atividade real. */ export const getAtividadeBancoDados = query({ args: {}, returns: v.object({ historico: v.array( v.object({ entradas: v.number(), saidas: v.number() }) ) }), handler: async (ctx) => { const agora = Date.now(); const haUmMinuto = agora - 60 * 1000; // Buscar atividades reais do sistema const atividadesRecentes = await ctx.db .query('logsAtividades') .withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto)) .order('asc') .collect(); // Buscar métricas também (para mensagens se houver) const metricasRecentes = await ctx.db .query('systemMetrics') .withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto)) .order('asc') .collect(); // Bucketizar em 30 pontos (~2s cada) para visualização const numBuckets = 30; const bucketSizeMs = Math.ceil(60_000 / numBuckets); const historico: Array<{ entradas: number; saidas: number }> = []; for (let i = 0; i < numBuckets; i++) { const inicio = haUmMinuto + i * bucketSizeMs; const fim = inicio + bucketSizeMs; // Contar atividades de criação/inserção (entradas) const atividadesBucket = atividadesRecentes.filter( (a) => a.timestamp >= inicio && a.timestamp < fim ); const entradasAtividades = atividadesBucket.filter( a => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar' ).length; // Contar atividades de exclusão/remoção (saídas) const saidasAtividades = atividadesBucket.filter( a => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar' ).length; // Usar mensagensPorMinuto como adicional se disponível const bucketMetricas = metricasRecentes.filter( (m) => m.timestamp >= inicio && m.timestamp < fim ); const somaMensagens = bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0; // Combinar atividades reais com métricas de mensagens const entradas = Math.max(0, Math.round(entradasAtividades + somaMensagens * 0.3)); const saidas = Math.max(0, Math.round(saidasAtividades + somaMensagens * 0.2)); historico.push({ entradas, saidas }); } return { historico }; } }); /** * Distribuição de operações (calculada a partir de logsAtividades e métricas) */ export const getDistribuicaoRequisicoes = query({ args: {}, returns: v.object({ queries: v.number(), mutations: v.number(), leituras: v.number(), escritas: v.number() }), handler: async (ctx) => { const umaHoraAtras = Date.now() - 60 * 60 * 1000; // Buscar atividades reais do sistema const atividades = await ctx.db .query('logsAtividades') .withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras)) .collect(); // Buscar métricas também const metricas = await ctx.db .query('systemMetrics') .withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras)) .order('desc') .take(100); // Contar operações de leitura (consultas, visualizações) const leituras = atividades.filter( a => a.acao === 'consultar' || a.acao === 'visualizar' || a.acao === 'listar' || a.acao === 'buscar' ).length; // Contar operações de escrita (criar, editar, excluir) const escritas = atividades.filter( a => a.acao === 'criar' || a.acao === 'editar' || a.acao === 'excluir' || a.acao === 'inserir' || a.acao === 'atualizar' || a.acao === 'deletar' || a.acao === 'cadastrar' || a.acao === 'remover' ).length; // Adicionar estimativa baseada em mensagens se disponível const totalMensagens = Math.max( 0, Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0)) ); // Queries são leituras + parte das mensagens (como consultas de chat) const queries = leituras + Math.round(totalMensagens * 0.5); // Mutations são escritas + parte das mensagens (como envio de mensagens) const mutations = escritas + Math.round(totalMensagens * 0.3); return { queries, mutations, leituras, escritas }; } });