From 4ed90d380df0ffd4eed236ba8200665b89cc67a0 Mon Sep 17 00:00:00 2001 From: killer-cf Date: Sat, 8 Nov 2025 18:30:27 -0300 Subject: [PATCH] refactor: improve layout and backend monitoring functionality - Streamlined the layout component in Svelte for better readability and consistency. - Enhanced the backend monitoring functions by updating argument structures and improving code clarity. - Added new query functions for system status and database activity, providing better insights into system performance. - Cleaned up existing code to ensure maintainability and improved error handling across various functions. --- .../components/ti/ReportGeneratorModal.svelte | 912 ++--- .../ti/SystemMonitorCardLocal.svelte | 3094 ++++++++--------- .../web/src/routes/(dashboard)/+layout.svelte | 103 +- .../(dashboard)/ti/monitoramento/+page.svelte | 99 +- packages/backend/convex/monitoramento.ts | 1129 +++--- 5 files changed, 2737 insertions(+), 2600 deletions(-) diff --git a/apps/web/src/lib/components/ti/ReportGeneratorModal.svelte b/apps/web/src/lib/components/ti/ReportGeneratorModal.svelte index 634840f..bf566b2 100644 --- a/apps/web/src/lib/components/ti/ReportGeneratorModal.svelte +++ b/apps/web/src/lib/components/ti/ReportGeneratorModal.svelte @@ -1,503 +1,509 @@ - + + + diff --git a/apps/web/src/lib/components/ti/SystemMonitorCardLocal.svelte b/apps/web/src/lib/components/ti/SystemMonitorCardLocal.svelte index 6afd867..766e9ae 100644 --- a/apps/web/src/lib/components/ti/SystemMonitorCardLocal.svelte +++ b/apps/web/src/lib/components/ti/SystemMonitorCardLocal.svelte @@ -1,1651 +1,1637 @@ -
-
- -
-
-
-
- Tempo Real - Atualização a cada 2s -
-
Modo Local
-
-
- - -
-
+
+
+ +
+
+
+
+ Tempo Real - Atualização a cada 2s +
+
Modo Local
+
+
+ + +
+
- -
- -
-
- - - -
-
CPU
-
- {formatValue(metrics.cpuUsage)} -
-
-
- {metrics.cpuUsage < 60 - ? "Normal" - : metrics.cpuUsage < 80 - ? "Atenção" - : "Crítico"} -
-
- -
+ +
+ +
+
+ + + +
+
CPU
+
+ {formatValue(metrics.cpuUsage)} +
+
+
+ {metrics.cpuUsage < 60 ? 'Normal' : metrics.cpuUsage < 80 ? 'Atenção' : 'Crítico'} +
+
+ +
- -
-
- - - -
-
Memória RAM
-
- {formatValue(metrics.memoryUsage)} -
-
-
- {metrics.memoryUsage < 60 - ? "Normal" - : metrics.memoryUsage < 80 - ? "Atenção" - : "Crítico"} -
-
- -
+ +
+
+ + + +
+
Memória RAM
+
+ {formatValue(metrics.memoryUsage)} +
+
+
+ {metrics.memoryUsage < 60 ? 'Normal' : metrics.memoryUsage < 80 ? 'Atenção' : 'Crítico'} +
+
+ +
- -
-
- - - -
-
Latência de Rede
-
- {formatValue(metrics.networkLatency, "ms")} -
-
-
- {metrics.networkLatency < 100 - ? "Excelente" - : metrics.networkLatency < 500 - ? "Boa" - : "Lenta"} -
-
- -
+ +
+
+ + + +
+
Latência de Rede
+
+ {formatValue(metrics.networkLatency, 'ms')} +
+
+
+ {metrics.networkLatency < 100 + ? 'Excelente' + : metrics.networkLatency < 500 + ? 'Boa' + : 'Lenta'} +
+
+ +
- -
-
- - - -
-
Armazenamento
-
- {formatValue(metrics.storageUsed)} -
-
-
- {metrics.storageUsed < 60 - ? "Normal" - : metrics.storageUsed < 80 - ? "Atenção" - : "Crítico"} -
-
- -
+ +
+
+ + + +
+
Armazenamento
+
+ {formatValue(metrics.storageUsed)} +
+
+
+ {metrics.storageUsed < 60 ? 'Normal' : metrics.storageUsed < 80 ? 'Atenção' : 'Crítico'} +
+
+ +
- -
-
- - - -
-
Usuários Online
-
- {metrics.usuariosOnline} -
-
-
Tempo Real
-
-
+ +
+
+ + + +
+
Usuários Online
+
+ {metrics.usuariosOnline} +
+
+
Tempo Real
+
+
- -
-
- - - -
-
Mensagens/min
-
- {metrics.mensagensPorMinuto} -
-
-
Atividade
-
-
+ +
+
+ + + +
+
Mensagens/min
+
+ {metrics.mensagensPorMinuto} +
+
+
Atividade
+
+
- -
-
- - - -
-
Tempo Resposta
-
- {formatValue(metrics.tempoRespostaMedio, "ms")} -
-
-
- {metrics.tempoRespostaMedio < 100 - ? "Rápido" - : metrics.tempoRespostaMedio < 500 - ? "Normal" - : "Lento"} -
-
-
+ +
+
+ + + +
+
Tempo Resposta
+
+ {formatValue(metrics.tempoRespostaMedio, 'ms')} +
+
+
+ {metrics.tempoRespostaMedio < 100 + ? 'Rápido' + : metrics.tempoRespostaMedio < 500 + ? 'Normal' + : 'Lento'} +
+
+
- -
-
- - - -
-
Erros (2s)
-
{metrics.errosCount}
-
-
- {metrics.errosCount === 0 ? "Sem erros" : "Verificar logs"} -
-
-
+ +
+
+ + + +
+
Erros (2s)
+
{metrics.errosCount}
+
+
+ {metrics.errosCount === 0 ? 'Sem erros' : 'Verificar logs'} +
+
+
- -
-
- - - -
-
FPS (Performance)
-
{metrics.fps}
-
-
- {metrics.fps >= 50 - ? "Fluido" - : metrics.fps >= 30 - ? "Moderado" - : "Lento"} -
-
-
+ +
+
+ + + +
+
FPS (Performance)
+
{metrics.fps}
+
+
+ {metrics.fps >= 50 ? 'Fluido' : metrics.fps >= 30 ? 'Moderado' : 'Lento'} +
+
+
- -
-
- - - -
-
Tipo de Conexão
-
- {metrics.networkType} -
-
- {#if metrics.networkSpeed > 0} -
- {metrics.networkSpeed} Mbps -
- {:else} -
N/A
- {/if} -
-
+ +
+
+ + + +
+
Tipo de Conexão
+
+ {metrics.networkType} +
+
+ {#if metrics.networkSpeed > 0} +
+ {metrics.networkSpeed} Mbps +
+ {:else} +
N/A
+ {/if} +
+
- -
-
- - - -
-
Navegador
-
- {metrics.browserName} -
-
-
- v{metrics.browserVersion} -
-
-
+ +
+
+ + + +
+
Navegador
+
+ {metrics.browserName} +
+
+
+ v{metrics.browserVersion} +
+
+
- -
-
- - - -
-
Resolução
-
- {metrics.screenResolution} -
-
-
Tela
-
-
+ +
+
+ + + +
+
Resolução
+
+ {metrics.screenResolution} +
+
+
Tela
+
+
- -
-
- - - -
-
RAM Dispositivo
-
- {metrics.deviceMemory || "N/A"}{metrics.deviceMemory ? "GB" : ""} -
-
-
Hardware
-
-
+ +
+
+ + + +
+
RAM Dispositivo
+
+ {metrics.deviceMemory || 'N/A'}{metrics.deviceMemory ? 'GB' : ''} +
+
+
Hardware
+
+
- -
-
- - - -
-
Núcleos CPU
-
- {metrics.hardwareConcurrency} -
-
-
Threads
-
-
+ +
+
+ + + +
+
Núcleos CPU
+
+ {metrics.hardwareConcurrency} +
+
+
Threads
+
+
- -
-
- - - -
-
Cache Browser
-
{metrics.cacheSize}
-
-
MB
-
-
+ +
+
+ + + +
+
Cache Browser
+
{metrics.cacheSize}
+
+
MB
+
+
- -
-
- - - -
-
Bateria
-
- {metrics.batteryLevel}% -
-
-
- {metrics.batteryCharging ? "⚡ Carregando" : "🔋 Bateria"} -
-
- -
+ +
+
+ + + +
+
Bateria
+
+ {metrics.batteryLevel}% +
+
+
+ {metrics.batteryCharging ? '⚡ Carregando' : '🔋 Bateria'} +
+
+ +
- -
-
- - - -
-
Tempo Ativo
-
- {Math.floor(metrics.uptime / 60)}:{String( - metrics.uptime % 60, - ).padStart(2, "0")} -
-
-
Minutos
-
-
+ +
+
+ + + +
+
Tempo Ativo
+
+ {Math.floor(metrics.uptime / 60)}:{String(metrics.uptime % 60).padStart(2, '0')} +
+
+
Minutos
+
+
- -
-
- - - -
-
HTTP Requests (2s)
-
{metrics.httpRequests}
-
-
Requisições
-
-
+ +
+
+ + + +
+
HTTP Requests (2s)
+
{metrics.httpRequests}
+
+
Requisições
+
+
- -
-
- - - -
-
WebSocket
-
- {metrics.wsConnections} -
-
-
- {metrics.wsStatus === "connected" - ? "🟢 Conectado" - : "🔴 Desconectado"} -
-
-
+ +
+
+ + + +
+
WebSocket
+
+ {metrics.wsConnections} +
+
+
+ {metrics.wsStatus === 'connected' ? '🟢 Conectado' : '🔴 Desconectado'} +
+
+
- -
-
- - - -
-
IndexedDB
-
- {metrics.indexedDBSize} -
-
-
MB
-
-
+ +
+
+ + + +
+
IndexedDB
+
+ {metrics.indexedDBSize} +
+
+
MB
+
+
- -
-
- - - -
-
Service Worker
-
- {metrics.serviceWorkerStatus} -
-
-
- {metrics.serviceWorkerStatus === "active" - ? "✓ Ativo" - : metrics.serviceWorkerStatus === "registered" - ? "○ Registrado" - : "✗ Nenhum"} -
-
-
+ +
+
+ + + +
+
Service Worker
+
+ {metrics.serviceWorkerStatus} +
+
+
+ {metrics.serviceWorkerStatus === 'active' + ? '✓ Ativo' + : metrics.serviceWorkerStatus === 'registered' + ? '○ Registrado' + : '✗ Nenhum'} +
+
+
- -
-
- - - -
-
DOM Nodes
-
{metrics.domNodes}
-
-
Elementos
-
-
+ +
+
+ + + +
+
DOM Nodes
+
{metrics.domNodes}
+
+
Elementos
+
+
- -
-
- - - -
-
JS Heap Size
-
- {metrics.jsHeapSize || "N/A"} -
-
-
- {metrics.jsHeapSize ? "MB" : "Indisponível"} -
-
-
-
+ +
+
+ + + +
+
JS Heap Size
+
+ {metrics.jsHeapSize || 'N/A'} +
+
+
+ {metrics.jsHeapSize ? 'MB' : 'Indisponível'} +
+
+
+
- - {#if metricsHistory.length > 5} -
-
-

- - - - Gráficos Interativos em Tempo Real -

-
+ + {#if metricsHistory.length > 5} +
+
+

+ + + + Gráficos Interativos em Tempo Real +

+
-
- -
-
-

- - - - Recursos do Sistema (Últimos 20 pontos) -

- -
-
+
+ +
+
+

+ + + + Recursos do Sistema (Últimos 20 pontos) +

+ +
+
- -
-
-

- - - - Atividade da Aplicação (Últimos 30 pontos) -

- -
-
+ +
+
+

+ + + + Atividade da Aplicação (Últimos 30 pontos) +

+ +
+
- -
-
-

- - - - - Distribuição de Uso de Recursos -

- -
-
+ +
+
+

+ + + + + Distribuição de Uso de Recursos +

+ +
+
- -
-
-

- - - - Métricas Técnicas Avançadas -

- -
-
-
-
- {:else} -
- - - - Coletando dados para gráficos... Aguarde alguns segundos. -
- {/if} + +
+
+

+ + + + Métricas Técnicas Avançadas +

+ +
+
+
+
+ {:else} +
+ + + + Coletando dados para gráficos... Aguarde alguns segundos. +
+ {/if} - -
- - - -
-

- Monitoramento Ativo (Modo Local) - 23 Métricas Técnicas + 4 Gráficos - Interativos -

-
- Sistema: CPU, RAM, Latência, Storage | - Aplicação: Usuários, Mensagens, Tempo Resposta, - Erros, HTTP Requests | - Performance: FPS, Conexão, Navegador, Tela | - Hardware: RAM Física, Núcleos CPU, Cache, Bateria, - Uptime | - Avançado: WebSocket, IndexedDB, Service Worker, DOM - Nodes, JS Heap -
- Gráficos: Linha (Recursos), Área (Atividade), Donut - (Distribuição), Barras (Métricas) -
- Última atualização: {new Date(metrics.timestamp).toLocaleString( - "pt-BR", - )} -
-
-
-
+ +
+ + + +
+

+ Monitoramento Ativo (Modo Local) - 23 Métricas Técnicas + 4 Gráficos Interativos +

+
+ Sistema: CPU, RAM, Latência, Storage | + Aplicação: Usuários, Mensagens, Tempo Resposta, Erros, HTTP Requests | + Performance: FPS, Conexão, Navegador, Tela | + Hardware: RAM Física, Núcleos CPU, Cache, Bateria, Uptime | + Avançado: WebSocket, IndexedDB, Service Worker, DOM Nodes, JS Heap +
+ Gráficos: Linha (Recursos), Área (Atividade), Donut (Distribuição), + Barras (Métricas) +
+ Última atualização: {new Date(metrics.timestamp).toLocaleString('pt-BR')} +
+
+
+
{#if showAlertModal} - (showAlertModal = false)} /> + (showAlertModal = false)} /> {/if} {#if showReportModal} - (showReportModal = false)} /> + (showReportModal = false)} /> {/if} diff --git a/apps/web/src/routes/(dashboard)/+layout.svelte b/apps/web/src/routes/(dashboard)/+layout.svelte index 0e974de..3e3778a 100644 --- a/apps/web/src/routes/(dashboard)/+layout.svelte +++ b/apps/web/src/routes/(dashboard)/+layout.svelte @@ -1,69 +1,60 @@ {#if routeAction} - -
- {@render children()} -
-
+ +
+ {@render children()} +
+
{:else} -
- {@render children()} -
+
+ {@render children()} +
{/if} diff --git a/apps/web/src/routes/(dashboard)/ti/monitoramento/+page.svelte b/apps/web/src/routes/(dashboard)/ti/monitoramento/+page.svelte index 82ab2b4..274680b 100644 --- a/apps/web/src/routes/(dashboard)/ti/monitoramento/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/monitoramento/+page.svelte @@ -1,55 +1,54 @@ -
- -
-
-
- - - -
-
-

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 }; + } });