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
+87
View File
@@ -1,9 +1,11 @@
import { useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '../../stores/authStore';
import { check as healthCheck } from '../../api/health';
import { getRunningJobs } from '../../api/jobs';
import { useMarketRegime } from '../../hooks/useMarketRegime';
import { useSystemEventSummary, useAcknowledgeSystemEvents } from '../../hooks/useAdmin';
import { regimeDot, regimeHeadline } from '../../lib/regime';
import TickerSearch from './TickerSearch';
@@ -24,6 +26,7 @@ const linkClasses = (isActive: boolean) =>
/** Desktop command bar — the mockup's top navigation. MobileNav covers <lg. */
export default function TopBar() {
const { role, username, logout } = useAuthStore();
const [alertsOpen, setAlertsOpen] = useState(false);
const health = useQuery({
queryKey: ['health'],
@@ -42,6 +45,11 @@ export default function TopBar() {
enabled: isBackendUp,
});
const eventSummary = useSystemEventSummary(7);
const ackEvents = useAcknowledgeSystemEvents();
const unacked = eventSummary.data?.unacknowledged ?? 0;
const unackedErrors = eventSummary.data?.unacknowledged_errors ?? 0;
const running = jobs.data?.running ?? [];
const regime = useMarketRegime();
@@ -108,6 +116,85 @@ export default function TopBar() {
</span>
</span>
)}
{/* System alerts badge — open issues from jobs / ingestion */}
<div className="relative">
<button
type="button"
onClick={() => setAlertsOpen((v) => !v)}
className={`relative flex items-center gap-1.5 rounded-md px-1.5 py-1 text-[11px] transition-colors ${
unacked > 0
? unackedErrors > 0
? 'text-red-300 hover:bg-red-500/10'
: 'text-amber-300 hover:bg-amber-500/10'
: 'text-gray-500 hover:bg-white/[0.04] hover:text-gray-300'
}`}
title={unacked > 0 ? `${unacked} open system alert${unacked === 1 ? '' : 's'}` : 'No open system alerts'}
aria-label={unacked > 0 ? `${unacked} open system alerts` : 'System alerts'}
aria-expanded={alertsOpen}
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.75}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 17h5l-1.4-1.4A2 2 0 0118 14.2V11a6 6 0 10-12 0v3.2c0 .5-.2 1-.6 1.4L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
{unacked > 0 && (
<span className={`num min-w-[1.1rem] rounded-full px-1 text-center text-[10px] font-semibold ${
unackedErrors > 0 ? 'bg-red-500/25 text-red-200' : 'bg-amber-500/25 text-amber-100'
}`}>
{unacked > 99 ? '99+' : unacked}
</span>
)}
</button>
{alertsOpen && (
<>
<button
type="button"
className="fixed inset-0 z-40 cursor-default"
aria-label="Close alerts"
onClick={() => setAlertsOpen(false)}
/>
<div className="absolute right-0 top-full z-50 mt-2 w-72 rounded-xl border border-white/[0.08] bg-[#12141c] p-3 shadow-xl">
<p className="text-[11px] font-medium text-gray-300">
{unacked > 0
? `${unacked} open alert${unacked === 1 ? '' : 's'} (last 7 days)`
: 'No open alerts'}
</p>
{eventSummary.data && eventSummary.data.total > 0 && (
<p className="mt-0.5 text-[10px] text-gray-500">
{eventSummary.data.unacknowledged_errors} error
{eventSummary.data.unacknowledged_errors === 1 ? '' : 's'}
{' · '}
{eventSummary.data.unacknowledged_warnings} warning
{eventSummary.data.unacknowledged_warnings === 1 ? '' : 's'} open
</p>
)}
<div className="mt-3 flex flex-col gap-1.5">
{role === 'admin' && (
<NavLink
to="/admin"
onClick={() => setAlertsOpen(false)}
className="rounded-md px-2 py-1.5 text-[12px] text-blue-300 transition-colors hover:bg-white/[0.04]"
>
View in Admin Alerts
</NavLink>
)}
<button
type="button"
disabled={unacked === 0 || ackEvents.isPending}
onClick={() => {
ackEvents.mutate(7, {
onSuccess: () => setAlertsOpen(false),
});
}}
className="rounded-md px-2 py-1.5 text-left text-[12px] text-gray-300 transition-colors hover:bg-white/[0.04] disabled:opacity-40"
>
{ackEvents.isPending ? 'Dismissing…' : 'Dismiss open alerts'}
</button>
</div>
</div>
</>
)}
</div>
<span
className={`inline-block h-2 w-2 rounded-full ${
isBackendUp ? 'bg-emerald-400 shadow-lg shadow-emerald-400/50' : 'bg-red-400 shadow-lg shadow-red-400/50'