mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-02-19 15:46:36 -05:00
254 lines
6.8 KiB
JavaScript
254 lines
6.8 KiB
JavaScript
/**
|
|
* NetAlertX SSE (Server-Sent Events) Manager
|
|
* Replaces polling with real-time updates from backend
|
|
* Falls back to polling if SSE unavailable
|
|
*/
|
|
|
|
class NetAlertXStateManager {
|
|
constructor() {
|
|
this.eventSource = null;
|
|
this.clientId = `client-${Math.random().toString(36).substr(2, 9)}`;
|
|
this.pollInterval = null;
|
|
this.pollBackoffInterval = 1000; // Start at 1s
|
|
this.maxPollInterval = 30000; // Max 30s
|
|
this.useSSE = true;
|
|
this.sseConnectAttempts = 0;
|
|
this.maxSSEAttempts = 3;
|
|
this.initialized = false;
|
|
}
|
|
|
|
/**
|
|
* Initialize the state manager
|
|
* Tries SSE first, falls back to polling if unavailable
|
|
*/
|
|
init() {
|
|
if (this.initialized) return;
|
|
// waiting until cache ready
|
|
const waitForInit = () => {
|
|
if (!isAppInitialized()) {
|
|
setTimeout(waitForInit, 300);
|
|
return;
|
|
}
|
|
|
|
console.log("[NetAlertX State] App initialized, starting state manager");
|
|
this.trySSE();
|
|
this.initialized = true;
|
|
};
|
|
|
|
waitForInit();
|
|
}
|
|
|
|
/**
|
|
* Attempt SSE connection with fetch streaming
|
|
* Uses Authorization header like all other endpoints
|
|
*/
|
|
async trySSE() {
|
|
if (this.sseConnectAttempts >= this.maxSSEAttempts) {
|
|
console.warn("[NetAlertX State] SSE failed after max attempts, switching to polling");
|
|
this.useSSE = false;
|
|
this.startPolling();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const apiToken = getSetting("API_TOKEN");
|
|
const apiBase = getApiBase().replace(/\/$/, '');
|
|
const sseUrl = `${apiBase}/sse/state?client=${encodeURIComponent(this.clientId)}`;
|
|
|
|
const response = await fetch(sseUrl, {
|
|
headers: { 'Authorization': `Bearer ${apiToken}` }
|
|
});
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
console.log("[NetAlertX State] Connected to SSE");
|
|
this.sseConnectAttempts = 0;
|
|
|
|
// Stream and parse SSE events
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
this.handleSSEError();
|
|
break;
|
|
}
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const events = buffer.split('\n\n');
|
|
buffer = events[events.length - 1];
|
|
|
|
events.slice(0, -1).forEach(e => this.processSSEEvent(e));
|
|
}
|
|
} catch (e) {
|
|
console.error("[NetAlertX State] SSE error:", e);
|
|
this.handleSSEError();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse and dispatch a single SSE event
|
|
*/
|
|
processSSEEvent(eventText) {
|
|
if (!eventText || !eventText.trim()) return;
|
|
|
|
const lines = eventText.split('\n');
|
|
let eventType = null, eventData = null;
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('event:')) eventType = line.substring(6).trim();
|
|
else if (line.startsWith('data:')) eventData = line.substring(5).trim();
|
|
}
|
|
|
|
if (!eventType || !eventData) return;
|
|
|
|
try {
|
|
switch (eventType) {
|
|
case 'state_update':
|
|
this.handleStateUpdate(JSON.parse(eventData));
|
|
break;
|
|
case 'unread_notifications_count_update':
|
|
this.handleUnreadNotificationsCountUpdate(JSON.parse(eventData));
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
console.error(`[NetAlertX State] Parse error for ${eventType}:`, e, "eventData:", eventData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle SSE connection error with exponential backoff
|
|
*/
|
|
handleSSEError() {
|
|
this.sseConnectAttempts++;
|
|
if (this.eventSource) {
|
|
this.eventSource.close();
|
|
this.eventSource = null;
|
|
}
|
|
|
|
if (this.sseConnectAttempts < this.maxSSEAttempts) {
|
|
console.log(`[NetAlertX State] Retry ${this.sseConnectAttempts}/${this.maxSSEAttempts}...`);
|
|
setTimeout(() => this.trySSE(), 5000);
|
|
} else {
|
|
this.trySSE();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle state update from SSE or Polling
|
|
*/
|
|
handleStateUpdate(appState) {
|
|
try {
|
|
// 1. Update the main status text
|
|
if (appState["currentState"]) {
|
|
const cleanState = appState["currentState"].replace(/"/g, "");
|
|
$("#state").html(cleanState);
|
|
}
|
|
|
|
// 2. Update Version placeholders
|
|
const version = appState["appVersion"] || "UNKNOWN";
|
|
$('[data-plc="version"]')
|
|
.html(version)
|
|
.attr('data-version', version);
|
|
|
|
// 3. Update Build Timestamp placeholders
|
|
const buildTime = appState["buildTimestamp"] || 0;
|
|
const displayTime = buildTime ? localizeTimestamp(buildTime) : "UNKNOWN";
|
|
|
|
$('[data-plc="build-timestamp"]')
|
|
.html(displayTime)
|
|
.attr('data-build-time', buildTime);
|
|
|
|
$('[data-plc="build-timestamp"]')
|
|
.html(displayTime)
|
|
.attr('data-build-time', buildTime);
|
|
|
|
// console.log("[NetAlertX State] UI updated via jQuery");
|
|
} catch (e) {
|
|
console.error("[NetAlertX State] Failed to update state display:", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle unread notifications count update
|
|
*/
|
|
handleUnreadNotificationsCountUpdate(data) {
|
|
try {
|
|
const count = data.count || 0;
|
|
console.log("[NetAlertX State] Unread notifications count:", count);
|
|
handleUnreadNotifications(count);
|
|
} catch (e) {
|
|
console.error("[NetAlertX State] Failed to handle unread count update:", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start polling fallback (if SSE fails)
|
|
*/
|
|
startPolling() {
|
|
console.log("[NetAlertX State] Starting polling fallback...");
|
|
this.poll();
|
|
}
|
|
|
|
/**
|
|
* Poll the server for state updates
|
|
*/
|
|
poll() {
|
|
$.get(
|
|
"php/server/query_json.php",
|
|
{ file: "app_state.json", nocache: Date.now() },
|
|
(appState) => {
|
|
this.handleStateUpdate(appState);
|
|
this.pollBackoffInterval = 1000; // Reset on success
|
|
this.pollInterval = setTimeout(() => this.poll(), this.pollBackoffInterval);
|
|
}
|
|
).fail(() => {
|
|
// Exponential backoff on failure
|
|
this.pollBackoffInterval = Math.min(
|
|
this.pollBackoffInterval * 1.5,
|
|
this.maxPollInterval
|
|
);
|
|
this.pollInterval = setTimeout(() => this.poll(), this.pollBackoffInterval);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stop all updates
|
|
*/
|
|
stop() {
|
|
if (this.eventSource) {
|
|
this.eventSource.close();
|
|
this.eventSource = null;
|
|
}
|
|
if (this.pollInterval) {
|
|
clearTimeout(this.pollInterval);
|
|
this.pollInterval = null;
|
|
}
|
|
this.initialized = false;
|
|
}
|
|
|
|
/**
|
|
* Get stats for debugging
|
|
*/
|
|
async getStats() {
|
|
try {
|
|
const apiToken = getSetting("API_TOKEN");
|
|
const apiBase = getApiBase();
|
|
const response = await fetch(`${apiBase}/sse/stats`, {
|
|
headers: {
|
|
Authorization: `Bearer ${apiToken}`,
|
|
},
|
|
});
|
|
return await response.json();
|
|
} catch (e) {
|
|
console.error("[NetAlertX State] Failed to get stats:", e);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Global instance
|
|
let netAlertXStateManager = new NetAlertXStateManager();
|