- Introduced new system metrics tracking with the ability to save and retrieve metrics such as CPU usage, memory usage, and network latency. - Added alert configuration functionality, allowing users to set thresholds for metrics and receive notifications via email or chat. - Updated the sidebar component to include a new "Monitorar SGSE" card for real-time system monitoring. - Enhanced the package dependencies with `papaparse` and `svelte-chartjs` for improved data handling and charting capabilities. - Updated the schema to support new tables for system metrics and alert configurations.
446 lines
14 KiB
Svelte
446 lines
14 KiB
Svelte
<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";
|
|
|
|
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 generating = $state(false);
|
|
|
|
// Métricas selecionadas
|
|
let selectedMetrics = $state({
|
|
cpuUsage: true,
|
|
memoryUsage: true,
|
|
networkLatency: true,
|
|
storageUsed: true,
|
|
usuariosOnline: true,
|
|
mensagensPorMinuto: true,
|
|
tempoRespostaMedio: 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",
|
|
};
|
|
|
|
function setPeriod(type: string) {
|
|
periodType = type;
|
|
const now = new Date();
|
|
|
|
switch (type) {
|
|
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");
|
|
break;
|
|
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 fim = endOfDay(new Date(`${dataFim}T${horaFim}`)).getTime();
|
|
return { inicio, fim };
|
|
}
|
|
|
|
async function generatePDF() {
|
|
generating = true;
|
|
|
|
try {
|
|
const { inicio, fim } = getDateRange();
|
|
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
|
dataInicio: inicio,
|
|
dataFim: fim,
|
|
});
|
|
|
|
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);
|
|
|
|
// 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 })}`,
|
|
14,
|
|
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(`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);
|
|
yPos += 10;
|
|
|
|
const statsData: any[] = [];
|
|
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
|
if (selected && relatorio.estatisticas[metric]) {
|
|
const stats = relatorio.estatisticas[metric];
|
|
if (stats) {
|
|
statsData.push([
|
|
metricLabels[metric],
|
|
stats.min.toFixed(2),
|
|
stats.max.toFixed(2),
|
|
stats.avg.toFixed(2),
|
|
]);
|
|
}
|
|
}
|
|
});
|
|
|
|
autoTable(doc, {
|
|
startY: yPos,
|
|
head: [["Métrica", "Mínimo", "Máximo", "Média"]],
|
|
body: statsData,
|
|
theme: "striped",
|
|
headStyles: { fillColor: [102, 126, 234] },
|
|
});
|
|
|
|
// Dados detalhados (últimos 50 registros)
|
|
const finalY = (doc as any).lastAutoTable.finalY || yPos + 10;
|
|
yPos = finalY + 15;
|
|
|
|
doc.setFontSize(14);
|
|
doc.setTextColor(102, 126, 234);
|
|
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]) => {
|
|
if (selected) {
|
|
row.push((m[metric] || 0).toFixed(1));
|
|
}
|
|
});
|
|
return row;
|
|
});
|
|
|
|
const headers = ["Data/Hora"];
|
|
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
|
if (selected) {
|
|
headers.push(metricLabels[metric]);
|
|
}
|
|
});
|
|
|
|
autoTable(doc, {
|
|
startY: yPos,
|
|
head: [headers],
|
|
body: detailsData,
|
|
theme: "grid",
|
|
headStyles: { fillColor: [102, 126, 234] },
|
|
styles: { fontSize: 8 },
|
|
});
|
|
|
|
// Footer
|
|
const pageCount = doc.getNumberOfPages();
|
|
for (let i = 1; i <= pageCount; i++) {
|
|
doc.setPage(i);
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(128, 128, 128);
|
|
doc.text(
|
|
`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" }
|
|
);
|
|
}
|
|
|
|
// Salvar
|
|
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.");
|
|
} finally {
|
|
generating = false;
|
|
}
|
|
}
|
|
|
|
async function generateCSV() {
|
|
generating = true;
|
|
|
|
try {
|
|
const { inicio, fim } = getDateRange();
|
|
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
|
dataInicio: inicio,
|
|
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 }),
|
|
};
|
|
|
|
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
|
if (selected) {
|
|
row[metricLabels[metric]] = m[metric] || 0;
|
|
}
|
|
});
|
|
|
|
return row;
|
|
});
|
|
|
|
// Gerar CSV
|
|
const csv = Papa.unparse(csvData);
|
|
|
|
// Download
|
|
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("download", `relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.csv`);
|
|
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.");
|
|
} finally {
|
|
generating = false;
|
|
}
|
|
}
|
|
|
|
function toggleAllMetrics(value: boolean) {
|
|
Object.keys(selectedMetrics).forEach((key) => {
|
|
selectedMetrics[key] = value;
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<dialog class="modal modal-open">
|
|
<div class="modal-box max-w-3xl bg-gradient-to-br from-base-100 to-base-200">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-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>
|
|
|
|
<!-- Seleção de Período -->
|
|
<div class="card bg-base-100 shadow-xl mb-6">
|
|
<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">
|
|
<button
|
|
type="button"
|
|
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')}
|
|
>
|
|
Última Semana
|
|
</button>
|
|
<button
|
|
type="button"
|
|
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'}
|
|
>
|
|
Personalizado
|
|
</button>
|
|
</div>
|
|
|
|
{#if periodType === 'custom'}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label" for="dataInicio">
|
|
<span class="label-text font-semibold">Data Início</span>
|
|
</label>
|
|
<input
|
|
id="dataInicio"
|
|
type="date"
|
|
class="input input-bordered input-primary"
|
|
bind:value={dataInicio}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="horaInicio">
|
|
<span class="label-text font-semibold">Hora Início</span>
|
|
</label>
|
|
<input
|
|
id="horaInicio"
|
|
type="time"
|
|
class="input input-bordered input-primary"
|
|
bind:value={horaInicio}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="dataFim">
|
|
<span class="label-text font-semibold">Data Fim</span>
|
|
</label>
|
|
<input
|
|
id="dataFim"
|
|
type="date"
|
|
class="input input-bordered input-primary"
|
|
bind:value={dataFim}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="horaFim">
|
|
<span class="label-text font-semibold">Hora Fim</span>
|
|
</label>
|
|
<input
|
|
id="horaFim"
|
|
type="time"
|
|
class="input input-bordered input-primary"
|
|
bind:value={horaFim}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Seleção de Métricas -->
|
|
<div class="card bg-base-100 shadow-xl mb-6">
|
|
<div class="card-body">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h4 class="card-title text-xl">Métricas a Incluir</h4>
|
|
<div class="flex gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost"
|
|
onclick={() => toggleAllMetrics(true)}
|
|
>
|
|
Selecionar Todas
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost"
|
|
onclick={() => toggleAllMetrics(false)}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{#each Object.entries(metricLabels) as [metric, label]}
|
|
<label class="label cursor-pointer justify-start gap-3 hover:bg-base-200 rounded-lg p-2">
|
|
<input
|
|
type="checkbox"
|
|
class="checkbox checkbox-primary"
|
|
bind:checked={selectedMetrics[metric]}
|
|
/>
|
|
<span class="label-text">{label}</span>
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Botões de Exportação -->
|
|
<div class="flex gap-3 justify-end">
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline"
|
|
onclick={onClose}
|
|
disabled={generating}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
onclick={generateCSV}
|
|
disabled={generating || !Object.values(selectedMetrics).some(v => v)}
|
|
>
|
|
{#if generating}
|
|
<span class="loading loading-spinner"></span>
|
|
{:else}
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
{/if}
|
|
Exportar CSV
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
onclick={generatePDF}
|
|
disabled={generating || !Object.values(selectedMetrics).some(v => v)}
|
|
>
|
|
{#if generating}
|
|
<span class="loading loading-spinner"></span>
|
|
{:else}
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
</svg>
|
|
{/if}
|
|
Exportar PDF
|
|
</button>
|
|
</div>
|
|
|
|
{#if !Object.values(selectedMetrics).some(v => v)}
|
|
<div class="alert alert-warning mt-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<span>Selecione pelo menos uma métrica para gerar o relatório.</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
<form method="dialog" class="modal-backdrop" onclick={onClose}>
|
|
<button type="button">close</button>
|
|
</form>
|
|
</dialog>
|
|
|