- _broadcast: on queue.Full drop the oldest item and retry the put
instead of removing the client from _clients — a slow tab now stays
subscribed and receives the latest data rather than being silently
ejected
- _broadcast: break instead of continue when _clients is empty so the
background generator thread exits rather than spinning indefinitely;
subscribe() already restarts it on the next connection
- base.html: expose _statsOpenHandler, _statsErrorHandler, and
_displayErrorHandler as window properties so reconnectSSE() can
reattach them after replacing the EventSource instances
- app.js: reconnectSSE() now reattaches those handlers after creating
each new EventSource so the status badge and display-stream console
logging survive a manual reconnect
Heartbeat path (~line 646) is a queue read (q.get), not a write; no
queue.Full can occur there so no change needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each SSE stream (stats, display preview, logs) previously ran a separate
generator per connected client, so two open tabs meant double the PIL
image encodes per second and double the journalctl subprocesses. Under
load or on reconnect storms the tight "20 per minute" rate limit was
easily exhausted, silently breaking tabs without any user-facing
explanation.
- Replace per-client sse_response generators with _StreamBroadcaster:
one background thread per stream type fans data to all subscribed
client queues, keeping CPU/subprocess work constant regardless of
how many tabs are open
- Add 30-second SSE heartbeat comments to keep idle connections alive
through proxies
- Raise SSE rate limit from "20/min" to "200/min" to prevent reconnect
storms from exhausting the limit
- Assign statsSource/displaySource to window.* so reconnectSSE() in
app.js can actually reach them (was dead code due to const scoping)
- Add displaySource error handler so display preview failures are no
longer completely silent
- Improve connection status badge: shows "Reconnecting…" on first few
errors, "Disconnected" with tooltip hint after persistent failure
- Complete the empty displaySource.onmessage stub in reconnectSSE()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>