-
-
-
-
-
-
Monitoramento SGSE
-
- Sistema de monitoramento técnico em tempo real
-
-
-
-
-
- Voltar
-
-
+
+
+
+
+
+
+
Monitoramento SGSE
+
+ Sistema de monitoramento técnico em tempo real
+
+
+
+
+
+ Voltar
+
+
-
-
+
+
diff --git a/packages/backend/convex/monitoramento.ts b/packages/backend/convex/monitoramento.ts
index f318b7f..3d32bc3 100644
--- a/packages/backend/convex/monitoramento.ts
+++ b/packages/backend/convex/monitoramento.ts
@@ -1,581 +1,736 @@
-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";
+import { v } from 'convex/values';
+import { mutation, query, internalMutation } from './_generated/server';
+import { internal } from './_generated/api';
+import { Id } 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;
+ 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();
+ 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,
- });
+ // 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,
- });
+ // 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();
+ // 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);
- }
+ for (const metrica of metricasAntigas) {
+ await ctx.db.delete(metrica._id);
+ }
- return {
- success: true,
- metricId,
- };
- },
+ 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");
- }
+ 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">;
+ 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(),
- });
- }
+ 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,
- };
- },
+ 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;
- },
+ 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();
- }
+ 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);
- }
+ // Limitar resultados
+ if (args.limit !== undefined && args.limit > 0) {
+ metricas = metricas.slice(0, args.limit);
+ }
- return metricas;
- },
+ 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);
+ 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;
- return metricas;
- },
+ 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();
+ 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;
- },
+ 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;
+ 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();
+ // 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
)[alerta.metricName];
- if (typeof rawValue !== "number") continue;
- const metricValue = rawValue;
+ for (const alerta of alertasAtivos) {
+ // Obter valor da métrica correspondente, validando tipo número
+ const rawValue = (metrica as Record)[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;
- }
+ // 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();
+ 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;
+ // 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,
- },
- });
+ // 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();
+ // 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 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));
+ 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 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);
- // }
- }
- }
+ // TODO: Enviar email se configurado (integração com sistema de email)
+ // if (alerta.notifyByEmail) {
+ // await enviarEmailAlerta(alerta, metricValue);
+ // }
+ }
+ }
- return null;
- },
+ 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();
+ 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,
- };
- };
+ // 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[]
- ),
- };
+ 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,
- };
- },
+ 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 };
- },
+ 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);
+ 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;
- return historico;
- },
+ const historico = await ctx.db.query('alertHistory').order('desc').take(limit);
+
+ return historico;
+ }
+});
+
+/**
+ * Status consolidado do sistema para o dashboard
+ */
+export const getStatusSistema = query({
+ args: {},
+ returns: v.object({
+ usuariosOnline: v.number(),
+ totalRegistros: v.number(),
+ tempoMedioResposta: v.number(),
+ cpuUsada: v.number(),
+ memoriaUsada: v.number(),
+ ultimaAtualizacao: v.number()
+ }),
+ handler: async (ctx) => {
+ // Última métrica, 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;
+ }
+
+ // Total de registros (estimativa baseada em tabelas principais)
+ const [usuarios, funcionarios, simbolos, solicitacoesAcesso, alertas, metricas] =
+ await Promise.all([
+ ctx.db.query('usuarios').collect(),
+ ctx.db.query('funcionarios').collect(),
+ ctx.db.query('simbolos').collect(),
+ ctx.db.query('solicitacoesAcesso').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 +
+ solicitacoesAcesso.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
+ };
+ }
+});
+
+/**
+ * Atividade do banco no último minuto (agregada em buckets)
+ * Usa mensagensPorMinuto como proxy de atividade quando disponível.
+ */
+export const getAtividadeBancoDados = query({
+ args: {},
+ returns: v.object({
+ historico: v.array(
+ v.object({
+ entradas: v.number(),
+ saidas: v.number()
+ })
+ )
+ }),
+ handler: async (ctx) => {
+ const agora = Date.now();
+ const haUmMinuto = agora - 60 * 1000;
+
+ 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 }> = [];
+
+ for (let i = 0; i < numBuckets; i++) {
+ const inicio = haUmMinuto + i * bucketSizeMs;
+ const fim = inicio + bucketSizeMs;
+ const bucketMetricas = metricasRecentes.filter(
+ (m) => m.timestamp >= inicio && m.timestamp < fim
+ );
+
+ // Usar mensagensPorMinuto como proxy de "entradas"; "saídas" como fração
+ const somaMensagens =
+ bucketMetricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0) || 0;
+ const entradas = Math.max(0, Math.round(somaMensagens));
+ const saidas = Math.max(0, Math.round(entradas * 0.6));
+
+ historico.push({ entradas, saidas });
+ }
+
+ return { historico };
+ }
+});
+
+/**
+ * Distribuição de operações (estimada a partir das métricas)
+ */
+export const getDistribuicaoRequisicoes = query({
+ args: {},
+ returns: v.object({
+ queries: v.number(),
+ mutations: v.number(),
+ leituras: v.number(),
+ escritas: 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);
+
+ const totalOps = Math.max(
+ 0,
+ Math.round(metricas.reduce((acc, m) => acc + (m.mensagensPorMinuto ?? 0), 0))
+ );
+
+ const queries = Math.round(totalOps * 0.7);
+ const mutations = Math.max(0, totalOps - queries);
+ const leituras = queries;
+ const escritas = mutations;
+
+ return { queries, mutations, leituras, escritas };
+ }
});