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:
@@ -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:
|
||||
|
||||
531
apps/web/src/routes/(dashboard)/ti/erros-servidor/+page.svelte
Normal file
531
apps/web/src/routes/(dashboard)/ti/erros-servidor/+page.svelte
Normal 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user