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.
This commit is contained in:
2025-11-08 18:30:27 -03:00
parent 5d76c375c2
commit 4ed90d380d
5 changed files with 2737 additions and 2600 deletions

View File

@@ -1,26 +1,38 @@
<script lang="ts"> <script lang="ts">
import { useConvexClient } from "convex-svelte"; import { useConvexClient } from 'convex-svelte';
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from '@sgse-app/backend/convex/_generated/api';
import { format, subDays, startOfDay, endOfDay } from "date-fns"; import { format, subDays, startOfDay, endOfDay } from 'date-fns';
import { ptBR } from "date-fns/locale"; import { ptBR } from 'date-fns/locale';
import jsPDF from "jspdf"; import jsPDF from 'jspdf';
import autoTable from "jspdf-autotable"; import autoTable from 'jspdf-autotable';
import Papa from "papaparse"; import Papa from 'papaparse';
let { onClose }: { onClose: () => void } = $props(); let { onClose }: { onClose: () => void } = $props();
const client = useConvexClient(); const client = useConvexClient();
// Estados // Estados
let periodType = $state("custom"); let periodType = $state('custom');
let dataInicio = $state(format(subDays(new Date(), 7), "yyyy-MM-dd")); let dataInicio = $state(format(subDays(new Date(), 7), 'yyyy-MM-dd'));
let dataFim = $state(format(new Date(), "yyyy-MM-dd")); let dataFim = $state(format(new Date(), 'yyyy-MM-dd'));
let horaInicio = $state("00:00"); let horaInicio = $state('00:00');
let horaFim = $state("23:59"); let horaFim = $state('23:59');
let generating = $state(false); let generating = $state(false);
// Métricas selecionadas // Métricas selecionadas
let selectedMetrics = $state({ const metricLabels = {
cpuUsage: 'Uso de CPU (%)',
memoryUsage: 'Uso de Memória (%)',
networkLatency: 'Latência de Rede (ms)',
storageUsed: 'Armazenamento (%)',
usuariosOnline: 'Usuários Online',
mensagensPorMinuto: 'Mensagens/min',
tempoRespostaMedio: 'Tempo Resposta (ms)',
errosCount: 'Erros'
} as const;
type MetricKey = keyof typeof metricLabels;
let selectedMetrics = $state<Record<MetricKey, boolean>>({
cpuUsage: true, cpuUsage: true,
memoryUsage: true, memoryUsage: true,
networkLatency: true, networkLatency: true,
@@ -28,44 +40,40 @@
usuariosOnline: true, usuariosOnline: true,
mensagensPorMinuto: true, mensagensPorMinuto: true,
tempoRespostaMedio: true, tempoRespostaMedio: true,
errosCount: true, errosCount: true
}); });
const metricLabels: Record<string, string> = { const metricEntries = $derived(Object.entries(metricLabels) as Array<[MetricKey, string]>);
cpuUsage: "Uso de CPU (%)",
memoryUsage: "Uso de Memória (%)",
networkLatency: "Latência de Rede (ms)",
storageUsed: "Armazenamento (%)",
usuariosOnline: "Usuários Online",
mensagensPorMinuto: "Mensagens/min",
tempoRespostaMedio: "Tempo Resposta (ms)",
errosCount: "Erros",
};
function setPeriod(type: string) { function isMetricSelected(key: MetricKey): boolean {
return selectedMetrics[key];
}
function setMetricSelected(key: MetricKey, value: boolean): void {
selectedMetrics[key] = value;
}
function setPeriod(type: 'today' | 'week' | 'month' | 'custom') {
periodType = type; periodType = type;
const now = new Date(); const now = new Date();
switch (type) { switch (type) {
case "today": case 'today':
dataInicio = format(now, "yyyy-MM-dd"); dataInicio = format(now, 'yyyy-MM-dd');
dataFim = format(now, "yyyy-MM-dd"); dataFim = format(now, 'yyyy-MM-dd');
break; break;
case "week": case 'week':
dataInicio = format(subDays(now, 7), "yyyy-MM-dd"); dataInicio = format(subDays(now, 7), 'yyyy-MM-dd');
dataFim = format(now, "yyyy-MM-dd"); dataFim = format(now, 'yyyy-MM-dd');
break; break;
case "month": case 'month':
dataInicio = format(subDays(now, 30), "yyyy-MM-dd"); dataInicio = format(subDays(now, 30), 'yyyy-MM-dd');
dataFim = format(now, "yyyy-MM-dd"); dataFim = format(now, 'yyyy-MM-dd');
break; break;
} }
} }
function getDateRange(): { inicio: number; fim: number } { function getDateRange(): { inicio: number; fim: number } {
const inicio = startOfDay( const inicio = startOfDay(new Date(`${dataInicio}T${horaInicio}`)).getTime();
new Date(`${dataInicio}T${horaInicio}`),
).getTime();
const fim = endOfDay(new Date(`${dataFim}T${horaFim}`)).getTime(); const fim = endOfDay(new Date(`${dataFim}T${horaFim}`)).getTime();
return { inicio, fim }; return { inicio, fim };
} }
@@ -77,97 +85,109 @@
const { inicio, fim } = getDateRange(); const { inicio, fim } = getDateRange();
const relatorio = await client.query(api.monitoramento.gerarRelatorio, { const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
dataInicio: inicio, dataInicio: inicio,
dataFim: fim, dataFim: fim
}); });
type Estatistica = { min: number; max: number; avg: number };
const estatPorMetrica = relatorio.estatisticas as unknown as Record<
MetricKey,
Estatistica | undefined
>;
const doc = new jsPDF(); const doc = new jsPDF();
// Título // Título
doc.setFontSize(20); doc.setFontSize(20);
doc.setTextColor(102, 126, 234); // Primary color doc.setTextColor(102, 126, 234); // Primary color
doc.text("Relatório de Monitoramento do Sistema", 14, 20); doc.text('Relatório de Monitoramento do Sistema', 14, 20);
// Subtítulo com período // Subtítulo com período
doc.setFontSize(12); doc.setFontSize(12);
doc.setTextColor(0, 0, 0); doc.setTextColor(0, 0, 0);
doc.text( doc.text(
`Período: ${format(inicio, "dd/MM/yyyy HH:mm", { locale: ptBR })} até ${format(fim, "dd/MM/yyyy HH:mm", { locale: ptBR })}`, `Período: ${format(inicio, 'dd/MM/yyyy HH:mm', { locale: ptBR })} até ${format(fim, 'dd/MM/yyyy HH:mm', { locale: ptBR })}`,
14, 14,
30, 30
); );
// Informações gerais // Informações gerais
doc.setFontSize(10); doc.setFontSize(10);
doc.text( doc.text(`Gerado em: ${format(new Date(), 'dd/MM/yyyy HH:mm', { locale: ptBR })}`, 14, 38);
`Gerado em: ${format(new Date(), "dd/MM/yyyy HH:mm", { locale: ptBR })}`,
14,
38,
);
doc.text(`Total de registros: ${relatorio.metricas.length}`, 14, 44); doc.text(`Total de registros: ${relatorio.metricas.length}`, 14, 44);
// Estatísticas // Estatísticas
let yPos = 55; let yPos = 55;
doc.setFontSize(14); doc.setFontSize(14);
doc.setTextColor(102, 126, 234); doc.setTextColor(102, 126, 234);
doc.text("Estatísticas do Período", 14, yPos); doc.text('Estatísticas do Período', 14, yPos);
yPos += 10; yPos += 10;
const statsData: any[] = []; const statsData: string[][] = [];
Object.entries(selectedMetrics).forEach(([metric, selected]) => { (Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
if (selected && relatorio.estatisticas[metric]) { ([metric, selected]) => {
const stats = relatorio.estatisticas[metric]; if (selected && estatPorMetrica[metric]) {
const stats = estatPorMetrica[metric];
if (stats) { if (stats) {
statsData.push([ statsData.push([
metricLabels[metric], metricLabels[metric],
stats.min.toFixed(2), stats.min.toFixed(2),
stats.max.toFixed(2), stats.max.toFixed(2),
stats.avg.toFixed(2), stats.avg.toFixed(2)
]); ]);
} }
} }
}); }
);
autoTable(doc, { autoTable(doc, {
startY: yPos, startY: yPos,
head: [["Métrica", "Mínimo", "Máximo", "Média"]], head: [['Métrica', 'Mínimo', 'Máximo', 'Média']],
body: statsData, body: statsData,
theme: "striped", theme: 'striped',
headStyles: { fillColor: [102, 126, 234] }, headStyles: { fillColor: [102, 126, 234] }
}); });
// Dados detalhados (últimos 50 registros) // Dados detalhados (últimos 50 registros)
const finalY = (doc as any).lastAutoTable.finalY || yPos + 10; type JsPDFWithAutoTable = jsPDF & {
lastAutoTable?: { finalY: number };
};
const finalY = (doc as JsPDFWithAutoTable).lastAutoTable?.finalY ?? yPos + 10;
yPos = finalY + 15; yPos = finalY + 15;
doc.setFontSize(14); doc.setFontSize(14);
doc.setTextColor(102, 126, 234); doc.setTextColor(102, 126, 234);
doc.text("Registros Detalhados (Últimos 50)", 14, yPos); doc.text('Registros Detalhados (Últimos 50)', 14, yPos);
yPos += 10; yPos += 10;
const detailsData = relatorio.metricas.slice(0, 50).map((m) => { const detailsData: string[][] = relatorio.metricas.slice(0, 50).map((m) => {
const row = [format(m.timestamp, "dd/MM HH:mm", { locale: ptBR })]; const row: string[] = [format(m.timestamp, 'dd/MM HH:mm', { locale: ptBR })];
Object.entries(selectedMetrics).forEach(([metric, selected]) => { (Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
([metric, selected]) => {
if (selected) { if (selected) {
row.push((m[metric] || 0).toFixed(1)); const value = (m as unknown as Record<MetricKey, number | undefined>)[metric] ?? 0;
row.push(value.toFixed(1));
} }
}); }
);
return row; return row;
}); });
const headers = ["Data/Hora"]; const headers = ['Data/Hora'];
Object.entries(selectedMetrics).forEach(([metric, selected]) => { (Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
([metric, selected]) => {
if (selected) { if (selected) {
headers.push(metricLabels[metric]); headers.push(metricLabels[metric]);
} }
}); }
);
autoTable(doc, { autoTable(doc, {
startY: yPos, startY: yPos,
head: [headers], head: [headers],
body: detailsData, body: detailsData,
theme: "grid", theme: 'grid',
headStyles: { fillColor: [102, 126, 234] }, headStyles: { fillColor: [102, 126, 234] },
styles: { fontSize: 8 }, styles: { fontSize: 8 }
}); });
// Footer // Footer
@@ -180,17 +200,15 @@
`SGSE - Sistema de Gestão da Secretaria de Esportes | Página ${i} de ${pageCount}`, `SGSE - Sistema de Gestão da Secretaria de Esportes | Página ${i} de ${pageCount}`,
doc.internal.pageSize.getWidth() / 2, doc.internal.pageSize.getWidth() / 2,
doc.internal.pageSize.getHeight() - 10, doc.internal.pageSize.getHeight() - 10,
{ align: "center" }, { align: 'center' }
); );
} }
// Salvar // Salvar
doc.save( doc.save(`relatorio-monitoramento-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`);
`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.pdf`,
);
} catch (error) { } catch (error) {
console.error("Erro ao gerar PDF:", error); console.error('Erro ao gerar PDF:', error);
alert("Erro ao gerar relatório PDF. Tente novamente."); alert('Erro ao gerar relatório PDF. Tente novamente.');
} finally { } finally {
generating = false; generating = false;
} }
@@ -203,22 +221,25 @@
const { inicio, fim } = getDateRange(); const { inicio, fim } = getDateRange();
const relatorio = await client.query(api.monitoramento.gerarRelatorio, { const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
dataInicio: inicio, dataInicio: inicio,
dataFim: fim, dataFim: fim
}); });
// Preparar dados para CSV // Preparar dados para CSV
const csvData = relatorio.metricas.map((m) => { const csvData = relatorio.metricas.map((m) => {
const row: any = { const row: Record<string, string | number> = {
"Data/Hora": format(m.timestamp, "dd/MM/yyyy HH:mm:ss", { 'Data/Hora': format(m.timestamp, 'dd/MM/yyyy HH:mm:ss', {
locale: ptBR, locale: ptBR
}), })
}; };
Object.entries(selectedMetrics).forEach(([metric, selected]) => { (Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
([metric, selected]) => {
if (selected) { if (selected) {
row[metricLabels[metric]] = m[metric] || 0; row[metricLabels[metric]] =
(m as unknown as Record<MetricKey, number | undefined>)[metric] ?? 0;
} }
}); }
);
return row; return row;
}); });
@@ -227,97 +248,85 @@
const csv = Papa.unparse(csvData); const csv = Papa.unparse(csvData);
// Download // Download
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a"); const link = document.createElement('a');
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
link.setAttribute("href", url); link.setAttribute('href', url);
link.setAttribute( link.setAttribute(
"download", 'download',
`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.csv`, `relatorio-monitoramento-${format(new Date(), 'yyyy-MM-dd-HHmm')}.csv`
); );
link.style.visibility = "hidden"; link.style.visibility = 'hidden';
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
} catch (error) { } catch (error) {
console.error("Erro ao gerar CSV:", error); console.error('Erro ao gerar CSV:', error);
alert("Erro ao gerar relatório CSV. Tente novamente."); alert('Erro ao gerar relatório CSV. Tente novamente.');
} finally { } finally {
generating = false; generating = false;
} }
} }
function toggleAllMetrics(value: boolean) { function toggleAllMetrics(value: boolean) {
Object.keys(selectedMetrics).forEach((key) => { (Object.keys(selectedMetrics) as MetricKey[]).forEach((key) => {
selectedMetrics[key] = value; selectedMetrics[key] = value;
}); });
} }
</script> </script>
<dialog class="modal modal-open"> <dialog class="modal modal-open">
<div class="modal-box max-w-3xl bg-linear-to-br from-base-100 to-base-200"> <div class="modal-box from-base-100 to-base-200 max-w-3xl bg-linear-to-br">
<button <button
type="button" type="button"
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
onclick={onClose} onclick={onClose}
> >
</button> </button>
<h3 class="font-bold text-3xl text-primary mb-2"> <h3 class="text-primary mb-2 text-3xl font-bold">📊 Gerador de Relatórios</h3>
📊 Gerador de Relatórios <p class="text-base-content/60 mb-6">Exporte dados de monitoramento em PDF ou CSV</p>
</h3>
<p class="text-base-content/60 mb-6">
Exporte dados de monitoramento em PDF ou CSV
</p>
<!-- Seleção de Período --> <!-- Seleção de Período -->
<div class="card bg-base-100 shadow-xl mb-6"> <div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body"> <div class="card-body">
<h4 class="card-title text-xl">Período</h4> <h4 class="card-title text-xl">Período</h4>
<!-- Botões de Período Rápido --> <!-- Botões de Período Rápido -->
<div class="flex gap-2 mb-4"> <div class="mb-4 flex gap-2">
<button <button
type="button" type="button"
class="btn btn-sm {periodType === 'today' class="btn btn-sm {periodType === 'today' ? 'btn-primary' : 'btn-outline'}"
? 'btn-primary' onclick={() => setPeriod('today')}
: 'btn-outline'}"
onclick={() => setPeriod("today")}
> >
Hoje Hoje
</button> </button>
<button <button
type="button" type="button"
class="btn btn-sm {periodType === 'week' class="btn btn-sm {periodType === 'week' ? 'btn-primary' : 'btn-outline'}"
? 'btn-primary' onclick={() => setPeriod('week')}
: 'btn-outline'}"
onclick={() => setPeriod("week")}
> >
Última Semana Última Semana
</button> </button>
<button <button
type="button" type="button"
class="btn btn-sm {periodType === 'month' class="btn btn-sm {periodType === 'month' ? 'btn-primary' : 'btn-outline'}"
? 'btn-primary' onclick={() => setPeriod('month')}
: 'btn-outline'}"
onclick={() => setPeriod("month")}
> >
Último Mês Último Mês
</button> </button>
<button <button
type="button" type="button"
class="btn btn-sm {periodType === 'custom' class="btn btn-sm {periodType === 'custom' ? 'btn-primary' : 'btn-outline'}"
? 'btn-primary' onclick={() => (periodType = 'custom')}
: 'btn-outline'}"
onclick={() => (periodType = "custom")}
> >
Personalizado Personalizado
</button> </button>
</div> </div>
{#if periodType === "custom"} {#if periodType === 'custom'}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control"> <div class="form-control">
<label class="label" for="dataInicio"> <label class="label" for="dataInicio">
<span class="label-text font-semibold">Data Início</span> <span class="label-text font-semibold">Data Início</span>
@@ -371,9 +380,9 @@
</div> </div>
<!-- Seleção de Métricas --> <!-- Seleção de Métricas -->
<div class="card bg-base-100 shadow-xl mb-6"> <div class="card bg-base-100 mb-6 shadow-xl">
<div class="card-body"> <div class="card-body">
<div class="flex items-center justify-between mb-4"> <div class="mb-4 flex items-center justify-between">
<h4 class="card-title text-xl">Métricas a Incluir</h4> <h4 class="card-title text-xl">Métricas a Incluir</h4>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -393,15 +402,17 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{#each Object.entries(metricLabels) as [metric, label]} {#each metricEntries as [metric, label] (metric)}
<label <label
class="label cursor-pointer justify-start gap-3 hover:bg-base-200 rounded-lg p-2" class="label hover:bg-base-200 cursor-pointer justify-start gap-3 rounded-lg p-2"
> >
<input <input
type="checkbox" type="checkbox"
class="checkbox checkbox-primary" class="checkbox checkbox-primary"
bind:checked={selectedMetrics[metric]} checked={isMetricSelected(metric)}
onchange={(e) =>
setMetricSelected(metric, (e.currentTarget as HTMLInputElement).checked)}
/> />
<span class="label-text">{label}</span> <span class="label-text">{label}</span>
</label> </label>
@@ -411,13 +422,8 @@
</div> </div>
<!-- Botões de Exportação --> <!-- Botões de Exportação -->
<div class="flex gap-3 justify-end"> <div class="flex justify-end gap-3">
<button <button type="button" class="btn btn-outline" onclick={onClose} disabled={generating}>
type="button"
class="btn btn-outline"
onclick={onClose}
disabled={generating}
>
Cancelar Cancelar
</button> </button>
@@ -480,7 +486,7 @@
<div class="alert alert-warning mt-4"> <div class="alert alert-warning mt-4">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6" class="h-6 w-6 shrink-0 stroke-current"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,45 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/state"; import { page } from '$app/state';
import ActionGuard from "$lib/components/ActionGuard.svelte"; import ActionGuard from '$lib/components/ActionGuard.svelte';
import { Toaster } from "svelte-sonner"; import { Toaster } from 'svelte-sonner';
import PushNotificationManager from "$lib/components/PushNotificationManager.svelte"; import PushNotificationManager from '$lib/components/PushNotificationManager.svelte';
const { children } = $props(); const { children } = $props();
// Resolver recurso/ação a partir da rota // Resolver recurso/ação a partir da rota
const routeAction = $derived.by(() => { const routeAction = $derived.by(() => {
const p = page.url.pathname; const p = page.url.pathname;
if (p === "/" || p === "/solicitar-acesso") return null; if (p === '/' || p === '/solicitar-acesso') return null;
// Funcionários // Funcionários
if (p.startsWith("/recursos-humanos/funcionarios")) { if (p.startsWith('/recursos-humanos/funcionarios')) {
if (p.includes("/cadastro")) if (p.includes('/cadastro')) return { recurso: 'funcionarios', acao: 'criar' };
return { recurso: "funcionarios", acao: "criar" }; if (p.includes('/excluir')) return { recurso: 'funcionarios', acao: 'excluir' };
if (p.includes("/excluir")) if (p.includes('/editar') || p.includes('/funcionarioId'))
return { recurso: "funcionarios", acao: "excluir" }; return { recurso: 'funcionarios', acao: 'editar' };
if (p.includes("/editar") || p.includes("/funcionarioId")) return { recurso: 'funcionarios', acao: 'listar' };
return { recurso: "funcionarios", acao: "editar" };
return { recurso: "funcionarios", acao: "listar" };
} }
// Símbolos // Símbolos
if (p.startsWith("/recursos-humanos/simbolos")) { if (p.startsWith('/recursos-humanos/simbolos')) {
if (p.includes("/cadastro")) if (p.includes('/cadastro')) return { recurso: 'simbolos', acao: 'criar' };
return { recurso: "simbolos", acao: "criar" }; if (p.includes('/excluir')) return { recurso: 'simbolos', acao: 'excluir' };
if (p.includes("/excluir")) if (p.includes('/editar') || p.includes('/simboloId'))
return { recurso: "simbolos", acao: "excluir" }; return { recurso: 'simbolos', acao: 'editar' };
if (p.includes("/editar") || p.includes("/simboloId")) return { recurso: 'simbolos', acao: 'listar' };
return { recurso: "simbolos", acao: "editar" };
return { recurso: "simbolos", acao: "listar" };
} }
// Outras áreas (uso genérico: ver) // Outras áreas (uso genérico: ver)
if (p.startsWith("/financeiro")) if (p.startsWith('/financeiro')) return { recurso: 'financeiro', acao: 'ver' };
return { recurso: "financeiro", acao: "ver" }; if (p.startsWith('/controladoria')) return { recurso: 'controladoria', acao: 'ver' };
if (p.startsWith("/controladoria")) if (p.startsWith('/licitacoes')) return { recurso: 'licitacoes', acao: 'ver' };
return { recurso: "controladoria", acao: "ver" }; if (p.startsWith('/compras')) return { recurso: 'compras', acao: 'ver' };
if (p.startsWith("/licitacoes")) if (p.startsWith('/juridico')) return { recurso: 'juridico', acao: 'ver' };
return { recurso: "licitacoes", acao: "ver" }; if (p.startsWith('/comunicacao')) return { recurso: 'comunicacao', acao: 'ver' };
if (p.startsWith("/compras")) return { recurso: "compras", acao: "ver" }; if (p.startsWith('/programas-esportivos'))
if (p.startsWith("/juridico")) return { recurso: "juridico", acao: "ver" }; return { recurso: 'programas_esportivos', acao: 'ver' };
if (p.startsWith("/comunicacao")) if (p.startsWith('/secretaria-executiva'))
return { recurso: "comunicacao", acao: "ver" }; return { recurso: 'secretaria_executiva', acao: 'ver' };
if (p.startsWith("/programas-esportivos")) if (p.startsWith('/gestao-pessoas')) return { recurso: 'gestao_pessoas', acao: 'ver' };
return { recurso: "programas_esportivos", acao: "ver" };
if (p.startsWith("/secretaria-executiva"))
return { recurso: "secretaria_executiva", acao: "ver" };
if (p.startsWith("/gestao-pessoas"))
return { recurso: "gestao_pessoas", acao: "ver" };
return null; return null;
}); });
@@ -56,12 +47,12 @@
{#if routeAction} {#if routeAction}
<ActionGuard recurso={routeAction.recurso} acao={routeAction.acao}> <ActionGuard recurso={routeAction.recurso} acao={routeAction.acao}>
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4"> <main id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
{@render children()} {@render children()}
</main> </main>
</ActionGuard> </ActionGuard>
{:else} {:else}
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4"> <main id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
{@render children()} {@render children()}
</main> </main>
{/if} {/if}

View File

@@ -1,17 +1,16 @@
<script lang="ts"> <script lang="ts">
import SystemMonitorCardLocal from "$lib/components/ti/SystemMonitorCardLocal.svelte"; import { resolve } from '$app/paths';
import SystemMonitorCardLocal from '$lib/components/ti/SystemMonitorCardLocal.svelte';
</script> </script>
<div class="container mx-auto px-4 py-6 max-w-7xl"> <div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between mb-8"> <div class="mb-8 flex items-center justify-between">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div <div class="from-primary/20 to-primary/10 rounded-2xl bg-linear-to-br p-3">
class="p-3 bg-linear-to-br from-primary/20 to-primary/10 rounded-2xl"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10 text-primary" class="text-primary h-10 w-10"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -25,13 +24,13 @@
</svg> </svg>
</div> </div>
<div> <div>
<h1 class="text-4xl font-bold text-primary">Monitoramento SGSE</h1> <h1 class="text-primary text-4xl font-bold">Monitoramento SGSE</h1>
<p class="text-base-content/60 mt-2 text-lg"> <p class="text-base-content/60 mt-2 text-lg">
Sistema de monitoramento técnico em tempo real Sistema de monitoramento técnico em tempo real
</p> </p>
</div> </div>
</div> </div>
<a href="/ti" class="btn btn-ghost"> <a href={resolve('/ti')} class="btn btn-ghost">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5" class="h-5 w-5"

View File

@@ -1,17 +1,15 @@
import { v } from "convex/values"; import { v } from 'convex/values';
import { mutation, query, internalMutation } from "./_generated/server"; import { mutation, query, internalMutation } from './_generated/server';
import { internal } from "./_generated/api"; import { internal } from './_generated/api';
import { Id, Doc } from "./_generated/dataModel"; import { Id } from './_generated/dataModel';
import type { QueryCtx } from "./_generated/server"; import type { QueryCtx } from './_generated/server';
/** /**
* Helper para obter usuário autenticado * Helper para obter usuário autenticado
*/ */
async function getUsuarioAutenticado(ctx: QueryCtx) { async function getUsuarioAutenticado(ctx: QueryCtx) {
const usuariosOnline = await ctx.db.query("usuarios").collect(); const usuariosOnline = await ctx.db.query('usuarios').collect();
const usuarioOnline = usuariosOnline.find( const usuarioOnline = usuariosOnline.find((u) => u.statusPresenca === 'online');
(u) => u.statusPresenca === "online"
);
return usuarioOnline || null; return usuarioOnline || null;
} }
@@ -27,17 +25,17 @@ export const salvarMetricas = mutation({
usuariosOnline: v.optional(v.number()), usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()), errosCount: v.optional(v.number())
}, },
returns: v.object({ returns: v.object({
success: v.boolean(), success: v.boolean(),
metricId: v.optional(v.id("systemMetrics")), metricId: v.optional(v.id('systemMetrics'))
}), }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const timestamp = Date.now(); const timestamp = Date.now();
// Salvar métricas // Salvar métricas
const metricId = await ctx.db.insert("systemMetrics", { const metricId = await ctx.db.insert('systemMetrics', {
timestamp, timestamp,
cpuUsage: args.cpuUsage, cpuUsage: args.cpuUsage,
memoryUsage: args.memoryUsage, memoryUsage: args.memoryUsage,
@@ -46,19 +44,19 @@ export const salvarMetricas = mutation({
usuariosOnline: args.usuariosOnline, usuariosOnline: args.usuariosOnline,
mensagensPorMinuto: args.mensagensPorMinuto, mensagensPorMinuto: args.mensagensPorMinuto,
tempoRespostaMedio: args.tempoRespostaMedio, tempoRespostaMedio: args.tempoRespostaMedio,
errosCount: args.errosCount, errosCount: args.errosCount
}); });
// Verificar alertas após salvar métricas // Verificar alertas após salvar métricas
await ctx.scheduler.runAfter(0, internal.monitoramento.verificarAlertasInternal, { await ctx.scheduler.runAfter(0, internal.monitoramento.verificarAlertasInternal, {
metricId, metricId
}); });
// Limpar métricas antigas (mais de 30 dias) // Limpar métricas antigas (mais de 30 dias)
const dataLimite = Date.now() - 30 * 24 * 60 * 60 * 1000; const dataLimite = Date.now() - 30 * 24 * 60 * 60 * 1000;
const metricasAntigas = await ctx.db const metricasAntigas = await ctx.db
.query("systemMetrics") .query('systemMetrics')
.withIndex("by_timestamp", (q) => q.lt("timestamp", dataLimite)) .withIndex('by_timestamp', (q) => q.lt('timestamp', dataLimite))
.collect(); .collect();
for (const metrica of metricasAntigas) { for (const metrica of metricasAntigas) {
@@ -67,9 +65,9 @@ export const salvarMetricas = mutation({
return { return {
success: true, success: true,
metricId, metricId
}; };
}, }
}); });
/** /**
@@ -77,31 +75,31 @@ export const salvarMetricas = mutation({
*/ */
export const configurarAlerta = mutation({ export const configurarAlerta = mutation({
args: { args: {
alertId: v.optional(v.id("alertConfigurations")), alertId: v.optional(v.id('alertConfigurations')),
metricName: v.string(), metricName: v.string(),
threshold: v.number(), threshold: v.number(),
operator: v.union( operator: v.union(
v.literal(">"), v.literal('>'),
v.literal("<"), v.literal('<'),
v.literal(">="), v.literal('>='),
v.literal("<="), v.literal('<='),
v.literal("==") v.literal('==')
), ),
enabled: v.boolean(), enabled: v.boolean(),
notifyByEmail: v.boolean(), notifyByEmail: v.boolean(),
notifyByChat: v.boolean(), notifyByChat: v.boolean()
}, },
returns: v.object({ returns: v.object({
success: v.boolean(), success: v.boolean(),
alertId: v.id("alertConfigurations"), alertId: v.id('alertConfigurations')
}), }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const usuario = await getUsuarioAutenticado(ctx); const usuario = await getUsuarioAutenticado(ctx);
if (!usuario) { if (!usuario) {
throw new Error("Não autenticado"); throw new Error('Não autenticado');
} }
let alertId: Id<"alertConfigurations">; let alertId: Id<'alertConfigurations'>;
if (args.alertId) { if (args.alertId) {
// Atualizar alerta existente // Atualizar alerta existente
@@ -112,12 +110,12 @@ export const configurarAlerta = mutation({
enabled: args.enabled, enabled: args.enabled,
notifyByEmail: args.notifyByEmail, notifyByEmail: args.notifyByEmail,
notifyByChat: args.notifyByChat, notifyByChat: args.notifyByChat,
lastModified: Date.now(), lastModified: Date.now()
}); });
alertId = args.alertId; alertId = args.alertId;
} else { } else {
// Criar novo alerta // Criar novo alerta
alertId = await ctx.db.insert("alertConfigurations", { alertId = await ctx.db.insert('alertConfigurations', {
metricName: args.metricName, metricName: args.metricName,
threshold: args.threshold, threshold: args.threshold,
operator: args.operator, operator: args.operator,
@@ -125,15 +123,15 @@ export const configurarAlerta = mutation({
notifyByEmail: args.notifyByEmail, notifyByEmail: args.notifyByEmail,
notifyByChat: args.notifyByChat, notifyByChat: args.notifyByChat,
createdBy: usuario._id, createdBy: usuario._id,
lastModified: Date.now(), lastModified: Date.now()
}); });
} }
return { return {
success: true, success: true,
alertId, alertId
}; };
}, }
}); });
/** /**
@@ -143,27 +141,27 @@ export const listarAlertas = query({
args: {}, args: {},
returns: v.array( returns: v.array(
v.object({ v.object({
_id: v.id("alertConfigurations"), _id: v.id('alertConfigurations'),
metricName: v.string(), metricName: v.string(),
threshold: v.number(), threshold: v.number(),
operator: v.union( operator: v.union(
v.literal(">"), v.literal('>'),
v.literal("<"), v.literal('<'),
v.literal(">="), v.literal('>='),
v.literal("<="), v.literal('<='),
v.literal("==") v.literal('==')
), ),
enabled: v.boolean(), enabled: v.boolean(),
notifyByEmail: v.boolean(), notifyByEmail: v.boolean(),
notifyByChat: v.boolean(), notifyByChat: v.boolean(),
createdBy: v.id("usuarios"), createdBy: v.id('usuarios'),
lastModified: v.number(), lastModified: v.number()
}) })
), ),
handler: async (ctx) => { handler: async (ctx) => {
const alertas = await ctx.db.query("alertConfigurations").collect(); const alertas = await ctx.db.query('alertConfigurations').collect();
return alertas; return alertas;
}, }
}); });
/** /**
@@ -174,11 +172,11 @@ export const obterMetricas = query({
dataInicio: v.optional(v.number()), dataInicio: v.optional(v.number()),
dataFim: v.optional(v.number()), dataFim: v.optional(v.number()),
metricName: v.optional(v.string()), metricName: v.optional(v.string()),
limit: v.optional(v.number()), limit: v.optional(v.number())
}, },
returns: v.array( returns: v.array(
v.object({ v.object({
_id: v.id("systemMetrics"), _id: v.id('systemMetrics'),
timestamp: v.number(), timestamp: v.number(),
cpuUsage: v.optional(v.number()), cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()),
@@ -187,7 +185,7 @@ export const obterMetricas = query({
usuariosOnline: v.optional(v.number()), usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()), errosCount: v.optional(v.number())
}) })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -197,28 +195,26 @@ export const obterMetricas = query({
const inicio: number = args.dataInicio as number; const inicio: number = args.dataInicio as number;
const fim: number = args.dataFim as number; const fim: number = args.dataFim as number;
metricas = await ctx.db metricas = await ctx.db
.query("systemMetrics") .query('systemMetrics')
.withIndex("by_timestamp", (q) => .withIndex('by_timestamp', (q) => q.gte('timestamp', inicio).lte('timestamp', fim))
q.gte("timestamp", inicio).lte("timestamp", fim) .order('desc')
)
.order("desc")
.collect(); .collect();
} else if (args.dataInicio !== undefined) { } else if (args.dataInicio !== undefined) {
const inicio: number = args.dataInicio as number; const inicio: number = args.dataInicio as number;
metricas = await ctx.db metricas = await ctx.db
.query("systemMetrics") .query('systemMetrics')
.withIndex("by_timestamp", (q) => q.gte("timestamp", inicio)) .withIndex('by_timestamp', (q) => q.gte('timestamp', inicio))
.order("desc") .order('desc')
.collect(); .collect();
} else if (args.dataFim !== undefined) { } else if (args.dataFim !== undefined) {
const fim: number = args.dataFim as number; const fim: number = args.dataFim as number;
metricas = await ctx.db metricas = await ctx.db
.query("systemMetrics") .query('systemMetrics')
.withIndex("by_timestamp", (q) => q.lte("timestamp", fim)) .withIndex('by_timestamp', (q) => q.lte('timestamp', fim))
.order("desc") .order('desc')
.collect(); .collect();
} else { } else {
metricas = await ctx.db.query("systemMetrics").order("desc").collect(); metricas = await ctx.db.query('systemMetrics').order('desc').collect();
} }
// Limitar resultados // Limitar resultados
@@ -227,7 +223,7 @@ export const obterMetricas = query({
} }
return metricas; return metricas;
}, }
}); });
/** /**
@@ -237,7 +233,7 @@ export const obterMetricasRecentes = query({
args: {}, args: {},
returns: v.array( returns: v.array(
v.object({ v.object({
_id: v.id("systemMetrics"), _id: v.id('systemMetrics'),
timestamp: v.number(), timestamp: v.number(),
cpuUsage: v.optional(v.number()), cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()),
@@ -246,20 +242,20 @@ export const obterMetricasRecentes = query({
usuariosOnline: v.optional(v.number()), usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()), errosCount: v.optional(v.number())
}) })
), ),
handler: async (ctx) => { handler: async (ctx) => {
const umaHoraAtras = Date.now() - 60 * 60 * 1000; const umaHoraAtras = Date.now() - 60 * 60 * 1000;
const metricas = await ctx.db const metricas = await ctx.db
.query("systemMetrics") .query('systemMetrics')
.withIndex("by_timestamp", (q) => q.gte("timestamp", umaHoraAtras)) .withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
.order("desc") .order('desc')
.take(100); .take(100);
return metricas; return metricas;
}, }
}); });
/** /**
@@ -269,7 +265,7 @@ export const obterUltimaMetrica = query({
args: {}, args: {},
returns: v.union( returns: v.union(
v.object({ v.object({
_id: v.id("systemMetrics"), _id: v.id('systemMetrics'),
timestamp: v.number(), timestamp: v.number(),
cpuUsage: v.optional(v.number()), cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()),
@@ -278,18 +274,15 @@ export const obterUltimaMetrica = query({
usuariosOnline: v.optional(v.number()), usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()), errosCount: v.optional(v.number())
}), }),
v.null() v.null()
), ),
handler: async (ctx) => { handler: async (ctx) => {
const metrica = await ctx.db const metrica = await ctx.db.query('systemMetrics').order('desc').first();
.query("systemMetrics")
.order("desc")
.first();
return metrica || null; return metrica || null;
}, }
}); });
/** /**
@@ -297,7 +290,7 @@ export const obterUltimaMetrica = query({
*/ */
export const verificarAlertasInternal = internalMutation({ export const verificarAlertasInternal = internalMutation({
args: { args: {
metricId: v.id("systemMetrics"), metricId: v.id('systemMetrics')
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -306,32 +299,32 @@ export const verificarAlertasInternal = internalMutation({
// Buscar configurações de alerta ativas // Buscar configurações de alerta ativas
const alertasAtivos = await ctx.db const alertasAtivos = await ctx.db
.query("alertConfigurations") .query('alertConfigurations')
.withIndex("by_enabled", (q) => q.eq("enabled", true)) .withIndex('by_enabled', (q) => q.eq('enabled', true))
.collect(); .collect();
for (const alerta of alertasAtivos) { for (const alerta of alertasAtivos) {
// Obter valor da métrica correspondente, validando tipo número // Obter valor da métrica correspondente, validando tipo número
const rawValue = (metrica as Record<string, unknown>)[alerta.metricName]; const rawValue = (metrica as Record<string, unknown>)[alerta.metricName];
if (typeof rawValue !== "number") continue; if (typeof rawValue !== 'number') continue;
const metricValue = rawValue; const metricValue = rawValue;
// Verificar se o alerta deve ser disparado // Verificar se o alerta deve ser disparado
let shouldTrigger = false; let shouldTrigger = false;
switch (alerta.operator) { switch (alerta.operator) {
case ">": case '>':
shouldTrigger = metricValue > alerta.threshold; shouldTrigger = metricValue > alerta.threshold;
break; break;
case "<": case '<':
shouldTrigger = metricValue < alerta.threshold; shouldTrigger = metricValue < alerta.threshold;
break; break;
case ">=": case '>=':
shouldTrigger = metricValue >= alerta.threshold; shouldTrigger = metricValue >= alerta.threshold;
break; break;
case "<=": case '<=':
shouldTrigger = metricValue <= alerta.threshold; shouldTrigger = metricValue <= alerta.threshold;
break; break;
case "==": case '==':
shouldTrigger = metricValue === alerta.threshold; shouldTrigger = metricValue === alerta.threshold;
break; break;
} }
@@ -340,51 +333,51 @@ export const verificarAlertasInternal = internalMutation({
// Verificar se já existe um alerta triggered recente (últimos 5 minutos) // Verificar se já existe um alerta triggered recente (últimos 5 minutos)
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000; const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
const alertaRecente = await ctx.db const alertaRecente = await ctx.db
.query("alertHistory") .query('alertHistory')
.withIndex("by_config", (q) => .withIndex('by_config', (q) =>
q.eq("configId", alerta._id).gte("timestamp", cincoMinutosAtras) q.eq('configId', alerta._id).gte('timestamp', cincoMinutosAtras)
) )
.filter((q) => q.eq(q.field("status"), "triggered")) .filter((q) => q.eq(q.field('status'), 'triggered'))
.first(); .first();
// Se já existe alerta recente, não disparar novamente // Se já existe alerta recente, não disparar novamente
if (alertaRecente) continue; if (alertaRecente) continue;
// Registrar alerta no histórico // Registrar alerta no histórico
await ctx.db.insert("alertHistory", { await ctx.db.insert('alertHistory', {
configId: alerta._id, configId: alerta._id,
metricName: alerta.metricName, metricName: alerta.metricName,
metricValue, metricValue,
threshold: alerta.threshold, threshold: alerta.threshold,
timestamp: Date.now(), timestamp: Date.now(),
status: "triggered", status: 'triggered',
notificationsSent: { notificationsSent: {
email: alerta.notifyByEmail, email: alerta.notifyByEmail,
chat: alerta.notifyByChat, chat: alerta.notifyByChat
}, }
}); });
// Criar notificação no chat se configurado // Criar notificação no chat se configurado
if (alerta.notifyByChat) { if (alerta.notifyByChat) {
// Buscar roles administrativas (nível <= 1) e filtrar usuários por roleId // Buscar roles administrativas (nível <= 1) e filtrar usuários por roleId
const rolesAdminOuTi = await ctx.db const rolesAdminOuTi = await ctx.db
.query("roles") .query('roles')
.filter((q) => q.lte(q.field("nivel"), 1)) .filter((q) => q.lte(q.field('nivel'), 1))
.collect(); .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 usuarios = await ctx.db.query('usuarios').collect();
const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId)); const usuariosTI = usuarios.filter((u) => rolesPermitidas.has(u.roleId));
for (const usuario of usuariosTI) { for (const usuario of usuariosTI) {
await ctx.db.insert("notificacoes", { await ctx.db.insert('notificacoes', {
usuarioId: usuario._id, usuarioId: usuario._id,
tipo: "nova_mensagem", tipo: 'nova_mensagem',
titulo: `⚠️ Alerta de Sistema: ${alerta.metricName}`, titulo: `⚠️ Alerta de Sistema: ${alerta.metricName}`,
descricao: `Métrica ${alerta.metricName} está em ${metricValue.toFixed(2)}% (limite: ${alerta.threshold}%)`, descricao: `Métrica ${alerta.metricName} está em ${metricValue.toFixed(2)}% (limite: ${alerta.threshold}%)`,
lida: false, lida: false,
criadaEm: Date.now(), criadaEm: Date.now()
}); });
} }
} }
@@ -397,7 +390,7 @@ export const verificarAlertasInternal = internalMutation({
} }
return null; return null;
}, }
}); });
/** /**
@@ -407,16 +400,16 @@ export const gerarRelatorio = query({
args: { args: {
dataInicio: v.number(), dataInicio: v.number(),
dataFim: v.number(), dataFim: v.number(),
metricNames: v.optional(v.array(v.string())), metricNames: v.optional(v.array(v.string()))
}, },
returns: v.object({ returns: v.object({
periodo: v.object({ periodo: v.object({
inicio: v.number(), inicio: v.number(),
fim: v.number(), fim: v.number()
}), }),
metricas: v.array( metricas: v.array(
v.object({ v.object({
_id: v.id("systemMetrics"), _id: v.id('systemMetrics'),
timestamp: v.number(), timestamp: v.number(),
cpuUsage: v.optional(v.number()), cpuUsage: v.optional(v.number()),
memoryUsage: v.optional(v.number()), memoryUsage: v.optional(v.number()),
@@ -425,58 +418,74 @@ export const gerarRelatorio = query({
usuariosOnline: v.optional(v.number()), usuariosOnline: v.optional(v.number()),
mensagensPorMinuto: v.optional(v.number()), mensagensPorMinuto: v.optional(v.number()),
tempoRespostaMedio: v.optional(v.number()), tempoRespostaMedio: v.optional(v.number()),
errosCount: v.optional(v.number()), errosCount: v.optional(v.number())
}) })
), ),
estatisticas: v.object({ estatisticas: v.object({
cpuUsage: v.optional(v.object({ cpuUsage: v.optional(
v.object({
min: v.number(), min: v.number(),
max: v.number(), max: v.number(),
avg: v.number(), avg: v.number()
})), })
memoryUsage: v.optional(v.object({ ),
memoryUsage: v.optional(
v.object({
min: v.number(), min: v.number(),
max: v.number(), max: v.number(),
avg: v.number(), avg: v.number()
})), })
networkLatency: v.optional(v.object({ ),
networkLatency: v.optional(
v.object({
min: v.number(), min: v.number(),
max: v.number(), max: v.number(),
avg: v.number(), avg: v.number()
})), })
storageUsed: v.optional(v.object({ ),
storageUsed: v.optional(
v.object({
min: v.number(), min: v.number(),
max: v.number(), max: v.number(),
avg: v.number(), avg: v.number()
})), })
usuariosOnline: v.optional(v.object({ ),
usuariosOnline: v.optional(
v.object({
min: v.number(), min: v.number(),
max: v.number(), max: v.number(),
avg: v.number(), avg: v.number()
})), })
mensagensPorMinuto: v.optional(v.object({ ),
mensagensPorMinuto: v.optional(
v.object({
min: v.number(), min: v.number(),
max: v.number(), max: v.number(),
avg: v.number(), avg: v.number()
})), })
tempoRespostaMedio: v.optional(v.object({ ),
tempoRespostaMedio: v.optional(
v.object({
min: v.number(), min: v.number(),
max: v.number(), max: v.number(),
avg: v.number(), avg: v.number()
})), })
errosCount: v.optional(v.object({ ),
errosCount: v.optional(
v.object({
min: v.number(), min: v.number(),
max: v.number(), max: v.number(),
avg: v.number(), avg: v.number()
})), })
}), )
})
}), }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Buscar métricas no período // Buscar métricas no período
const metricas = await ctx.db const metricas = await ctx.db
.query("systemMetrics") .query('systemMetrics')
.withIndex("by_timestamp", (q) => .withIndex('by_timestamp', (q) =>
q.gte("timestamp", args.dataInicio).lte("timestamp", args.dataFim) q.gte('timestamp', args.dataInicio).lte('timestamp', args.dataFim)
) )
.collect(); .collect();
@@ -488,7 +497,7 @@ export const gerarRelatorio = query({
return { return {
min: Math.min(...valores), min: Math.min(...valores),
max: Math.max(...valores), max: Math.max(...valores),
avg: valores.reduce((a, b) => a + b, 0) / valores.length, avg: valores.reduce((a, b) => a + b, 0) / valores.length
}; };
}; };
@@ -516,18 +525,18 @@ export const gerarRelatorio = query({
), ),
errosCount: calcularEstatisticas( errosCount: calcularEstatisticas(
metricas.map((m) => m.errosCount).filter((v) => v !== undefined) as number[] metricas.map((m) => m.errosCount).filter((v) => v !== undefined) as number[]
), )
}; };
return { return {
periodo: { periodo: {
inicio: args.dataInicio, inicio: args.dataInicio,
fim: args.dataFim, fim: args.dataFim
}, },
metricas, metricas,
estatisticas, estatisticas
}; };
}, }
}); });
/** /**
@@ -535,15 +544,15 @@ export const gerarRelatorio = query({
*/ */
export const deletarAlerta = mutation({ export const deletarAlerta = mutation({
args: { args: {
alertId: v.id("alertConfigurations"), alertId: v.id('alertConfigurations')
}, },
returns: v.object({ returns: v.object({
success: v.boolean(), success: v.boolean()
}), }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
await ctx.db.delete(args.alertId); await ctx.db.delete(args.alertId);
return { success: true }; return { success: true };
}, }
}); });
/** /**
@@ -551,31 +560,177 @@ export const deletarAlerta = mutation({
*/ */
export const obterHistoricoAlertas = query({ export const obterHistoricoAlertas = query({
args: { args: {
limit: v.optional(v.number()), limit: v.optional(v.number())
}, },
returns: v.array( returns: v.array(
v.object({ v.object({
_id: v.id("alertHistory"), _id: v.id('alertHistory'),
configId: v.id("alertConfigurations"), configId: v.id('alertConfigurations'),
metricName: v.string(), metricName: v.string(),
metricValue: v.number(), metricValue: v.number(),
threshold: v.number(), threshold: v.number(),
timestamp: v.number(), timestamp: v.number(),
status: v.union(v.literal("triggered"), v.literal("resolved")), status: v.union(v.literal('triggered'), v.literal('resolved')),
notificationsSent: v.object({ notificationsSent: v.object({
email: v.boolean(), email: v.boolean(),
chat: v.boolean(), chat: v.boolean()
}), })
}) })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const limit = args.limit || 50; const limit = args.limit || 50;
const historico = await ctx.db const historico = await ctx.db.query('alertHistory').order('desc').take(limit);
.query("alertHistory")
.order("desc")
.take(limit);
return historico; 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 };
}
}); });