feat: implement error handling and logging in server hooks to capture and notify on 404 and 500 errors, enhancing server reliability and monitoring

This commit is contained in:
2025-12-08 11:52:27 -03:00
parent e1f1af7530
commit fdfbd8b051
31 changed files with 7305 additions and 932 deletions

View File

@@ -13,7 +13,8 @@
Clock,
Video,
Building,
Info
Info,
AlertCircle
} from 'lucide-svelte';
type HighlightVariant = 'solid' | 'outline';
type FeatureIcon =
@@ -29,7 +30,8 @@
| 'userPlus'
| 'clock'
| 'video'
| 'building';
| 'building'
| 'alertCircle';
type PaletteKey = 'primary' | 'success' | 'secondary' | 'accent' | 'info' | 'error' | 'warning';
type TiRouteId =
@@ -43,6 +45,7 @@
| '/(dashboard)/ti/solicitacoes-acesso'
| '/(dashboard)/ti/times'
| '/(dashboard)/ti/notificacoes'
| '/(dashboard)/ti/erros-servidor'
| '/(dashboard)/ti/monitoramento'
| '/(dashboard)/ti/configuracoes-ponto'
| '/(dashboard)/ti/configuracoes-relogio'
@@ -151,7 +154,8 @@
userPlus: UserPlus,
clock: Clock,
video: Video,
building: Building
building: Building,
alertCircle: AlertCircle
};
// Removido: iconPaths substituído por iconComponents com Lucide
@@ -294,6 +298,19 @@
palette: 'success',
icon: 'shieldCheck'
},
{
title: 'Erros do Servidor',
description:
'Monitoramento e análise de erros 404 e 500 do servidor. Visualize histórico, estatísticas e detalhes dos erros registrados.',
ctaLabel: 'Ver Erros',
href: '/(dashboard)/ti/erros-servidor',
palette: 'error',
icon: 'alertCircle',
highlightBadges: [
{ label: '404/500', variant: 'solid' },
{ label: 'Monitoramento', variant: 'outline' }
]
},
{
title: 'LGPD - Proteção de Dados',
description:

View File

@@ -0,0 +1,531 @@
<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { resolve } from '$app/paths';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import {
AlertCircle,
FileX,
Server,
Filter,
Search,
Calendar,
X,
Info,
CheckCircle,
Clock,
ExternalLink,
Copy
} from 'lucide-svelte';
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
let limite = $state(50);
let mostrarFiltros = $state(false);
let erroSelecionado = $state<any>(null);
let copiado = $state(false);
// Filtros
let filtroStatusCode = $state<number | undefined>(undefined);
let filtroNotificado = $state<boolean | undefined>(undefined);
let filtroDataInicio = $state<string>('');
let filtroDataFim = $state<string>('');
const client = useConvexClient();
// Argumentos para as queries (usando $derived para reatividade)
const argsErros = $derived({
limite,
statusCode: filtroStatusCode,
notificado: filtroNotificado,
dataInicio: filtroDataInicio ? new Date(filtroDataInicio).getTime() : undefined,
dataFim: filtroDataFim ? new Date(filtroDataFim + 'T23:59:59').getTime() : undefined
});
const argsEstatisticas = $derived({
dataInicio: filtroDataInicio ? new Date(filtroDataInicio).getTime() : undefined,
dataFim: filtroDataFim ? new Date(filtroDataFim + 'T23:59:59').getTime() : undefined
});
// Query reativa
const errosQuery = useQuery(api.errosServidor.listarErros, argsErros);
const estatisticasQuery = useQuery(api.errosServidor.obterEstatisticasErros, argsEstatisticas);
// Extrair dados e erros
const erros = $derived(errosQuery?.data ?? []);
const estatisticas = $derived(estatisticasQuery?.data);
const carregando = $derived(errosQuery === undefined || estatisticasQuery === undefined);
// Detectar erros nas queries
let erroErros = $state<string | null>(null);
let erroEstatisticas = $state<string | null>(null);
// Monitorar erros nas queries
$effect(() => {
try {
// Debug: log do estado da query
if (import.meta.env.DEV) {
console.log('🔍 [Erros Servidor] Estado da query de erros:', {
undefined: errosQuery === undefined,
hasData: errosQuery?.data !== undefined,
hasError: errosQuery && typeof errosQuery === 'object' && 'error' in errosQuery,
dataLength: Array.isArray(errosQuery?.data) ? errosQuery.data.length : 'N/A'
});
}
if (errosQuery && typeof errosQuery === 'object' && 'error' in errosQuery) {
const error = (errosQuery as { error?: unknown }).error;
if (error !== undefined && error !== null) {
erroErros = error instanceof Error ? error.message : String(error);
console.error('❌ Erro na query de erros:', error);
} else {
erroErros = null;
}
} else if (errosQuery?.data !== undefined) {
erroErros = null;
if (import.meta.env.DEV) {
console.log(
`✅ [Erros Servidor] Query de erros carregada: ${Array.isArray(errosQuery.data) ? errosQuery.data.length : 0} erros encontrados`
);
}
}
} catch (e) {
console.error('Erro ao processar query de erros:', e);
erroErros = e instanceof Error ? e.message : String(e);
}
});
$effect(() => {
try {
if (estatisticasQuery && typeof estatisticasQuery === 'object' && 'error' in estatisticasQuery) {
const error = (estatisticasQuery as { error?: unknown }).error;
if (error !== undefined && error !== null) {
erroEstatisticas = error instanceof Error ? error.message : String(error);
console.error('❌ Erro na query de estatísticas:', error);
} else {
erroEstatisticas = null;
}
} else if (estatisticasQuery?.data !== undefined) {
erroEstatisticas = null;
}
} catch (e) {
console.error('Erro ao processar query de estatísticas:', e);
erroEstatisticas = e instanceof Error ? e.message : String(e);
}
});
// Funções auxiliares
function formatarData(timestamp: number): string {
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm:ss", { locale: ptBR });
}
function obterCorStatusCode(statusCode: number): string {
if (statusCode === 404) return 'badge-warning';
if (statusCode >= 500) return 'badge-error';
return 'badge-info';
}
function obterIconeStatusCode(statusCode: number) {
if (statusCode === 404) return FileX;
if (statusCode >= 500) return AlertCircle;
return Server;
}
function limparFiltros() {
filtroStatusCode = undefined;
filtroNotificado = undefined;
filtroDataInicio = '';
filtroDataFim = '';
}
function copiarParaAreaTransferencia(texto: string) {
navigator.clipboard.writeText(texto).then(() => {
copiado = true;
setTimeout(() => {
copiado = false;
}, 2000);
});
}
function abrirDetalhes(erro: any) {
erroSelecionado = erro;
}
function fecharDetalhes() {
erroSelecionado = null;
}
</script>
<ProtectedRoute allowedRoles={['ti_master', 'ti_usuario', 'admin']} maxLevel={1}>
<div class="container mx-auto p-6">
<!-- Cabeçalho -->
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold">Erros do Servidor</h1>
<p class="text-base-content/70 mt-1">Monitoramento e análise de erros 404 e 500</p>
</div>
<button
class="btn btn-outline"
onclick={() => (mostrarFiltros = !mostrarFiltros)}
>
<Filter class="h-5 w-5" />
Filtros
</button>
</div>
<!-- Mensagens de Erro -->
{#if erroErros}
<div class="alert alert-error mb-6">
<AlertCircle class="h-6 w-6" />
<div>
<h3 class="font-bold">Erro ao carregar erros do servidor</h3>
<div class="text-sm">{erroErros}</div>
</div>
</div>
{/if}
{#if erroEstatisticas}
<div class="alert alert-warning mb-6">
<AlertCircle class="h-6 w-6" />
<div>
<h3 class="font-bold">Erro ao carregar estatísticas</h3>
<div class="text-sm">{erroEstatisticas}</div>
</div>
</div>
{/if}
<!-- Estatísticas -->
{#if estatisticas}
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="stat">
<div class="stat-title">Total de Erros</div>
<div class="stat-value text-2xl">{estatisticas.total}</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="stat">
<div class="stat-title">Notificados</div>
<div class="stat-value text-2xl text-success">{estatisticas.notificados}</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="stat">
<div class="stat-title">Não Notificados</div>
<div class="stat-value text-2xl text-warning">{estatisticas.naoNotificados}</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="stat">
<div class="stat-title">Últimas 24h</div>
<div class="stat-value text-2xl text-info">{estatisticas.ultimas24h}</div>
</div>
</div>
</div>
</div>
{/if}
<!-- Filtros -->
{#if mostrarFiltros}
<div class="card mb-6 bg-base-100 shadow">
<div class="card-body">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-semibold">Filtros</h3>
<button class="btn btn-ghost btn-sm" onclick={limparFiltros}>
<X class="h-4 w-4" />
Limpar
</button>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="form-control">
<label class="label">
<span class="label-text">Código HTTP</span>
</label>
<select
class="select select-bordered"
bind:value={filtroStatusCode}
onchange={(e) => {
filtroStatusCode = e.target.value ? Number(e.target.value) : undefined;
}}
>
<option value={undefined}>Todos</option>
<option value={404}>404 - Não Encontrado</option>
<option value={500}>500 - Erro Interno</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Status Notificação</span>
</label>
<select
class="select select-bordered"
bind:value={filtroNotificado}
onchange={(e) => {
filtroNotificado =
e.target.value === '' ? undefined : e.target.value === 'true';
}}
>
<option value={undefined}>Todos</option>
<option value={true}>Notificados</option>
<option value={false}>Não Notificados</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Data Início</span>
</label>
<input
type="date"
class="input input-bordered"
bind:value={filtroDataInicio}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Data Fim</span>
</label>
<input
type="date"
class="input input-bordered"
bind:value={filtroDataFim}
/>
</div>
</div>
</div>
</div>
{/if}
<!-- Tabela de Erros -->
<div class="card bg-base-100 shadow">
<div class="card-body">
{#if carregando}
<div class="flex justify-center p-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if erros.length === 0}
<div class="flex flex-col items-center justify-center p-8 text-center">
<CheckCircle class="text-success mb-4 h-16 w-16" />
<p class="text-lg font-semibold">Nenhum erro encontrado</p>
<p class="text-base-content/70 mb-4">
Não há erros registrados com os filtros aplicados.
</p>
<div class="bg-base-200 rounded-lg p-4 max-w-md text-left">
<p class="text-sm font-semibold mb-2">💡 Como funciona:</p>
<ul class="text-sm text-base-content/70 space-y-1 list-disc list-inside">
<li>Erros 404 e 500 são registrados automaticamente pelo sistema</li>
<li>Os erros aparecem aqui após ocorrerem no sistema</li>
<li>Você pode testar acessando uma URL inexistente para gerar um erro 404</li>
</ul>
</div>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Data/Hora</th>
<th>Código</th>
<th>URL</th>
<th>Método</th>
<th>Usuário</th>
<th>Notificado</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{#each erros as erro (erro._id)}
{@const StatusIcon = obterIconeStatusCode(erro.statusCode)}
<tr>
<td>
<div class="flex items-center gap-2">
<Clock class="h-4 w-4 text-base-content/50" />
<span class="text-sm">{formatarData(erro.criadoEm)}</span>
</div>
</td>
<td>
<span class="badge {obterCorStatusCode(erro.statusCode)}">
<StatusIcon class="mr-1 h-3 w-3" />
{erro.statusCode}
</span>
</td>
<td>
<div class="max-w-xs truncate" title={erro.url || 'N/A'}>
{erro.url || 'N/A'}
</div>
</td>
<td>
<span class="badge badge-outline">{erro.method || 'N/A'}</span>
</td>
<td>{erro.usuarioNome || 'Anônimo'}</td>
<td>
{#if erro.notificado}
<span class="badge badge-success">
<CheckCircle class="mr-1 h-3 w-3" />
Sim
</span>
{:else}
<span class="badge badge-warning">
<Clock class="mr-1 h-3 w-3" />
Não
</span>
{/if}
</td>
<td>
<button
class="btn btn-ghost btn-sm"
onclick={() => abrirDetalhes(erro)}
>
<Info class="h-4 w-4" />
Detalhes
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
</div>
<!-- Modal de Detalhes -->
{#if erroSelecionado}
<div class="modal modal-open">
<div class="modal-box max-w-4xl">
<h3 class="mb-4 text-lg font-bold">Detalhes do Erro</h3>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="label">
<span class="label-text font-semibold">Código HTTP</span>
</label>
<span class="badge {obterCorStatusCode(erroSelecionado.statusCode)} text-lg">
{erroSelecionado.statusCode}
</span>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">Data/Hora</span>
</label>
<p>{formatarData(erroSelecionado.criadoEm)}</p>
</div>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">URL</span>
</label>
<div class="flex items-center gap-2">
<input
type="text"
class="input input-bordered flex-1"
value={erroSelecionado.url || 'N/A'}
readonly
/>
<button
class="btn btn-ghost btn-sm"
onclick={() => copiarParaAreaTransferencia(erroSelecionado.url || '')}
title="Copiar URL"
>
{#if copiado}
<CheckCircle class="h-4 w-4 text-success" />
{:else}
<Copy class="h-4 w-4" />
{/if}
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="label">
<span class="label-text font-semibold">Método HTTP</span>
</label>
<p>{erroSelecionado.method || 'N/A'}</p>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">IP Address</span>
</label>
<p>{erroSelecionado.ipAddress || 'N/A'}</p>
</div>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">Mensagem</span>
</label>
<div class="rounded-lg bg-base-200 p-3">
<p class="whitespace-pre-wrap">{erroSelecionado.mensagem || 'N/A'}</p>
</div>
</div>
{#if erroSelecionado.stack}
<div>
<label class="label">
<span class="label-text font-semibold">Stack Trace</span>
</label>
<div class="rounded-lg bg-base-200 p-3">
<pre class="max-h-96 overflow-auto whitespace-pre-wrap text-xs">{erroSelecionado.stack}</pre>
</div>
<button
class="btn btn-ghost btn-sm mt-2"
onclick={() => copiarParaAreaTransferencia(erroSelecionado.stack || '')}
>
<Copy class="mr-2 h-4 w-4" />
Copiar Stack Trace
</button>
</div>
{/if}
<div class="grid grid-cols-2 gap-4">
<div>
<label class="label">
<span class="label-text font-semibold">User Agent</span>
</label>
<p class="text-sm">{erroSelecionado.userAgent || 'N/A'}</p>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">Usuário</span>
</label>
<p>{erroSelecionado.usuarioNome || 'Anônimo'}</p>
</div>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">Status de Notificação</span>
</label>
{#if erroSelecionado.notificado}
<div class="flex items-center gap-2">
<CheckCircle class="text-success h-5 w-5" />
<span>Notificado em {erroSelecionado.notificadoEm ? formatarData(erroSelecionado.notificadoEm) : 'N/A'}</span>
</div>
{:else}
<div class="flex items-center gap-2">
<Clock class="text-warning h-5 w-5" />
<span>Pendente de notificação</span>
</div>
{/if}
</div>
</div>
<div class="modal-action">
<button class="btn" onclick={fecharDetalhes}>Fechar</button>
</div>
</div>
</div>
{/if}
</ProtectedRoute>