fix(web): guard malformed operational events

This commit is contained in:
root 2026-05-06 07:18:47 +00:00
parent 943cfda6b5
commit 257b10fc81
3 changed files with 26 additions and 10 deletions

View File

@ -63,9 +63,10 @@ function App() {
const showMarketplaceTab = isAdmin || tabFlags.marketplace; const showMarketplaceTab = isAdmin || tabFlags.marketplace;
// Critical system events (for the alert banner) // Critical system events (for the alert banner)
const recentCriticalEvents = (botState.operationalEvents ?? []).filter(e => const recentCriticalEvents = (botState.operationalEvents ?? []).filter((e): e is NonNullable<typeof e> =>
(e.severity === 'ERROR' || e.severity === 'WARN') && Boolean(e)
Date.now() - e.timestamp < 600_000 && (e.severity === 'ERROR' || e.severity === 'WARN')
&& Date.now() - e.timestamp < 600_000
); );
const hasCriticalEvents = recentCriticalEvents.length > 0; const hasCriticalEvents = recentCriticalEvents.length > 0;

View File

@ -185,6 +185,16 @@ export interface BotState {
}>; }>;
} }
function isOperationalEventRecord(value: unknown): value is NonNullable<BotState['operationalEvents']>[number] {
if (!value || typeof value !== 'object') return false;
const event = value as Record<string, unknown>;
return typeof event.id === 'string'
&& typeof event.type === 'string'
&& typeof event.severity === 'string'
&& typeof event.message === 'string'
&& typeof event.timestamp === 'number';
}
export const DEFAULT_BOT_STATE: BotState = { export const DEFAULT_BOT_STATE: BotState = {
symbols: {}, symbols: {},
positions: [], positions: [],
@ -381,6 +391,10 @@ export const useWebSocket = (url: string) => {
}); });
newSocket.on('operational_event', (event: any) => { newSocket.on('operational_event', (event: any) => {
if (!isOperationalEventRecord(event)) {
console.warn('Ignoring malformed operational_event payload', event);
return;
}
setBotState(prev => ({ setBotState(prev => ({
...prev, ...prev,
operationalEvents: [event, ...(prev.operationalEvents || [])].slice(0, EVENT_BUFFER_LIMIT) operationalEvents: [event, ...(prev.operationalEvents || [])].slice(0, EVENT_BUFFER_LIMIT)

View File

@ -75,6 +75,7 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
const observabilityHealth = (botState.health || {}) as any; const observabilityHealth = (botState.health || {}) as any;
const tradingControl = botState.health?.tradingControl; const tradingControl = botState.health?.tradingControl;
const isPaused = tradingControl?.mode === 'PAUSED'; const isPaused = tradingControl?.mode === 'PAUSED';
const operationalEvents = (botState.operationalEvents ?? []).filter((event): event is NonNullable<typeof event> => Boolean(event));
const formatDuration = (ms?: number) => { const formatDuration = (ms?: number) => {
if (!ms || !Number.isFinite(ms)) return 'Idle'; if (!ms || !Number.isFinite(ms)) return 'Idle';
@ -376,18 +377,18 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
</button> </button>
</div> </div>
<span className="text-[10px] text-zinc-500 font-mono"> <span className="text-[10px] text-zinc-500 font-mono">
Buffer: {botState.operationalEvents?.length || 0} events Buffer: {operationalEvents.length || 0} events
</span> </span>
</div> </div>
{/* 24h Severity Summary Bar */} {/* 24h Severity Summary Bar */}
{botState.operationalEvents && botState.operationalEvents.length > 0 && ( {operationalEvents.length > 0 && (
<div className="px-5 py-3 bg-white/[0.01] border-b border-white/[0.04] flex items-center gap-6"> <div className="px-5 py-3 bg-white/[0.01] border-b border-white/[0.04] flex items-center gap-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-red-500" /> <div className="w-1.5 h-1.5 rounded-full bg-red-500" />
<span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold"> <span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">
Errors: <span className="text-red-400 text-xs ml-1"> Errors: <span className="text-red-400 text-xs ml-1">
{botState.operationalEvents.filter(e => e.severity === 'ERROR' && (Date.now() - e.timestamp < 86400000)).length} {operationalEvents.filter(e => e.severity === 'ERROR' && (Date.now() - e.timestamp < 86400000)).length}
</span> </span>
</span> </span>
</div> </div>
@ -395,7 +396,7 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
<div className="w-1.5 h-1.5 rounded-full bg-orange-500" /> <div className="w-1.5 h-1.5 rounded-full bg-orange-500" />
<span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold"> <span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">
Warnings: <span className="text-orange-400 text-xs ml-1"> Warnings: <span className="text-orange-400 text-xs ml-1">
{botState.operationalEvents.filter(e => e.severity === 'WARN' && (Date.now() - e.timestamp < 86400000)).length} {operationalEvents.filter(e => e.severity === 'WARN' && (Date.now() - e.timestamp < 86400000)).length}
</span> </span>
</span> </span>
</div> </div>
@ -403,7 +404,7 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
<div className="w-1.5 h-1.5 rounded-full bg-blue-500" /> <div className="w-1.5 h-1.5 rounded-full bg-blue-500" />
<span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold"> <span className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">
Info: <span className="text-blue-400 text-xs ml-1"> Info: <span className="text-blue-400 text-xs ml-1">
{botState.operationalEvents.filter(e => e.severity === 'INFO' && (Date.now() - e.timestamp < 86400000)).length} {operationalEvents.filter(e => e.severity === 'INFO' && (Date.now() - e.timestamp < 86400000)).length}
</span> </span>
</span> </span>
</div> </div>
@ -413,14 +414,14 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
</div> </div>
)} )}
<div className="max-h-[400px] overflow-y-auto custom-scrollbar"> <div className="max-h-[400px] overflow-y-auto custom-scrollbar">
{(!botState.operationalEvents || botState.operationalEvents.length === 0) ? ( {(operationalEvents.length === 0) ? (
<div className="py-12 flex flex-col items-center justify-center text-zinc-600"> <div className="py-12 flex flex-col items-center justify-center text-zinc-600">
<ShieldCheck size={24} className="opacity-20 mb-2" /> <ShieldCheck size={24} className="opacity-20 mb-2" />
<p className="text-xs">No actionable issues detected</p> <p className="text-xs">No actionable issues detected</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-white/[0.02]"> <div className="divide-y divide-white/[0.02]">
{botState.operationalEvents.map((event) => ( {operationalEvents.map((event) => (
<div key={event.id} className="px-5 py-3 hover:bg-white/[0.01] transition-colors"> <div key={event.id} className="px-5 py-3 hover:bg-white/[0.01] transition-colors">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">