feat: enhance push notification management and error handling

- Implemented error handling for unhandled promise rejections related to message channels, improving stability during push notification operations.
- Updated the PushNotificationManager component to manage push subscription registration with timeouts, preventing application hangs.
- Enhanced the sidebar and chat components to display user avatars, improving user experience and visual consistency.
- Refactored email processing logic to support scheduled email sending, integrating new backend functionalities for better email management.
- Improved overall error handling and logging across components to reduce console spam and enhance debugging capabilities.
This commit is contained in:
2025-11-05 06:14:52 -03:00
parent f6671e0f16
commit aa3e3470cd
20 changed files with 2515 additions and 1665 deletions

View File

@@ -2,6 +2,7 @@ import { v } from "convex/values";
import { mutation, query, internalMutation, internalQuery, action } from "./_generated/server";
import { internal, api } from "./_generated/api";
import { renderizarTemplate } from "./templatesMensagens";
import type { Doc, Id } from "./_generated/dataModel";
// ========== INTERNAL QUERIES ==========
@@ -116,8 +117,14 @@ export const enfileirarEmail = mutation({
corpo: v.string(),
templateId: v.optional(v.id("templatesMensagens")),
enviadoPor: v.id("usuarios"), // Obrigatório conforme schema
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
},
handler: async (ctx, args) => {
// Validar agendamento se fornecido
if (args.agendadaPara !== undefined && args.agendadaPara <= Date.now()) {
throw new Error("Data de agendamento deve ser futura");
}
const emailId = await ctx.db.insert("notificacoesEmail", {
destinatario: args.destinatario,
destinatarioId: args.destinatarioId,
@@ -128,8 +135,13 @@ export const enfileirarEmail = mutation({
tentativas: 0,
criadoEm: Date.now(),
enviadoPor: args.enviadoPor,
agendadaPara: args.agendadaPara,
});
// O cron job processará emails automaticamente:
// - Emails sem agendamento serão processados imediatamente (próxima execução do cron)
// - Emails agendados serão processados quando a hora chegar
return emailId;
},
});
@@ -144,12 +156,16 @@ export const enviarEmailComTemplate = action({
templateCodigo: v.string(),
variaveis: v.optional(v.record(v.string(), v.string())),
enviadoPor: v.id("usuarios"), // Obrigatório conforme schema
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
},
handler: async (ctx, args) => {
handler: async (ctx, args): Promise<Id<"notificacoesEmail">> => {
// Buscar template
const template = await ctx.runQuery(api.templatesMensagens.obterTemplatePorCodigo, {
codigo: args.templateCodigo,
});
const template: Doc<"templatesMensagens"> | null = await ctx.runQuery(
api.templatesMensagens.obterTemplatePorCodigo,
{
codigo: args.templateCodigo,
}
);
if (!template) {
throw new Error(`Template não encontrado: ${args.templateCodigo}`);
@@ -160,16 +176,21 @@ export const enviarEmailComTemplate = action({
const tituloRenderizado = renderizarTemplate(template.titulo, variaveisTemplate);
const corpoRenderizado = renderizarTemplate(template.corpo, variaveisTemplate);
// Enfileirar email
const emailId = await ctx.runMutation(api.email.enfileirarEmail, {
// Enfileirar email via mutation
const emailId: Id<"notificacoesEmail"> = await ctx.runMutation(api.email.enfileirarEmail, {
destinatario: args.destinatario,
destinatarioId: args.destinatarioId,
assunto: tituloRenderizado,
corpo: corpoRenderizado,
templateId: template._id,
templateId: template._id, // template._id sempre existe se template não é null
enviadoPor: args.enviadoPor,
agendadaPara: args.agendadaPara,
});
if (!emailId) {
throw new Error("Erro ao enfileirar email: ID não retornado");
}
return emailId;
},
});
@@ -182,29 +203,45 @@ export const enviarEmailComTemplate = action({
export const processarFilaEmails = internalMutation({
args: {},
handler: async (ctx) => {
// Buscar emails pendentes (limitado a 10 por vez para não sobrecarregar)
const emailsPendentes = await ctx.db
const agora = Date.now();
// Buscar emails pendentes que devem ser processados agora
// (sem agendamento OU com agendamento que já passou)
const emailsParaProcessar = await ctx.db
.query("notificacoesEmail")
.filter((q) => q.eq(q.field("status"), "pendente"))
.filter((q) => {
const statusPendente = q.eq(q.field("status"), "pendente");
const semAgendamento = q.eq(q.field("agendadaPara"), undefined);
const agendamentoJaPassou = q.and(
q.neq(q.field("agendadaPara"), undefined),
q.lte(q.field("agendadaPara"), agora)
);
return q.and(
statusPendente,
q.or(semAgendamento, agendamentoJaPassou)
);
})
.order("asc") // Mais antigos primeiro
.take(10);
if (emailsPendentes.length === 0) {
if (emailsParaProcessar.length === 0) {
return { processados: 0 };
}
// Agendar envio de cada email via action
for (const email of emailsPendentes) {
for (const email of emailsParaProcessar) {
// Agendar envio assíncrono (não bloqueia o cron)
ctx.scheduler.runAfter(0, api.actions.email.enviar, {
emailId: email._id,
}).catch((error) => {
console.error(`Erro ao agendar envio de email ${email._id}:`, error);
}).catch((error: unknown) => {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Erro ao agendar envio de email ${email._id}:`, errorMessage);
});
}
return { processados: emailsPendentes.length };
},
return { processados: emailsParaProcessar.length };
}
});
// ========== QUERIES ==========
@@ -221,6 +258,7 @@ export const listarFilaEmails = query({
v.literal("enviado"),
v.literal("falha")
)),
_refresh: v.optional(v.number()), // Parâmetro ignorado, usado apenas para forçar refresh no frontend
},
handler: async (ctx, args) => {
let emails;
@@ -247,7 +285,9 @@ export const listarFilaEmails = query({
* Obter estatísticas da fila de emails (para debug e monitoramento)
*/
export const obterEstatisticasFilaEmails = query({
args: {},
args: {
_refresh: v.optional(v.number()), // Parâmetro ignorado, usado apenas para forçar refresh no frontend
},
returns: v.object({
pendentes: v.number(),
enviando: v.number(),
@@ -324,14 +364,15 @@ export const processarFilaEmailsManual = action({
emailId: email._id,
});
processados++;
} catch (error) {
console.error(`Erro ao agendar envio de email ${email._id}:`, error);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Erro ao agendar envio de email ${email._id}:`, errorMessage);
falhas++;
}
}
return { sucesso: true, processados, falhas };
} catch (error) {
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
sucesso: false,