Add system alerts log with nav badge and Admin Alerts tab.
Deploy / lint (push) Successful in 8s
Deploy / test (push) Failing after 1m4s
Deploy / deploy (push) Has been skipped

Persist job and ingestion warnings/errors for 7 days, surface a dismissible top-nav badge, treat stale OHLCV as a warning (e.g. ticker renames), and show market bar age on the ticker freshness chip.
This commit is contained in:
2026-07-14 13:38:04 +02:00
parent ed82d0a665
commit f59c0c3484
16 changed files with 825 additions and 7 deletions
@@ -0,0 +1,96 @@
import { useSystemEvents, useAcknowledgeSystemEvents } from '../../hooks/useAdmin';
import { SkeletonTable } from '../ui/Skeleton';
import { Button } from '../ui/Button';
function formatWhen(iso: string | null): string {
if (!iso) return '—';
const d = new Date(iso);
const mins = Math.floor((Date.now() - d.getTime()) / 60_000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
if (days < 7) return `${days}d ago`;
return d.toLocaleString();
}
function severityClass(severity: string): string {
if (severity === 'error') return 'text-red-300 border-red-400/30 bg-red-500/10';
return 'text-amber-300 border-amber-400/30 bg-amber-500/10';
}
export function SystemEventsPanel() {
const { data: events, isLoading, isError } = useSystemEvents(7);
const ack = useAcknowledgeSystemEvents();
const unacked = (events ?? []).filter((e) => !e.acknowledged_at).length;
if (isLoading) return <SkeletonTable rows={4} cols={3} />;
if (isError) {
return (
<div className="glass p-4 text-sm text-red-300">
Failed to load system events.
</div>
);
}
const rows = events ?? [];
return (
<div className="glass p-5">
<div className="mb-3 flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="text-xs font-medium uppercase tracking-widest text-gray-500">
System alerts
</h3>
<p className="mt-1 text-[11.5px] text-gray-500">
Operational warnings and errors jobs, ingestion, renames/stale data, and other sources · last 7 days
{unacked > 0 ? ` · ${unacked} open` : ''}
</p>
</div>
<Button
variant="ghost"
onClick={() => ack.mutate(7)}
disabled={ack.isPending || unacked === 0}
>
{ack.isPending ? 'Dismissing…' : 'Dismiss open alerts'}
</Button>
</div>
{rows.length === 0 ? (
<p className="text-sm text-gray-500">No warnings or errors in the last 7 days.</p>
) : (
<ul className="divide-y divide-white/[0.05]">
{rows.map((e) => (
<li
key={e.id}
className={`flex flex-col gap-1 py-2.5 sm:flex-row sm:items-start sm:justify-between sm:gap-4 ${
e.acknowledged_at ? 'opacity-50' : ''
}`}
>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span
className={`num rounded border px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${severityClass(e.severity)}`}
>
{e.severity}
</span>
<span className="num text-[11px] text-gray-500">{e.source}</span>
{e.symbol && (
<span className="num text-[11px] font-medium text-blue-300">{e.symbol}</span>
)}
{e.acknowledged_at && (
<span className="text-[10px] text-gray-600">dismissed</span>
)}
</div>
<p className="mt-1 text-[13px] leading-snug text-gray-200">{e.message}</p>
<p className="mt-0.5 num text-[10px] text-gray-600">{e.code}</p>
</div>
<span className="num shrink-0 text-[11px] text-gray-500">{formatWhen(e.created_at)}</span>
</li>
))}
</ul>
)}
</div>
);
}