mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-25 13:43:31 +00:00
fix(web-ui): support multiple browser tabs via SSE broadcaster pattern
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>
This commit is contained in:
@@ -1370,33 +1370,58 @@
|
||||
|
||||
<!-- SSE connection for real-time updates -->
|
||||
<script>
|
||||
// Connect to SSE streams
|
||||
const statsSource = new EventSource('/api/v3/stream/stats');
|
||||
const displaySource = new EventSource('/api/v3/stream/display');
|
||||
// Assign to window so reconnectSSE() in app.js can reach them.
|
||||
window.statsSource = new EventSource('/api/v3/stream/stats');
|
||||
window.displaySource = new EventSource('/api/v3/stream/display');
|
||||
|
||||
statsSource.onmessage = function(event) {
|
||||
window.statsSource.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
updateSystemStats(data);
|
||||
};
|
||||
|
||||
displaySource.onmessage = function(event) {
|
||||
window.displaySource.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
updateDisplayPreview(data);
|
||||
};
|
||||
|
||||
// Connection status
|
||||
statsSource.addEventListener('open', function() {
|
||||
document.getElementById('connection-status').innerHTML = `
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span class="text-gray-600">Connected</span>
|
||||
`;
|
||||
function _setConnectionStatus(connected, reconnecting) {
|
||||
const el = document.getElementById('connection-status');
|
||||
if (!el) return;
|
||||
if (connected) {
|
||||
el.innerHTML = `
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span class="text-gray-600">Connected</span>
|
||||
`;
|
||||
} else if (reconnecting) {
|
||||
el.innerHTML = `
|
||||
<div class="w-2 h-2 bg-yellow-500 rounded-full animate-pulse"></div>
|
||||
<span class="text-gray-600">Reconnecting…</span>
|
||||
`;
|
||||
} else {
|
||||
el.innerHTML = `
|
||||
<div class="w-2 h-2 bg-red-500 rounded-full"></div>
|
||||
<span class="text-gray-600" title="Connection lost — try refreshing the page">Disconnected</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
var _statsErrorCount = 0;
|
||||
window.statsSource.addEventListener('open', function() {
|
||||
_statsErrorCount = 0;
|
||||
_setConnectionStatus(true, false);
|
||||
});
|
||||
|
||||
statsSource.addEventListener('error', function() {
|
||||
document.getElementById('connection-status').innerHTML = `
|
||||
<div class="w-2 h-2 bg-red-500 rounded-full"></div>
|
||||
<span class="text-gray-600">Disconnected</span>
|
||||
`;
|
||||
window.statsSource.addEventListener('error', function() {
|
||||
_statsErrorCount++;
|
||||
// EventSource readyState 0 = CONNECTING (auto-retrying), 2 = CLOSED
|
||||
var reconnecting = window.statsSource.readyState === EventSource.CONNECTING;
|
||||
_setConnectionStatus(false, reconnecting && _statsErrorCount <= 3);
|
||||
});
|
||||
|
||||
window.displaySource.addEventListener('error', function() {
|
||||
// Display stream errors don't change the status badge but log to console
|
||||
// so failures aren't completely silent.
|
||||
console.warn('LEDMatrix: display preview stream error (readyState=' + window.displaySource.readyState + ')');
|
||||
});
|
||||
|
||||
function updateSystemStats(data) {
|
||||
|
||||
Reference in New Issue
Block a user