Initial commit with translated description

This commit is contained in:
2026-03-29 10:19:19 +08:00
commit 5aa1f324c6
81 changed files with 27526 additions and 0 deletions

119
public/js/api.js Normal file
View File

@@ -0,0 +1,119 @@
/**
* API and SSE connection management for Command Center
*/
// SSE connection state
let eventSource = null;
let sseConnected = false;
let sseReconnectAttempts = 0;
let pollInterval = null;
const SSE_MAX_RECONNECT_DELAY = 30000;
/**
* Fetch the unified state from the server
* @returns {Promise<Object>} Dashboard state
*/
export async function fetchState() {
const response = await fetch("/api/state");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
/**
* Connect to SSE for real-time updates
* @param {Function} onUpdate - Callback when state updates
* @param {Function} onStatusChange - Callback for connection status changes
*/
export function connectSSE(onUpdate, onStatusChange) {
if (typeof EventSource === "undefined") {
console.warn("[SSE] EventSource not supported, using polling fallback");
onStatusChange?.("polling", "Polling Mode");
startPolling(onUpdate);
return;
}
onStatusChange?.("connecting", "Connecting...");
try {
eventSource = new EventSource("/api/events");
eventSource.onopen = function () {
console.log("[SSE] Connected");
sseConnected = true;
sseReconnectAttempts = 0;
onStatusChange?.("connected", "🟢 Live");
stopPolling();
};
eventSource.addEventListener("connected", function (e) {
try {
const data = JSON.parse(e.data);
console.log("[SSE] Server greeting:", data.message);
} catch (err) {}
});
eventSource.addEventListener("update", function (e) {
try {
const data = JSON.parse(e.data);
onUpdate?.(data);
} catch (err) {
console.error("[SSE] Failed to parse update:", err);
}
});
eventSource.addEventListener("heartbeat", function (e) {
try {
const data = JSON.parse(e.data);
console.log("[SSE] Heartbeat, clients:", data.clients);
} catch (err) {}
});
eventSource.onerror = function (e) {
console.error("[SSE] Connection error");
sseConnected = false;
eventSource.close();
eventSource = null;
onStatusChange?.("disconnected", "🔴 Disconnected");
// Exponential backoff for reconnection
sseReconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, sseReconnectAttempts - 1), SSE_MAX_RECONNECT_DELAY);
console.log(`[SSE] Reconnecting in ${delay}ms (attempt ${sseReconnectAttempts})`);
// Start polling as fallback while disconnected
startPolling(onUpdate);
setTimeout(() => connectSSE(onUpdate, onStatusChange), delay);
};
} catch (err) {
console.error("[SSE] Failed to create EventSource:", err);
onStatusChange?.("disconnected", "🔴 Error");
startPolling(onUpdate);
}
}
function startPolling(onUpdate) {
if (pollInterval) return;
console.log("[Polling] Starting fallback polling");
pollInterval = setInterval(async () => {
try {
const state = await fetchState();
onUpdate?.(state);
} catch (err) {
console.error("[Polling] Failed:", err);
}
}, 2000);
}
function stopPolling() {
if (pollInterval) {
console.log("[Polling] Stopping fallback polling (SSE connected)");
clearInterval(pollInterval);
pollInterval = null;
}
}
export function isConnected() {
return sseConnected;
}