feat: log de errores para el admin (backend + frontend)
Panel nuevo en Configuracion ("Errores del sistema", solo superadmin): errores de backend
(excepciones 500 no manejadas, con stack/ruta/metodo/usuario) y de frontend (errores JS y
promesas sin catch en el navegador del usuario, antes invisibles para cualquiera mas alla de esa
persona). Filtro por origen, boton para limpiar el historial.
Error Boundary de React: si algo rompe el render, ya no queda pantalla en blanco -- aviso simple
con boton de recargar, y el error queda reportado igual.
Nuevo error_logs (tabla, se recorta sola a 1000 filas) + GET/DELETE /api/error-logs (superadmin)
+ POST /api/error-logs (publico, rate-limited, para reportar incluso sin sesion).
Probado de punta a punta en dev: error real de backend (500) capturado con stack completo y
usuario correcto, error JS global y promesa rechazada capturados desde el navegador, panel
muestra ambos y el boton de limpiar funciona. Se encontro y corrigio un bug real en el camino: la
identificacion de usuario opcional leia jwt.payload.id en vez de .sub (como realmente lo firma
auth/service.js), asi que nunca identificaba a nadie.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { Component } from 'react';
|
||||
import { reportError } from '../services/errorLogs.js';
|
||||
|
||||
// Ultimo recurso ante un error de render de React (que si no, deja la pantalla en blanco sin
|
||||
// ninguna pista). No usa hooks a proposito (useUi podria fallar si el error viene de un provider
|
||||
// roto) — texto fijo, estilos inline con los tokens de color de la app.
|
||||
export class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
reportError({
|
||||
message: error?.message || 'Error de render',
|
||||
stack: `${error?.stack || ''}\n${info?.componentStack || ''}`.slice(0, 5000),
|
||||
url: window.location.href,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
justifyContent: 'center', gap: 12, padding: 24, textAlign: 'center',
|
||||
background: 'rgb(var(--canvas, 32 32 32))', color: 'rgb(var(--body, 230 230 230))',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0 }}>Algo salió mal</h2>
|
||||
<p style={{ margin: 0, opacity: .7, maxWidth: 420 }}>
|
||||
El error ya quedó registrado para el administrador. Probá recargar la página.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
style={{
|
||||
marginTop: 8, padding: '8px 18px', borderRadius: 6, border: 'none', cursor: 'pointer',
|
||||
background: 'rgb(var(--accent, 37 99 235))', color: '#fff', fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Recargar
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user