850 lines
23 KiB
TypeScript
850 lines
23 KiB
TypeScript
import { v } from 'convex/values';
|
|
import { api, internal } from './_generated/api';
|
|
import type { Id } from './_generated/dataModel';
|
|
import type { QueryCtx } from './_generated/server';
|
|
import { internalMutation, mutation, query } 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;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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<string, unknown>)[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 (admin === true) e filtrar usuários por roleId
|
|
const rolesAdminOuTi = await ctx.db
|
|
.query('roles')
|
|
.filter((q) => q.eq(q.field('admin'), true))
|
|
.collect();
|
|
|
|
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();
|
|
|
|
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 apenas a role TI_MASTER
|
|
const roleTiMaster = await ctx.db
|
|
.query('roles')
|
|
.filter((q) => q.eq(q.field('admin'), true))
|
|
.collect();
|
|
|
|
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();
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
});
|
|
|