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>