import { action, internalMutation, internalAction, internalQuery, query } from './_generated/server'; import { v } from 'convex/values'; import { api, internal } from './_generated/api'; import type { Id } from './_generated/dataModel'; import { getCurrentUserFunction } from './auth'; /** * Action pública para registrar erro do servidor e notificar equipe técnica * Esta função será chamada pelo handleError do SvelteKit */ export const registrarErroServidor = action({ args: { statusCode: v.number(), mensagem: v.string(), stack: v.optional(v.string()), url: v.optional(v.string()), method: v.optional(v.string()), ipAddress: v.optional(v.string()), userAgent: v.optional(v.string()), usuarioId: v.optional(v.id('usuarios')) }, handler: async (ctx, args) => { // Registrar erro no banco const erroId = await ctx.runMutation(internal.errosServidor.inserirErro, { statusCode: args.statusCode, mensagem: args.mensagem, stack: args.stack, url: args.url, method: args.method, ipAddress: args.ipAddress, userAgent: args.userAgent, usuarioId: args.usuarioId }); // Notificar equipe técnica (assíncrono) ctx.scheduler .runAfter(0, internal.errosServidor.notificarEquipeTecnica, { erroId }) .catch((error) => { console.error('Erro ao agendar notificação de erro:', error); }); return { sucesso: true, erroId }; } }); /** * Mutation interna para inserir erro no banco */ export const inserirErro = internalMutation({ args: { statusCode: v.number(), mensagem: v.string(), stack: v.optional(v.string()), url: v.optional(v.string()), method: v.optional(v.string()), ipAddress: v.optional(v.string()), userAgent: v.optional(v.string()), usuarioId: v.optional(v.id('usuarios')) }, handler: async (ctx, args) => { const erroId = await ctx.db.insert('errosServidor', { statusCode: args.statusCode, mensagem: args.mensagem, stack: args.stack, url: args.url, method: args.method, ipAddress: args.ipAddress, userAgent: args.userAgent, usuarioId: args.usuarioId, notificado: false, criadoEm: Date.now() }); return erroId; } }); /** * Action interna para notificar equipe técnica sobre erro do servidor */ export const notificarEquipeTecnica = internalAction({ args: { erroId: v.id('errosServidor') }, handler: async (ctx, args) => { // Buscar detalhes do erro const erro = await ctx.runQuery(internal.errosServidor.obterErroPorId, { erroId: args.erroId }); if (!erro) { console.error('Erro não encontrado:', args.erroId); return; } // Buscar usuários da equipe técnica (roles com nível <= 1) const rolesAdminOuTi = await ctx.runQuery(internal.errosServidor.obterRolesTI, {}); if (rolesAdminOuTi.length === 0) { console.warn('Nenhuma role de TI encontrada para notificação de erro'); return; } const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id)); const usuarios = await ctx.runQuery(internal.errosServidor.obterUsuariosTI, { rolesPermitidas: Array.from(rolesPermitidas) }); if (usuarios.length === 0) { console.warn('Nenhum usuário de TI encontrado para notificação de erro'); return; } // Preparar informações do erro para notificação const urlFormatada = erro.url || 'N/A'; const metodoFormatado = erro.method || 'N/A'; const stackFormatado = erro.stack ? erro.stack.substring(0, 500) + (erro.stack.length > 500 ? '...' : '') : 'N/A'; // Notificar via chat (notificações internas) for (const usuario of usuarios) { await ctx.runMutation(internal.errosServidor.criarNotificacaoChat, { usuarioId: usuario._id, statusCode: erro.statusCode, mensagem: erro.mensagem, url: urlFormatada, method: metodoFormatado }); } // Notificar via email (apenas para usuários com email) const usuariosComEmail = usuarios.filter((u) => u.email); for (const usuario of usuariosComEmail) { try { // Determinar código do template baseado no status code const templateCodigo = erro.statusCode === 404 ? 'ERRO_SERVIDOR_404' : 'ERRO_SERVIDOR_500'; // Verificar se existe template de erro do servidor const templateExiste = await ctx.runQuery( api.templatesMensagens.obterTemplatePorCodigo, { codigo: templateCodigo } ); if (templateExiste) { // Usar template personalizado await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: usuario.email!, destinatarioId: usuario._id, templateCodigo, variaveis: { destinatarioNome: usuario.nome, statusCode: erro.statusCode.toString(), mensagem: erro.mensagem, url: urlFormatada, method: metodoFormatado, stack: stackFormatado, timestamp: new Date(erro.criadoEm).toLocaleString('pt-BR') }, enviadoPor: usuario._id // Usar o próprio usuário como remetente }); } else { // Template não existe, criar email simples com HTML básico const assunto = erro.statusCode === 404 ? `⚠️ Erro 404 - Página não encontrada: ${urlFormatada.substring(0, 50)}` : `🚨 Erro ${erro.statusCode} no Servidor - ${urlFormatada.substring(0, 50)}`; const corpo = `

${erro.statusCode === 404 ? 'Página Não Encontrada (404)' : 'Erro do Servidor Detectado'}

Código: ${erro.statusCode}

Mensagem: ${erro.mensagem}

URL: ${urlFormatada}

Método: ${metodoFormatado}

Data/Hora: ${new Date(erro.criadoEm).toLocaleString('pt-BR')}

${erro.stack && erro.statusCode !== 404 ? `

Stack Trace:

${stackFormatado}

` : ''} `; await ctx.runMutation(api.email.enfileirarEmail, { destinatario: usuario.email!, destinatarioId: usuario._id, assunto, corpo, enviadoPor: usuario._id }); } } catch (error) { console.error(`Erro ao enviar email de notificação para ${usuario.email}:`, error); } } // Marcar erro como notificado await ctx.runMutation(internal.errosServidor.marcarComoNotificado, { erroId: args.erroId }); } }); /** * Query interna para obter erro por ID */ export const obterErroPorId = internalQuery({ args: { erroId: v.id('errosServidor') }, handler: async (ctx, args) => { return await ctx.db.get(args.erroId); } }); /** * Query interna para obter roles de TI */ export const obterRolesTI = internalQuery({ args: {}, handler: async (ctx) => { return await ctx.db .query('roles') .filter((q) => q.lte(q.field('nivel'), 1)) .collect(); } }); /** * Query interna para obter usuários de TI */ export const obterUsuariosTI = internalQuery({ args: { rolesPermitidas: v.array(v.id('roles')) }, handler: async (ctx, args) => { const usuarios = await ctx.db.query('usuarios').collect(); return usuarios.filter((u) => args.rolesPermitidas.includes(u.roleId)); } }); /** * Mutation interna para criar notificação no chat */ export const criarNotificacaoChat = internalMutation({ args: { usuarioId: v.id('usuarios'), statusCode: v.number(), mensagem: v.string(), url: v.string(), method: v.string() }, handler: async (ctx, args) => { const tituloNotificacao = args.statusCode === 404 ? `⚠️ Erro 404 - Página não encontrada` : `🚨 Erro ${args.statusCode} no Servidor`; const descricaoNotificacao = args.statusCode === 404 ? `Página não encontrada: ${args.url} (${args.method})` : `Erro detectado em ${args.url} (${args.method}): ${args.mensagem.substring(0, 100)}`; await ctx.db.insert('notificacoes', { usuarioId: args.usuarioId, tipo: 'nova_mensagem', titulo: tituloNotificacao, descricao: descricaoNotificacao, lida: false, criadaEm: Date.now() }); } }); /** * Mutation interna para marcar erro como notificado */ export const marcarComoNotificado = internalMutation({ args: { erroId: v.id('errosServidor') }, handler: async (ctx, args) => { await ctx.db.patch(args.erroId, { notificado: true, notificadoEm: Date.now() }); } }); /** * Query pública para listar erros do servidor (apenas para TI) */ export const listarErros = query({ args: { limite: v.optional(v.number()), statusCode: v.optional(v.number()), notificado: v.optional(v.boolean()), dataInicio: v.optional(v.number()), dataFim: v.optional(v.number()) }, handler: async (ctx, args) => { // Verificar se usuário tem permissão (nível <= 1) const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Não autenticado'); } const role = await ctx.db.get(usuario.roleId); if (!role || role.nivel > 1) { throw new Error('Acesso negado. Apenas usuários de TI podem visualizar erros do servidor.'); } // Construir query com filtros let erros; if (args.statusCode !== undefined) { erros = await ctx.db .query('errosServidor') .withIndex('by_status_code', (q) => q.eq('statusCode', args.statusCode!)) .collect(); } else { erros = await ctx.db .query('errosServidor') .withIndex('by_criado_em') .collect(); } // Aplicar filtros adicionais que não são índices if (args.notificado !== undefined) { erros = erros.filter((e) => e.notificado === args.notificado); } if (args.dataInicio !== undefined) { erros = erros.filter((e) => e.criadoEm >= args.dataInicio!); } if (args.dataFim !== undefined) { erros = erros.filter((e) => e.criadoEm <= args.dataFim!); } // Ordenar por data (mais recentes primeiro) erros.sort((a, b) => b.criadoEm - a.criadoEm); // Aplicar limite const limite = args.limite || 100; erros = erros.slice(0, limite); // Buscar informações do usuário se houver usuarioId const errosComUsuario = await Promise.all( erros.map(async (erro) => { let usuarioNome = null; if (erro.usuarioId) { const usuarioErro = await ctx.db.get(erro.usuarioId); usuarioNome = usuarioErro?.nome || null; } return { ...erro, usuarioNome }; }) ); return errosComUsuario; } }); /** * Query pública para obter estatísticas de erros */ export const obterEstatisticasErros = query({ args: { dataInicio: v.optional(v.number()), dataFim: v.optional(v.number()) }, handler: async (ctx, args) => { // Verificar se usuário tem permissão (nível <= 1) const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Não autenticado'); } const role = await ctx.db.get(usuario.roleId); if (!role || role.nivel > 1) { throw new Error('Acesso negado. Apenas usuários de TI podem visualizar estatísticas de erros.'); } // Buscar todos os erros no período let erros = await ctx.db.query('errosServidor').withIndex('by_criado_em').collect(); // Aplicar filtros de data if (args.dataInicio !== undefined) { erros = erros.filter((e) => e.criadoEm >= args.dataInicio!); } if (args.dataFim !== undefined) { erros = erros.filter((e) => e.criadoEm <= args.dataFim!); } // Calcular estatísticas const total = erros.length; const porStatus = erros.reduce( (acc, erro) => { const status = erro.statusCode.toString(); acc[status] = (acc[status] || 0) + 1; return acc; }, {} as Record ); const notificados = erros.filter((e) => e.notificado).length; const naoNotificados = total - notificados; // Erros mais recentes (últimas 24 horas) const agora = Date.now(); const ultimas24h = erros.filter((e) => agora - e.criadoEm <= 24 * 60 * 60 * 1000).length; return { total, porStatus, notificados, naoNotificados, ultimas24h }; } });