Add system alerts log with nav badge and Admin Alerts tab.
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user