fix(web): guard malformed operational events
This commit is contained in:
parent
943cfda6b5
commit
257b10fc81
@ -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;
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user