Merge remote-tracking branch 'origin' into feat-pedidos

This commit is contained in:
2025-12-11 10:08:12 -03:00
194 changed files with 30374 additions and 10247 deletions

View File

@@ -145,6 +145,217 @@ export const listarAlertas = query({
}
});
/**
* 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
*/
@@ -346,56 +557,94 @@ export const verificarAlertasInternal = internalMutation({
.filter((q) => q.eq(q.field('admin'), true))
.collect();
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
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();
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()
});
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 usuários administradores/TI para receber o alerta por email
const rolesAdminOuTi = await ctx.db
// Buscar apenas a role TI_MASTER
const roleTiMaster = await ctx.db
.query('roles')
.filter((q) => q.eq(q.field('admin'), true))
.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);
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();
for (const usuario of usuariosTI) {
const email = usuario.email;
if (!email) continue;
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;
// 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()
};
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;
}
// 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: usuario._id
});
// 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
}
}
}
}
}
}
@@ -612,49 +861,62 @@ export const getStatusSistema = query({
ultimaAtualizacao: v.number()
}),
handler: async (ctx) => {
// Última métrica, se existir
const ultimaMetrica = (await ctx.db.query('systemMetrics').order('desc').first()) ?? null;
try {
// Últimatrica, 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;
// 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
};
} catch (error) {
console.error('Erro em getStatusSistema:', error);
// Retornar valores padrão em caso de erro
return {
usuariosOnline: 0,
totalRegistros: 0,
tempoMedioResposta: 0,
cpuUsada: 0,
memoriaUsada: 0,
ultimaAtualizacao: Date.now()
};
}
// 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
};
}
});
@@ -673,60 +935,66 @@ export const getAtividadeBancoDados = query({
)
}),
handler: async (ctx) => {
const agora = Date.now();
const haUmMinuto = agora - 60 * 1000;
try {
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 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();
// 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 }> = [];
// 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;
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 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;
// 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;
// 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));
// 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 });
historico.push({ entradas, saidas });
}
return { historico };
} catch (error) {
console.error('Erro em getAtividadeBancoDados:', error);
// Retornar histórico vazio em caso de erro
return { historico: Array(30).fill({ entradas: 0, saidas: 0 }) };
}
return { historico };
}
});
@@ -742,55 +1010,61 @@ export const getDistribuicaoRequisicoes = query({
escritas: v.number()
}),
handler: async (ctx) => {
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
try {
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 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);
// 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 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;
// 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))
);
// 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);
// 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);
// Mutations são escritas + parte das mensagens (como envio de mensagens)
const mutations = escritas + Math.round(totalMensagens * 0.3);
return { queries, mutations, leituras, escritas };
return { queries, mutations, leituras, escritas };
} catch (error) {
console.error('Erro em getDistribuicaoRequisicoes:', error);
// Retornar valores padrão em caso de erro
return { queries: 0, mutations: 0, leituras: 0, escritas: 0 };
}
}
});