From e9190f1ea9e0d491bd019cd361ea0d87e0c816d0 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 9 Oct 2026 17:04:32 +0200 Subject: [PATCH] u --- src/devtools/SseMonitor.jsx | 104 ++++++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 4 deletions(-) diff --git a/src/devtools/SseMonitor.jsx b/src/devtools/SseMonitor.jsx index 6a5d379..fb4b189 100644 --- a/src/devtools/SseMonitor.jsx +++ b/src/devtools/SseMonitor.jsx @@ -12,10 +12,11 @@ import CloseIcon from '@mui/icons-material/Close'; import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import SensorsIcon from '@mui/icons-material/Sensors'; -import sseAutomat from '../automats/sseAutomat'; +import sseAutomat, { getSseClientId } from '../automats/sseAutomat'; const PANEL_WIDTH = 420; const PANEL_HEIGHT = 380; +const RAW_EVENT_LIMIT = 500; const pulseAnimationA = keyframes` 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); } 35% { transform: scale(1.16); box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.55); } @@ -42,11 +43,15 @@ class SseMonitor extends PureComponent { this.state = { status: sseAutomat.state.status, companyId: sseAutomat.state.companyId, - events: sseAutomat.state.events, + events: [], open: false, position: null, eventPulse: 0, }; + this.nextRawId = 0; + this.rawAbort = null; + this.rawReconnectTimer = null; + this.rawBuffer = ''; this.panelRef = createRef(); this.handleToggle = this.handleToggle.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); @@ -61,14 +66,15 @@ class SseMonitor extends PureComponent { this.unsubscribe = sseAutomat.subscribe(this, (state) => ({ status: state.status, companyId: state.companyId, - events: state.events, })); window.addEventListener('keydown', this.handleKeyDown); + this.startRawStream(); } componentWillUnmount() { this.unsubscribe?.(); window.removeEventListener('keydown', this.handleKeyDown); + this.stopRawStream(); } componentDidUpdate(prevProps, prevState) { @@ -77,6 +83,96 @@ class SseMonitor extends PureComponent { if (currentEventId != null && currentEventId !== previousEventId) { this.setState((state) => ({ eventPulse: state.eventPulse + 1 })); } + if (prevState.companyId !== this.state.companyId) { + this.startRawStream(); + } + } + + pushRawEvent(type, data) { + const entry = { + id: ++this.nextRawId, + type: type || 'message', + data: data ?? '', + lastEventId: '', + time: Date.now(), + }; + this.setState((state) => ({ + events: [...state.events.slice(-(RAW_EVENT_LIMIT - 1)), entry], + })); + } + + handleRawChunk(text) { + this.rawBuffer += text; + let boundary; + while ((boundary = this.rawBuffer.indexOf('\n\n')) !== -1) { + const frame = this.rawBuffer.slice(0, boundary); + this.rawBuffer = this.rawBuffer.slice(boundary + 2); + let type = ''; + const dataLines = []; + let comment = ''; + for (const line of frame.split('\n')) { + if (line.startsWith(':')) { + comment = line.slice(1).trim(); + } else if (line.startsWith('event:')) { + type = line.slice(6).trim(); + } else if (line.startsWith('data:')) { + dataLines.push(line.slice(5).trimStart()); + } + } + // No filtering: show every frame, including heartbeats and + // events no automat listens for (brand, modelconversation, ...). + if (!type && comment) { + this.pushRawEvent(comment, ''); + } else if (type || dataLines.length > 0) { + this.pushRawEvent(type || 'message', dataLines.join('\n')); + } + } + } + + stopRawStream() { + if (this.rawReconnectTimer) { + clearTimeout(this.rawReconnectTimer); + this.rawReconnectTimer = null; + } + if (this.rawAbort) { + try { + this.rawAbort.abort(); + } catch {} + this.rawAbort = null; + } + } + + async startRawStream() { + this.stopRawStream(); + this.rawBuffer = ''; + const controller = new AbortController(); + this.rawAbort = controller; + try { + const url = new URL('/api/events', window.location.origin); + try { + const cid = getSseClientId(); + if (cid) url.searchParams.set('cid', `${cid}-monitor`); + } catch {} + const res = await fetch(url.href, { + credentials: 'include', + signal: controller.signal, + headers: { Accept: 'text/event-stream' }, + }); + if (!res.ok || !res.body) throw new Error(`SSE monitor HTTP ${res.status}`); + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + this.handleRawChunk(decoder.decode(value, { stream: true })); + } + } catch (err) { + if (controller.signal.aborted) return; + this.pushRawEvent('error', String(err?.message ?? err)); + } + if (!controller.signal.aborted && !this.rawReconnectTimer) { + this.rawReconnectTimer = setTimeout(() => this.startRawStream(), 3000); + } } handleKeyDown(event) { @@ -94,7 +190,7 @@ class SseMonitor extends PureComponent { } handleClear() { - sseAutomat.actions.clearEvents(); + this.setState({ events: [] }); } handlePointerDown(event) {