import { action, internalMutation, internalAction, internalQuery, query } from './_generated/server'; import { v } from 'convex/values'; import { api, internal } from './_generated/api'; import type { Id, Doc } 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 // Anotação explícita de tipo para evitar problemas de tipagem circular ao // chamar uma função registrada neste mesmo módulo (ver docs do Convex). const erroId: Id<'errosServidor'> = 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: Doc<'roles'>[] = 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: Doc<'roles'>) => r._id)); const usuarios: Doc<'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: Doc<'usuarios'>) => 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 = `
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