chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import { SearchableSelect } from '../components/SearchableSelect';
import { useEffect, useState } from 'react';
import type { FormEvent } from 'react';
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { getAuditEvent, listAudit } from '../lib/api';
import type { AuditEventDetail, AuditEventSummary, PageMeta } from '../lib/api';
import { actionLabel, formatDate } from '../lib/format';
interface AuditFilters {
search: string;
action: string;
source: string;
from: string;
to: string;
}
const emptyFilters: AuditFilters = { search: '', action: '', source: '', from: '', to: '' };
export function AuditPage() {
const [events, setEvents] = useState<AuditEventSummary[]>([]);
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
const [draft, setDraft] = useState<AuditFilters>(emptyFilters);
const [filters, setFilters] = useState<AuditFilters>(emptyFilters);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [detail, setDetail] = useState<AuditEventDetail | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
useEffect(() => {
setLoading(true); setError('');
listAudit({
page, pageSize: 25, search: filters.search, action: filters.action,
source: filters.source,
from: filters.from ? `${filters.from}T00:00:00.000Z` : undefined,
to: filters.to ? `${filters.to}T23:59:59.999Z` : undefined,
}).then((response) => { setEvents(response.data); setMeta(response.meta); })
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [filters, page]);
const apply = (event: FormEvent) => { event.preventDefault(); setPage(1); setFilters({ ...draft }); };
const clear = () => { setDraft(emptyFilters); setFilters(emptyFilters); setPage(1); };
const set = (key: keyof AuditFilters, value: string) => setDraft((current) => ({ ...current, [key]: value }));
const openDetail = async (id: string) => {
setDetail(null); setDetailLoading(true); setError('');
try { setDetail(await getAuditEvent(id)); }
catch (requestError) { setError(errorMessage(requestError)); }
finally { setDetailLoading(false); }
};
return <section>
<div className="page-heading"><div><span className="eyebrow">TRAZABILIDAD</span><h1>Auditoría</h1><p>Registro central de accesos y cambios administrativos.</p></div><span className="count-pill large">{meta.total} eventos</span></div>
<form className="audit-filters panel" onSubmit={apply}>
<label className="search-field"><Icon name="search" /><input value={draft.search} onChange={(event) => set('search', event.target.value)} placeholder="Usuario, entidad, acción o request ID" /></label>
<label className="field compact-field"><span>Acción</span><input value={draft.action} onChange={(event) => set('action', event.target.value)} placeholder="Ej. USER_UPDATED" /></label>
<label className="field compact-field"><span>Origen</span><SearchableSelect value={draft.source} onChange={(event) => set('source', event.target.value)}><option value="">Todos</option><option>WEB</option><option>ANDROID</option><option>SYSTEM</option><option>IMPORT</option></SearchableSelect></label>
<label className="field compact-field"><span>Desde</span><input type="date" value={draft.from} onChange={(event) => set('from', event.target.value)} /></label>
<label className="field compact-field"><span>Hasta</span><input type="date" value={draft.to} onChange={(event) => set('to', event.target.value)} /></label>
<div className="filter-actions"><button className="button text" type="button" onClick={clear}>Limpiar</button><button className="button primary">Aplicar filtros</button></div>
</form>
{error && <Alert>{error}</Alert>}
{loading ? <LoadingBlock label="Consultando auditoría…" /> : events.length === 0 ? <EmptyState title="Sin eventos" text="No hay registros para los filtros seleccionados." /> : <div className="table-panel audit-table"><div className="table-scroll"><table><thead><tr><th>Fecha y hora</th><th>Usuario</th><th>Acción</th><th>Entidad</th><th>Origen</th><th>Request ID</th><th /></tr></thead><tbody>{events.map((event) => <tr key={event.id}><td>{formatDate(event.occurredAt)}</td><td><strong>{event.actorUsername ?? 'Sistema'}</strong><small className="cell-subtext">{event.ip ?? 'IP no registrada'}</small></td><td><span className={`event-badge ${event.action.includes('FAILED') || event.action.includes('REUSE') ? 'warning' : ''}`}>{actionLabel(event.action)}</span><small className="cell-subtext code-text">{event.action}</small></td><td>{event.entityType ?? '—'}<small className="cell-subtext code-text">{event.entityId ?? ''}</small></td><td><span className="tag">{event.source}</span></td><td><code>{event.requestId ? `${event.requestId.slice(0, 12)}` : '—'}</code></td><td><button className="icon-button" onClick={() => openDetail(event.id)} aria-label="Ver detalle"><Icon name="chevron" /></button></td></tr>)}</tbody></table></div><div className="pagination"><button className="button secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}>Anterior</button><span>Página {page} de {Math.max(meta.totalPages, 1)}</span><button className="button secondary" disabled={page >= meta.totalPages} onClick={() => setPage(page + 1)}>Siguiente</button></div></div>}
{(detailLoading || detail) && <div className="modal-backdrop" onMouseDown={(event) => { if (event.target === event.currentTarget && !detailLoading) setDetail(null); }}><aside className="detail-drawer" role="dialog" aria-modal="true" aria-label="Detalle de auditoría">{detailLoading ? <LoadingBlock label="Cargando detalle…" /> : detail && <><div className="drawer-heading"><div><span className="eyebrow">EVENTO DE AUDITORÍA</span><h2>{actionLabel(detail.action)}</h2></div><button className="icon-button" onClick={() => setDetail(null)} aria-label="Cerrar">×</button></div><dl className="detail-list"><div><dt>Fecha</dt><dd>{formatDate(detail.occurredAt)}</dd></div><div><dt>Usuario</dt><dd>{detail.actorUsername ?? 'Sistema'}</dd></div><div><dt>Origen / IP</dt><dd>{detail.source} · {detail.ip ?? '—'}</dd></div><div><dt>Entidad</dt><dd>{detail.entityType ?? '—'} {detail.entityId ? `· ${detail.entityId}` : ''}</dd></div><div><dt>Request ID</dt><dd><code>{detail.requestId ?? '—'}</code></dd></div><div><dt>User agent</dt><dd>{detail.userAgent ?? '—'}</dd></div></dl><JsonDetail title="Datos anteriores" value={detail.beforeData} /><JsonDetail title="Datos posteriores" value={detail.afterData} /><JsonDetail title="Metadatos" value={detail.metadata} /></>}</aside></div>}
</section>;
}
function JsonDetail({ title, value }: { title: string; value: Record<string, unknown> | null }) {
if (!value) return null;
return <details className="json-detail" open><summary>{title}</summary><pre>{JSON.stringify(value, null, 2)}</pre></details>;
}