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:
@@ -1,26 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { format, subDays, startOfDay, endOfDay } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import jsPDF from "jspdf";
|
||||
import autoTable from "jspdf-autotable";
|
||||
import Papa from "papaparse";
|
||||
import { useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import { format, subDays, startOfDay, endOfDay } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
let { onClose }: { onClose: () => void } = $props();
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
// Estados
|
||||
let periodType = $state("custom");
|
||||
let dataInicio = $state(format(subDays(new Date(), 7), "yyyy-MM-dd"));
|
||||
let dataFim = $state(format(new Date(), "yyyy-MM-dd"));
|
||||
let horaInicio = $state("00:00");
|
||||
let horaFim = $state("23:59");
|
||||
let periodType = $state('custom');
|
||||
let dataInicio = $state(format(subDays(new Date(), 7), 'yyyy-MM-dd'));
|
||||
let dataFim = $state(format(new Date(), 'yyyy-MM-dd'));
|
||||
let horaInicio = $state('00:00');
|
||||
let horaFim = $state('23:59');
|
||||
let generating = $state(false);
|
||||
|
||||
// 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,
|
||||
memoryUsage: true,
|
||||
networkLatency: true,
|
||||
@@ -28,44 +40,40 @@
|
||||
usuariosOnline: true,
|
||||
mensagensPorMinuto: true,
|
||||
tempoRespostaMedio: true,
|
||||
errosCount: true,
|
||||
errosCount: true
|
||||
});
|
||||
|
||||
const metricLabels: Record<string, 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",
|
||||
};
|
||||
const metricEntries = $derived(Object.entries(metricLabels) as Array<[MetricKey, string]>);
|
||||
|
||||
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;
|
||||
const now = new Date();
|
||||
|
||||
switch (type) {
|
||||
case "today":
|
||||
dataInicio = format(now, "yyyy-MM-dd");
|
||||
dataFim = format(now, "yyyy-MM-dd");
|
||||
case 'today':
|
||||
dataInicio = format(now, 'yyyy-MM-dd');
|
||||
dataFim = format(now, 'yyyy-MM-dd');
|
||||
break;
|
||||
case "week":
|
||||
dataInicio = format(subDays(now, 7), "yyyy-MM-dd");
|
||||
dataFim = format(now, "yyyy-MM-dd");
|
||||
case 'week':
|
||||
dataInicio = format(subDays(now, 7), 'yyyy-MM-dd');
|
||||
dataFim = format(now, 'yyyy-MM-dd');
|
||||
break;
|
||||
case "month":
|
||||
dataInicio = format(subDays(now, 30), "yyyy-MM-dd");
|
||||
dataFim = format(now, "yyyy-MM-dd");
|
||||
case 'month':
|
||||
dataInicio = format(subDays(now, 30), 'yyyy-MM-dd');
|
||||
dataFim = format(now, 'yyyy-MM-dd');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getDateRange(): { inicio: number; fim: number } {
|
||||
const inicio = startOfDay(
|
||||
new Date(`${dataInicio}T${horaInicio}`),
|
||||
).getTime();
|
||||
const inicio = startOfDay(new Date(`${dataInicio}T${horaInicio}`)).getTime();
|
||||
const fim = endOfDay(new Date(`${dataFim}T${horaFim}`)).getTime();
|
||||
return { inicio, fim };
|
||||
}
|
||||
@@ -77,97 +85,109 @@
|
||||
const { inicio, fim } = getDateRange();
|
||||
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
||||
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();
|
||||
|
||||
// Título
|
||||
doc.setFontSize(20);
|
||||
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
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
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,
|
||||
30,
|
||||
30
|
||||
);
|
||||
|
||||
// Informações gerais
|
||||
doc.setFontSize(10);
|
||||
doc.text(
|
||||
`Gerado em: ${format(new Date(), "dd/MM/yyyy HH:mm", { locale: ptBR })}`,
|
||||
14,
|
||||
38,
|
||||
);
|
||||
doc.text(`Gerado em: ${format(new Date(), 'dd/MM/yyyy HH:mm', { locale: ptBR })}`, 14, 38);
|
||||
doc.text(`Total de registros: ${relatorio.metricas.length}`, 14, 44);
|
||||
|
||||
// Estatísticas
|
||||
let yPos = 55;
|
||||
doc.setFontSize(14);
|
||||
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;
|
||||
|
||||
const statsData: any[] = [];
|
||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
||||
if (selected && relatorio.estatisticas[metric]) {
|
||||
const stats = relatorio.estatisticas[metric];
|
||||
const statsData: string[][] = [];
|
||||
(Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
|
||||
([metric, selected]) => {
|
||||
if (selected && estatPorMetrica[metric]) {
|
||||
const stats = estatPorMetrica[metric];
|
||||
if (stats) {
|
||||
statsData.push([
|
||||
metricLabels[metric],
|
||||
stats.min.toFixed(2),
|
||||
stats.max.toFixed(2),
|
||||
stats.avg.toFixed(2),
|
||||
stats.avg.toFixed(2)
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [["Métrica", "Mínimo", "Máximo", "Média"]],
|
||||
head: [['Métrica', 'Mínimo', 'Máximo', 'Média']],
|
||||
body: statsData,
|
||||
theme: "striped",
|
||||
headStyles: { fillColor: [102, 126, 234] },
|
||||
theme: 'striped',
|
||||
headStyles: { fillColor: [102, 126, 234] }
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(102, 126, 234);
|
||||
doc.text("Registros Detalhados (Últimos 50)", 14, yPos);
|
||||
doc.text('Registros Detalhados (Últimos 50)', 14, yPos);
|
||||
yPos += 10;
|
||||
|
||||
const detailsData = relatorio.metricas.slice(0, 50).map((m) => {
|
||||
const row = [format(m.timestamp, "dd/MM HH:mm", { locale: ptBR })];
|
||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
||||
const detailsData: string[][] = relatorio.metricas.slice(0, 50).map((m) => {
|
||||
const row: string[] = [format(m.timestamp, 'dd/MM HH:mm', { locale: ptBR })];
|
||||
(Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
|
||||
([metric, 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;
|
||||
});
|
||||
|
||||
const headers = ["Data/Hora"];
|
||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
||||
const headers = ['Data/Hora'];
|
||||
(Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
|
||||
([metric, selected]) => {
|
||||
if (selected) {
|
||||
headers.push(metricLabels[metric]);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [headers],
|
||||
body: detailsData,
|
||||
theme: "grid",
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [102, 126, 234] },
|
||||
styles: { fontSize: 8 },
|
||||
styles: { fontSize: 8 }
|
||||
});
|
||||
|
||||
// Footer
|
||||
@@ -180,17 +200,15 @@
|
||||
`SGSE - Sistema de Gestão da Secretaria de Esportes | Página ${i} de ${pageCount}`,
|
||||
doc.internal.pageSize.getWidth() / 2,
|
||||
doc.internal.pageSize.getHeight() - 10,
|
||||
{ align: "center" },
|
||||
{ align: 'center' }
|
||||
);
|
||||
}
|
||||
|
||||
// Salvar
|
||||
doc.save(
|
||||
`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.pdf`,
|
||||
);
|
||||
doc.save(`relatorio-monitoramento-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`);
|
||||
} catch (error) {
|
||||
console.error("Erro ao gerar PDF:", error);
|
||||
alert("Erro ao gerar relatório PDF. Tente novamente.");
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
alert('Erro ao gerar relatório PDF. Tente novamente.');
|
||||
} finally {
|
||||
generating = false;
|
||||
}
|
||||
@@ -203,22 +221,25 @@
|
||||
const { inicio, fim } = getDateRange();
|
||||
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
||||
dataInicio: inicio,
|
||||
dataFim: fim,
|
||||
dataFim: fim
|
||||
});
|
||||
|
||||
// Preparar dados para CSV
|
||||
const csvData = relatorio.metricas.map((m) => {
|
||||
const row: any = {
|
||||
"Data/Hora": format(m.timestamp, "dd/MM/yyyy HH:mm:ss", {
|
||||
locale: ptBR,
|
||||
}),
|
||||
const row: Record<string, string | number> = {
|
||||
'Data/Hora': format(m.timestamp, 'dd/MM/yyyy HH:mm:ss', {
|
||||
locale: ptBR
|
||||
})
|
||||
};
|
||||
|
||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
||||
(Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
|
||||
([metric, selected]) => {
|
||||
if (selected) {
|
||||
row[metricLabels[metric]] = m[metric] || 0;
|
||||
row[metricLabels[metric]] =
|
||||
(m as unknown as Record<MetricKey, number | undefined>)[metric] ?? 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return row;
|
||||
});
|
||||
@@ -227,97 +248,85 @@
|
||||
const csv = Papa.unparse(csvData);
|
||||
|
||||
// Download
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute(
|
||||
"download",
|
||||
`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.csv`,
|
||||
'download',
|
||||
`relatorio-monitoramento-${format(new Date(), 'yyyy-MM-dd-HHmm')}.csv`
|
||||
);
|
||||
link.style.visibility = "hidden";
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
console.error("Erro ao gerar CSV:", error);
|
||||
alert("Erro ao gerar relatório CSV. Tente novamente.");
|
||||
console.error('Erro ao gerar CSV:', error);
|
||||
alert('Erro ao gerar relatório CSV. Tente novamente.');
|
||||
} finally {
|
||||
generating = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllMetrics(value: boolean) {
|
||||
Object.keys(selectedMetrics).forEach((key) => {
|
||||
(Object.keys(selectedMetrics) as MetricKey[]).forEach((key) => {
|
||||
selectedMetrics[key] = value;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<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
|
||||
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}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h3 class="font-bold text-3xl text-primary mb-2">
|
||||
📊 Gerador de Relatórios
|
||||
</h3>
|
||||
<p class="text-base-content/60 mb-6">
|
||||
Exporte dados de monitoramento em PDF ou CSV
|
||||
</p>
|
||||
<h3 class="text-primary mb-2 text-3xl font-bold">📊 Gerador de Relatórios</h3>
|
||||
<p class="text-base-content/60 mb-6">Exporte dados de monitoramento em PDF ou CSV</p>
|
||||
|
||||
<!-- 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">
|
||||
<h4 class="card-title text-xl">Período</h4>
|
||||
|
||||
<!-- Botões de Período Rápido -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="mb-4 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {periodType === 'today'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'}"
|
||||
onclick={() => setPeriod("today")}
|
||||
class="btn btn-sm {periodType === 'today' ? 'btn-primary' : 'btn-outline'}"
|
||||
onclick={() => setPeriod('today')}
|
||||
>
|
||||
Hoje
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {periodType === 'week'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'}"
|
||||
onclick={() => setPeriod("week")}
|
||||
class="btn btn-sm {periodType === 'week' ? 'btn-primary' : 'btn-outline'}"
|
||||
onclick={() => setPeriod('week')}
|
||||
>
|
||||
Última Semana
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {periodType === 'month'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'}"
|
||||
onclick={() => setPeriod("month")}
|
||||
class="btn btn-sm {periodType === 'month' ? 'btn-primary' : 'btn-outline'}"
|
||||
onclick={() => setPeriod('month')}
|
||||
>
|
||||
Último Mês
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {periodType === 'custom'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'}"
|
||||
onclick={() => (periodType = "custom")}
|
||||
class="btn btn-sm {periodType === 'custom' ? 'btn-primary' : 'btn-outline'}"
|
||||
onclick={() => (periodType = 'custom')}
|
||||
>
|
||||
Personalizado
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if periodType === "custom"}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{#if periodType === 'custom'}
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label" for="dataInicio">
|
||||
<span class="label-text font-semibold">Data Início</span>
|
||||
@@ -371,9 +380,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 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="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>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@@ -393,15 +402,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{#each Object.entries(metricLabels) as [metric, label]}
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
{#each metricEntries as [metric, label] (metric)}
|
||||
<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
|
||||
type="checkbox"
|
||||
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>
|
||||
</label>
|
||||
@@ -411,13 +422,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Botões de Exportação -->
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
onclick={onClose}
|
||||
disabled={generating}
|
||||
>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" class="btn btn-outline" onclick={onClose} disabled={generating}>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
@@ -480,7 +486,7 @@
|
||||
<div class="alert alert-warning mt-4">
|
||||
<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"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,54 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import ActionGuard from "$lib/components/ActionGuard.svelte";
|
||||
import { Toaster } from "svelte-sonner";
|
||||
import PushNotificationManager from "$lib/components/PushNotificationManager.svelte";
|
||||
import { page } from '$app/state';
|
||||
import ActionGuard from '$lib/components/ActionGuard.svelte';
|
||||
import { Toaster } from 'svelte-sonner';
|
||||
import PushNotificationManager from '$lib/components/PushNotificationManager.svelte';
|
||||
const { children } = $props();
|
||||
|
||||
// Resolver recurso/ação a partir da rota
|
||||
const routeAction = $derived.by(() => {
|
||||
const p = page.url.pathname;
|
||||
if (p === "/" || p === "/solicitar-acesso") return null;
|
||||
if (p === '/' || p === '/solicitar-acesso') return null;
|
||||
|
||||
// Funcionários
|
||||
if (p.startsWith("/recursos-humanos/funcionarios")) {
|
||||
if (p.includes("/cadastro"))
|
||||
return { recurso: "funcionarios", acao: "criar" };
|
||||
if (p.includes("/excluir"))
|
||||
return { recurso: "funcionarios", acao: "excluir" };
|
||||
if (p.includes("/editar") || p.includes("/funcionarioId"))
|
||||
return { recurso: "funcionarios", acao: "editar" };
|
||||
return { recurso: "funcionarios", acao: "listar" };
|
||||
if (p.startsWith('/recursos-humanos/funcionarios')) {
|
||||
if (p.includes('/cadastro')) return { recurso: 'funcionarios', acao: 'criar' };
|
||||
if (p.includes('/excluir')) return { recurso: 'funcionarios', acao: 'excluir' };
|
||||
if (p.includes('/editar') || p.includes('/funcionarioId'))
|
||||
return { recurso: 'funcionarios', acao: 'editar' };
|
||||
return { recurso: 'funcionarios', acao: 'listar' };
|
||||
}
|
||||
|
||||
// Símbolos
|
||||
if (p.startsWith("/recursos-humanos/simbolos")) {
|
||||
if (p.includes("/cadastro"))
|
||||
return { recurso: "simbolos", acao: "criar" };
|
||||
if (p.includes("/excluir"))
|
||||
return { recurso: "simbolos", acao: "excluir" };
|
||||
if (p.includes("/editar") || p.includes("/simboloId"))
|
||||
return { recurso: "simbolos", acao: "editar" };
|
||||
return { recurso: "simbolos", acao: "listar" };
|
||||
if (p.startsWith('/recursos-humanos/simbolos')) {
|
||||
if (p.includes('/cadastro')) return { recurso: 'simbolos', acao: 'criar' };
|
||||
if (p.includes('/excluir')) return { recurso: 'simbolos', acao: 'excluir' };
|
||||
if (p.includes('/editar') || p.includes('/simboloId'))
|
||||
return { recurso: 'simbolos', acao: 'editar' };
|
||||
return { recurso: 'simbolos', acao: 'listar' };
|
||||
}
|
||||
|
||||
// Outras áreas (uso genérico: ver)
|
||||
if (p.startsWith("/financeiro"))
|
||||
return { recurso: "financeiro", acao: "ver" };
|
||||
if (p.startsWith("/controladoria"))
|
||||
return { recurso: "controladoria", acao: "ver" };
|
||||
if (p.startsWith("/licitacoes"))
|
||||
return { recurso: "licitacoes", acao: "ver" };
|
||||
if (p.startsWith("/compras")) return { recurso: "compras", acao: "ver" };
|
||||
if (p.startsWith("/juridico")) return { recurso: "juridico", acao: "ver" };
|
||||
if (p.startsWith("/comunicacao"))
|
||||
return { recurso: "comunicacao", acao: "ver" };
|
||||
if (p.startsWith("/programas-esportivos"))
|
||||
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" };
|
||||
if (p.startsWith('/financeiro')) return { recurso: 'financeiro', acao: 'ver' };
|
||||
if (p.startsWith('/controladoria')) return { recurso: 'controladoria', acao: 'ver' };
|
||||
if (p.startsWith('/licitacoes')) return { recurso: 'licitacoes', acao: 'ver' };
|
||||
if (p.startsWith('/compras')) return { recurso: 'compras', acao: 'ver' };
|
||||
if (p.startsWith('/juridico')) return { recurso: 'juridico', acao: 'ver' };
|
||||
if (p.startsWith('/comunicacao')) return { recurso: 'comunicacao', acao: 'ver' };
|
||||
if (p.startsWith('/programas-esportivos'))
|
||||
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;
|
||||
});
|
||||
@@ -56,12 +47,12 @@
|
||||
|
||||
{#if routeAction}
|
||||
<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()}
|
||||
</main>
|
||||
</ActionGuard>
|
||||
{: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()}
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<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>
|
||||
|
||||
<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 -->
|
||||
<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="p-3 bg-linear-to-br from-primary/20 to-primary/10 rounded-2xl"
|
||||
>
|
||||
<div class="from-primary/20 to-primary/10 rounded-2xl bg-linear-to-br p-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10 text-primary"
|
||||
class="text-primary h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -25,13 +24,13 @@
|
||||
</svg>
|
||||
</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">
|
||||
Sistema de monitoramento técnico em tempo real
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/ti" class="btn btn-ghost">
|
||||
<a href={resolve('/ti')} class="btn btn-ghost">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
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"
|
||||
);
|
||||
const usuariosOnline = await ctx.db.query('usuarios').collect();
|
||||
const usuarioOnline = usuariosOnline.find((u) => u.statusPresenca === 'online');
|
||||
return usuarioOnline || null;
|
||||
}
|
||||
|
||||
@@ -27,17 +25,17 @@ export const salvarMetricas = mutation({
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number())
|
||||
},
|
||||
returns: v.object({
|
||||
success: v.boolean(),
|
||||
metricId: v.optional(v.id("systemMetrics")),
|
||||
metricId: v.optional(v.id('systemMetrics'))
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Salvar métricas
|
||||
const metricId = await ctx.db.insert("systemMetrics", {
|
||||
const metricId = await ctx.db.insert('systemMetrics', {
|
||||
timestamp,
|
||||
cpuUsage: args.cpuUsage,
|
||||
memoryUsage: args.memoryUsage,
|
||||
@@ -46,19 +44,19 @@ export const salvarMetricas = mutation({
|
||||
usuariosOnline: args.usuariosOnline,
|
||||
mensagensPorMinuto: args.mensagensPorMinuto,
|
||||
tempoRespostaMedio: args.tempoRespostaMedio,
|
||||
errosCount: args.errosCount,
|
||||
errosCount: args.errosCount
|
||||
});
|
||||
|
||||
// Verificar alertas após salvar métricas
|
||||
await ctx.scheduler.runAfter(0, internal.monitoramento.verificarAlertasInternal, {
|
||||
metricId,
|
||||
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))
|
||||
.query('systemMetrics')
|
||||
.withIndex('by_timestamp', (q) => q.lt('timestamp', dataLimite))
|
||||
.collect();
|
||||
|
||||
for (const metrica of metricasAntigas) {
|
||||
@@ -67,9 +65,9 @@ export const salvarMetricas = mutation({
|
||||
|
||||
return {
|
||||
success: true,
|
||||
metricId,
|
||||
metricId
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -77,31 +75,31 @@ export const salvarMetricas = mutation({
|
||||
*/
|
||||
export const configurarAlerta = mutation({
|
||||
args: {
|
||||
alertId: v.optional(v.id("alertConfigurations")),
|
||||
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("==")
|
||||
v.literal('>'),
|
||||
v.literal('<'),
|
||||
v.literal('>='),
|
||||
v.literal('<='),
|
||||
v.literal('==')
|
||||
),
|
||||
enabled: v.boolean(),
|
||||
notifyByEmail: v.boolean(),
|
||||
notifyByChat: v.boolean(),
|
||||
notifyByChat: v.boolean()
|
||||
},
|
||||
returns: v.object({
|
||||
success: v.boolean(),
|
||||
alertId: v.id("alertConfigurations"),
|
||||
alertId: v.id('alertConfigurations')
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getUsuarioAutenticado(ctx);
|
||||
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) {
|
||||
// Atualizar alerta existente
|
||||
@@ -112,12 +110,12 @@ export const configurarAlerta = mutation({
|
||||
enabled: args.enabled,
|
||||
notifyByEmail: args.notifyByEmail,
|
||||
notifyByChat: args.notifyByChat,
|
||||
lastModified: Date.now(),
|
||||
lastModified: Date.now()
|
||||
});
|
||||
alertId = args.alertId;
|
||||
} else {
|
||||
// Criar novo alerta
|
||||
alertId = await ctx.db.insert("alertConfigurations", {
|
||||
alertId = await ctx.db.insert('alertConfigurations', {
|
||||
metricName: args.metricName,
|
||||
threshold: args.threshold,
|
||||
operator: args.operator,
|
||||
@@ -125,15 +123,15 @@ export const configurarAlerta = mutation({
|
||||
notifyByEmail: args.notifyByEmail,
|
||||
notifyByChat: args.notifyByChat,
|
||||
createdBy: usuario._id,
|
||||
lastModified: Date.now(),
|
||||
lastModified: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
alertId,
|
||||
alertId
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -143,27 +141,27 @@ export const listarAlertas = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("alertConfigurations"),
|
||||
_id: v.id('alertConfigurations'),
|
||||
metricName: v.string(),
|
||||
threshold: v.number(),
|
||||
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(),
|
||||
notifyByEmail: v.boolean(),
|
||||
notifyByChat: v.boolean(),
|
||||
createdBy: v.id("usuarios"),
|
||||
lastModified: v.number(),
|
||||
createdBy: v.id('usuarios'),
|
||||
lastModified: v.number()
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const alertas = await ctx.db.query("alertConfigurations").collect();
|
||||
const alertas = await ctx.db.query('alertConfigurations').collect();
|
||||
return alertas;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -174,11 +172,11 @@ export const obterMetricas = query({
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number()),
|
||||
metricName: v.optional(v.string()),
|
||||
limit: v.optional(v.number()),
|
||||
limit: v.optional(v.number())
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("systemMetrics"),
|
||||
_id: v.id('systemMetrics'),
|
||||
timestamp: v.number(),
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
@@ -187,7 +185,7 @@ export const obterMetricas = query({
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number())
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -197,28 +195,26 @@ export const obterMetricas = query({
|
||||
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")
|
||||
.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")
|
||||
.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")
|
||||
.query('systemMetrics')
|
||||
.withIndex('by_timestamp', (q) => q.lte('timestamp', fim))
|
||||
.order('desc')
|
||||
.collect();
|
||||
} else {
|
||||
metricas = await ctx.db.query("systemMetrics").order("desc").collect();
|
||||
metricas = await ctx.db.query('systemMetrics').order('desc').collect();
|
||||
}
|
||||
|
||||
// Limitar resultados
|
||||
@@ -227,7 +223,7 @@ export const obterMetricas = query({
|
||||
}
|
||||
|
||||
return metricas;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -237,7 +233,7 @@ export const obterMetricasRecentes = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("systemMetrics"),
|
||||
_id: v.id('systemMetrics'),
|
||||
timestamp: v.number(),
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
@@ -246,20 +242,20 @@ export const obterMetricasRecentes = query({
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: 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")
|
||||
.query('systemMetrics')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
|
||||
.order('desc')
|
||||
.take(100);
|
||||
|
||||
return metricas;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -269,7 +265,7 @@ export const obterUltimaMetrica = query({
|
||||
args: {},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id("systemMetrics"),
|
||||
_id: v.id('systemMetrics'),
|
||||
timestamp: v.number(),
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
@@ -278,18 +274,15 @@ export const obterUltimaMetrica = query({
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number())
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const metrica = await ctx.db
|
||||
.query("systemMetrics")
|
||||
.order("desc")
|
||||
.first();
|
||||
const metrica = await ctx.db.query('systemMetrics').order('desc').first();
|
||||
|
||||
return metrica || null;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -297,7 +290,7 @@ export const obterUltimaMetrica = query({
|
||||
*/
|
||||
export const verificarAlertasInternal = internalMutation({
|
||||
args: {
|
||||
metricId: v.id("systemMetrics"),
|
||||
metricId: v.id('systemMetrics')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -306,32 +299,32 @@ export const verificarAlertasInternal = internalMutation({
|
||||
|
||||
// Buscar configurações de alerta ativas
|
||||
const alertasAtivos = await ctx.db
|
||||
.query("alertConfigurations")
|
||||
.withIndex("by_enabled", (q) => q.eq("enabled", true))
|
||||
.query('alertConfigurations')
|
||||
.withIndex('by_enabled', (q) => q.eq('enabled', true))
|
||||
.collect();
|
||||
|
||||
for (const alerta of alertasAtivos) {
|
||||
// Obter valor da métrica correspondente, validando tipo número
|
||||
const rawValue = (metrica as Record<string, unknown>)[alerta.metricName];
|
||||
if (typeof rawValue !== "number") continue;
|
||||
if (typeof rawValue !== 'number') continue;
|
||||
const metricValue = rawValue;
|
||||
|
||||
// Verificar se o alerta deve ser disparado
|
||||
let shouldTrigger = false;
|
||||
switch (alerta.operator) {
|
||||
case ">":
|
||||
case '>':
|
||||
shouldTrigger = metricValue > alerta.threshold;
|
||||
break;
|
||||
case "<":
|
||||
case '<':
|
||||
shouldTrigger = metricValue < alerta.threshold;
|
||||
break;
|
||||
case ">=":
|
||||
case '>=':
|
||||
shouldTrigger = metricValue >= alerta.threshold;
|
||||
break;
|
||||
case "<=":
|
||||
case '<=':
|
||||
shouldTrigger = metricValue <= alerta.threshold;
|
||||
break;
|
||||
case "==":
|
||||
case '==':
|
||||
shouldTrigger = metricValue === alerta.threshold;
|
||||
break;
|
||||
}
|
||||
@@ -340,51 +333,51 @@ export const verificarAlertasInternal = internalMutation({
|
||||
// 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)
|
||||
.query('alertHistory')
|
||||
.withIndex('by_config', (q) =>
|
||||
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();
|
||||
|
||||
// Se já existe alerta recente, não disparar novamente
|
||||
if (alertaRecente) continue;
|
||||
|
||||
// Registrar alerta no histórico
|
||||
await ctx.db.insert("alertHistory", {
|
||||
await ctx.db.insert('alertHistory', {
|
||||
configId: alerta._id,
|
||||
metricName: alerta.metricName,
|
||||
metricValue,
|
||||
threshold: alerta.threshold,
|
||||
timestamp: Date.now(),
|
||||
status: "triggered",
|
||||
status: 'triggered',
|
||||
notificationsSent: {
|
||||
email: alerta.notifyByEmail,
|
||||
chat: alerta.notifyByChat,
|
||||
},
|
||||
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))
|
||||
.query('roles')
|
||||
.filter((q) => q.lte(q.field('nivel'), 1))
|
||||
.collect();
|
||||
|
||||
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
|
||||
|
||||
const usuarios = await ctx.db.query("usuarios").collect();
|
||||
const 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", {
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId: usuario._id,
|
||||
tipo: "nova_mensagem",
|
||||
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(),
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -397,7 +390,7 @@ export const verificarAlertasInternal = internalMutation({
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -407,16 +400,16 @@ export const gerarRelatorio = query({
|
||||
args: {
|
||||
dataInicio: v.number(),
|
||||
dataFim: v.number(),
|
||||
metricNames: v.optional(v.array(v.string())),
|
||||
metricNames: v.optional(v.array(v.string()))
|
||||
},
|
||||
returns: v.object({
|
||||
periodo: v.object({
|
||||
inicio: v.number(),
|
||||
fim: v.number(),
|
||||
fim: v.number()
|
||||
}),
|
||||
metricas: v.array(
|
||||
v.object({
|
||||
_id: v.id("systemMetrics"),
|
||||
_id: v.id('systemMetrics'),
|
||||
timestamp: v.number(),
|
||||
cpuUsage: v.optional(v.number()),
|
||||
memoryUsage: v.optional(v.number()),
|
||||
@@ -425,58 +418,74 @@ export const gerarRelatorio = query({
|
||||
usuariosOnline: v.optional(v.number()),
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number())
|
||||
})
|
||||
),
|
||||
estatisticas: v.object({
|
||||
cpuUsage: v.optional(v.object({
|
||||
cpuUsage: v.optional(
|
||||
v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
memoryUsage: v.optional(v.object({
|
||||
avg: v.number()
|
||||
})
|
||||
),
|
||||
memoryUsage: v.optional(
|
||||
v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
networkLatency: v.optional(v.object({
|
||||
avg: v.number()
|
||||
})
|
||||
),
|
||||
networkLatency: v.optional(
|
||||
v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
storageUsed: v.optional(v.object({
|
||||
avg: v.number()
|
||||
})
|
||||
),
|
||||
storageUsed: v.optional(
|
||||
v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
usuariosOnline: v.optional(v.object({
|
||||
avg: v.number()
|
||||
})
|
||||
),
|
||||
usuariosOnline: v.optional(
|
||||
v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
mensagensPorMinuto: v.optional(v.object({
|
||||
avg: v.number()
|
||||
})
|
||||
),
|
||||
mensagensPorMinuto: v.optional(
|
||||
v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
tempoRespostaMedio: v.optional(v.object({
|
||||
avg: v.number()
|
||||
})
|
||||
),
|
||||
tempoRespostaMedio: v.optional(
|
||||
v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: v.number(),
|
||||
})),
|
||||
errosCount: v.optional(v.object({
|
||||
avg: v.number()
|
||||
})
|
||||
),
|
||||
errosCount: v.optional(
|
||||
v.object({
|
||||
min: v.number(),
|
||||
max: v.number(),
|
||||
avg: 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)
|
||||
.query('systemMetrics')
|
||||
.withIndex('by_timestamp', (q) =>
|
||||
q.gte('timestamp', args.dataInicio).lte('timestamp', args.dataFim)
|
||||
)
|
||||
.collect();
|
||||
|
||||
@@ -488,7 +497,7 @@ export const gerarRelatorio = query({
|
||||
return {
|
||||
min: Math.min(...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(
|
||||
metricas.map((m) => m.errosCount).filter((v) => v !== undefined) as number[]
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
return {
|
||||
periodo: {
|
||||
inicio: args.dataInicio,
|
||||
fim: args.dataFim,
|
||||
fim: args.dataFim
|
||||
},
|
||||
metricas,
|
||||
estatisticas,
|
||||
estatisticas
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -535,15 +544,15 @@ export const gerarRelatorio = query({
|
||||
*/
|
||||
export const deletarAlerta = mutation({
|
||||
args: {
|
||||
alertId: v.id("alertConfigurations"),
|
||||
alertId: v.id('alertConfigurations')
|
||||
},
|
||||
returns: v.object({
|
||||
success: v.boolean(),
|
||||
success: v.boolean()
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.alertId);
|
||||
return { success: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -551,31 +560,177 @@ export const deletarAlerta = mutation({
|
||||
*/
|
||||
export const obterHistoricoAlertas = query({
|
||||
args: {
|
||||
limit: v.optional(v.number()),
|
||||
limit: v.optional(v.number())
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("alertHistory"),
|
||||
configId: v.id("alertConfigurations"),
|
||||
_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")),
|
||||
status: v.union(v.literal('triggered'), v.literal('resolved')),
|
||||
notificationsSent: v.object({
|
||||
email: v.boolean(),
|
||||
chat: 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);
|
||||
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 };
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user