Files
sgse-app/packages/backend/convex/monitoramento.ts
deyvisonwanderley 0d011b8f42 refactor: enhance role management UI and integrate profile management features
- Introduced a modal for managing user profiles, allowing for the creation and editing of profiles with improved state management.
- Updated the role filtering logic to enhance type safety and readability.
- Refactored UI components for better user experience, including improved button states and loading indicators.
- Removed outdated code related to permissions and streamlined the overall structure for maintainability.
2025-11-03 15:14:33 -03:00

582 lines
16 KiB
TypeScript

import { v } from "convex/values";
import { mutation, query, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { Id, Doc } from "./_generated/dataModel";
import type { QueryCtx } 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: {},
returns: v.array(
v.object({
_id: 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(),
createdBy: v.id("usuarios"),
lastModified: v.number(),
})
),
handler: async (ctx) => {
const alertas = await ctx.db.query("alertConfigurations").collect();
return alertas;
},
});
/**
* 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 (nível <= 1) e filtrar usuários por roleId
const rolesAdminOuTi = await ctx.db
.query("roles")
.filter((q) => q.lte(q.field("nivel"), 1))
.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));
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(),
});
}
}
// TODO: Enviar email se configurado (integração com sistema de email)
// if (alerta.notifyByEmail) {
// await enviarEmailAlerta(alerta, metricValue);
// }
}
}
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;
},
});