mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 11:16:55 +00:00
fix(alerts): overhaul alerts & notifications system for local and remote nodes
- StackAlertSheet: fetch agent status on open and show contextual banner (amber warning when no channels configured, green when active, blue info callout on remote nodes explaining remote-instance evaluation semantics) - StackAlertSheet: surface actual server error message on addAlert failure instead of generic string; add console logging for all failure paths - SettingsModal: expose Notifications tab for remote nodes so agents can be configured directly on any Sencho instance; section header shows node name and Remote badge with tooltip when proxying to a remote node - SettingsModal: re-fetch agents/settings when active node changes - SettingsModal: add hidden DialogTitle/DialogDescription to satisfy Radix UI accessibility requirements (eliminates console errors) - backend POST /api/alerts: add Zod validation schema; rejects unknown metric/operator values, negative thresholds, and missing fields with 400 - EditorLayout WS: upgrade reconnect from flat 5s retry to exponential backoff (1s→2s→4s→8s→16s→30s max); onerror now logs the event; cleanup only closes OPEN sockets — CONNECTING sockets are closed in onopen after isMounted check, eliminating "closed before established" StrictMode error
This commit is contained in:
@@ -194,10 +194,22 @@ export default function EditorLayout() {
|
||||
let ws: WebSocket | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let isMounted = true;
|
||||
let retryCount = 0;
|
||||
const MAX_RETRY_DELAY_MS = 30000;
|
||||
|
||||
const connect = () => {
|
||||
if (!isMounted) return;
|
||||
ws = new WebSocket(`${wsBase}/ws/notifications`);
|
||||
|
||||
ws.onopen = () => {
|
||||
if (!isMounted) {
|
||||
// Component unmounted while the handshake was in-flight (React StrictMode double-mount)
|
||||
ws?.close();
|
||||
return;
|
||||
}
|
||||
retryCount = 0; // Reset backoff on successful connect
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
@@ -209,14 +221,18 @@ export default function EditorLayout() {
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (isMounted) {
|
||||
reconnectTimer = setTimeout(connect, 5000);
|
||||
}
|
||||
ws.onclose = (event) => {
|
||||
if (!isMounted) return;
|
||||
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s max
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCount), MAX_RETRY_DELAY_MS);
|
||||
retryCount++;
|
||||
console.debug(`[WS notifications] closed (code=${event.code}), reconnecting in ${delay}ms (attempt ${retryCount})`);
|
||||
reconnectTimer = setTimeout(connect, delay);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws?.close();
|
||||
ws.onerror = (event) => {
|
||||
// onerror always fires before onclose - log it and let onclose handle reconnect
|
||||
console.warn('[WS notifications] error event', event);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -225,7 +241,12 @@ export default function EditorLayout() {
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
ws?.close();
|
||||
// Only close an already-open connection. If still CONNECTING, let onopen
|
||||
// detect isMounted=false and close then — avoids the browser warning
|
||||
// "WebSocket is closed before the connection is established".
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
|
||||
Reference in New Issue
Block a user