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;
// Critical system events (for the alert banner)
const recentCriticalEvents = (botState.operationalEvents ?? []).filter(e =>
(e.severity === 'ERROR' || e.severity === 'WARN') &&
Date.now() - e.timestamp < 600_000
const recentCriticalEvents = (botState.operationalEvents ?? []).filter((e): e is NonNullable<typeof e> =>
Boolean(e)
&& (e.severity === 'ERROR' || e.severity === 'WARN')
&& Date.now() - e.timestamp < 600_000
);
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 = {
symbols: {},
positions: [],
@ -381,6 +391,10 @@ export const useWebSocket = (url: string) => {
});
newSocket.on('operational_event', (event: any) => {
if (!isOperationalEventRecord(event)) {
console.warn('Ignoring malformed operational_event payload', event);
return;
}
setBotState(prev => ({
...prev,
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 tradingControl = botState.health?.tradingControl;
const isPaused = tradingControl?.mode === 'PAUSED';
const operationalEvents = (botState.operationalEvents ?? []).filter((event): event is NonNullable<typeof event> => Boolean(event));
const formatDuration = (ms?: number) => {
if (!ms || !Number.isFinite(ms)) return 'Idle';
@ -376,18 +377,18 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
</button>
</div>
<span className="text-[10px] text-zinc-500 font-mono">
Buffer: {botState.operationalEvents?.length || 0} events
Buffer: {operationalEvents.length || 0} events
</span>
</div>
{/* 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="flex items-center gap-2">
<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">
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>
</div>
@ -395,7 +396,7 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
<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">
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>
</div>
@ -403,7 +404,7 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
<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">
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>
</div>
@ -413,14 +414,14 @@ export const AdminTab = ({ botState, socket }: AdminTabProps) => {
</div>
)}
<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">
<ShieldCheck size={24} className="opacity-20 mb-2" />
<p className="text-xs">No actionable issues detected</p>
</div>
) : (
<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 className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3">