push terminal session changes and alert evaluator health

This commit is contained in:
lda committed 2026-10-01 11:32:26 +07:00 Verified
1 parent cf7622e818
commit f7c982cbd7
11 files changed
+663 -101

No files matched your search

+30 -4
View File
@@ -16,10 +16,18 @@ import { Textarea } from "@/components/ui/textarea";
type Props = {
alerts: Alert[];
transitions: AlertTransition[];
alertsHealth: { available: boolean; reason?: string } | null;
alertsStreamConnected: boolean;
onRefresh: () => Promise<void>;
};
export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
export function AlertsPage({
alerts,
transitions,
alertsHealth,
alertsStreamConnected,
onRefresh,
}: Props) {
const [severity, setSeverity] = useState("all");
const [status, setStatus] = useState("all");
const [kind, setKind] = useState("all");
@@ -66,9 +74,27 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
<Card>
<CardHeader className="flex items-center justify-between gap-2">
<CardTitle>Active Alerts</CardTitle>
<Button size="sm" variant="outline" onClick={() => void onRefresh()}>
Refresh
</Button>
<div className="flex flex-wrap items-center justify-end gap-x-3 gap-y-1">
<div
aria-live="polite"
className="text-right text-xs text-muted-foreground"
>
{!alertsStreamConnected && <div>Alert stream disconnected</div>}
{alertsHealth?.available === false && (
<div>
Alert results may be stale or unavailable
{alertsHealth.reason ? `: ${alertsHealth.reason}` : ""}
</div>
)}
</div>
<Button
size="sm"
variant="outline"
onClick={() => void onRefresh()}
>
Refresh
</Button>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid gap-2 sm:grid-cols-3">
+18
View File
@@ -27,6 +27,8 @@ type Props = {
transitions: AlertTransition[];
audit: AuditEvent[];
loading: boolean;
alertsHealth: { available: boolean; reason?: string } | null;
alertsStreamConnected: boolean;
onRefresh: () => void;
};
@@ -66,6 +68,8 @@ export function DashboardPage({
transitions,
audit,
loading,
alertsHealth,
alertsStreamConnected,
onRefresh,
}: Props) {
const connected = agents.filter((a) => a.connected).length;
@@ -165,6 +169,20 @@ export function DashboardPage({
</Link>
</CardHeader>
<CardContent>
{(!alertsStreamConnected || alertsHealth?.available === false) && (
<div
aria-live="polite"
className="mb-2 text-xs text-muted-foreground"
>
{!alertsStreamConnected && <div>Alert stream disconnected</div>}
{alertsHealth?.available === false && (
<div>
Alert results may be stale or unavailable
{alertsHealth.reason ? `: ${alertsHealth.reason}` : ""}
</div>
)}
</div>
)}
<div className="text-2xl font-bold tabular-nums">{alerts.length}</div>
{Object.keys(severityBuckets).length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1.5">
+88 -25
View File
@@ -513,9 +513,38 @@ export function TerminalPage({
terminalRef.current = terminal;
fitRef.current = fit;
let cancelled = false;
let refreshInFlight: Promise<TerminalSession[]> | null = null;
let refreshRequested = false;
const refreshSessions = (): Promise<TerminalSession[]> => {
if (refreshInFlight) {
refreshRequested = true;
return refreshInFlight;
}
refreshInFlight = (async () => {
let listed: TerminalSession[] = [];
do {
refreshRequested = false;
listed = await listTerminals();
if (!cancelled) {
setSessions((current) =>
reconcileTerminalSessions(current, listed),
);
}
} while (refreshRequested && !cancelled);
return listed;
})().finally(() => {
refreshInFlight = null;
if (refreshRequested && !cancelled) {
void refreshSessions().catch(() => undefined);
}
});
return refreshInFlight;
};
async function restoreTerminalSession() {
try {
const listed = await listTerminals();
const listed = await refreshSessions();
if (cancelled) return;
setSessions(orderTerminalSessions(listed));
const rememberedId = window.sessionStorage.getItem(
@@ -557,7 +586,6 @@ export function TerminalPage({
}
}
let cancelled = false;
const input = terminal.onData((data) => writeTerminalData(data, true));
let resizeFrame = 0;
let lastHostWidth = 0;
@@ -575,30 +603,61 @@ export function TerminalPage({
});
});
resizeObserver.observe(hostRef.current);
void restoreTerminalSession();
let refreshInFlight = false;
const refreshSessions = () => {
if (refreshInFlight) return;
refreshInFlight = true;
void listTerminals()
.then((listed) => {
if (!cancelled) {
setSessions((current) =>
reconcileTerminalSessions(current, listed),
);
let eventsSocket: WebSocket | null = null;
let reconnectTimer = 0;
let healthyTimer = 0;
let reconnectAttempt = 0;
const connectEvents = () => {
if (cancelled) return;
const socket = new WebSocket(
websocketUrl("/api/v1/control/terminals/events/ws"),
);
eventsSocket = socket;
socket.onmessage = (event) => {
if (cancelled || eventsSocket !== socket) return;
if (typeof event.data !== "string") return;
try {
const message = JSON.parse(event.data) as { type?: string };
if (message.type === "sessions_changed") {
void refreshSessions().catch(() => undefined);
}
})
.catch(() => {
// The attached terminal transport remains authoritative while a
// background list refresh is temporarily unavailable.
})
.finally(() => {
refreshInFlight = false;
});
} catch {
// Ignore malformed event frames; the fallback refresh remains active.
}
};
socket.onopen = () => {
window.clearTimeout(healthyTimer);
healthyTimer = window.setTimeout(() => {
if (eventsSocket === socket && socket.readyState === WebSocket.OPEN) {
reconnectAttempt = 0;
}
}, 10_000);
};
socket.onclose = () => {
if (cancelled || eventsSocket !== socket) return;
window.clearTimeout(healthyTimer);
const ceiling = Math.min(500 * 2 ** reconnectAttempt, 12_000);
const delay = Math.min(
Math.round(ceiling * (0.8 + Math.random() * 0.4)),
15_000,
);
reconnectAttempt = Math.min(reconnectAttempt + 1, 5);
reconnectTimer = window.setTimeout(connectEvents, delay);
};
socket.onerror = () => socket.close();
};
refreshSessionsRef.current = refreshSessions;
const refreshTimer = window.setInterval(refreshSessions, 15_000);
window.addEventListener("focus", refreshSessions);
connectEvents();
void restoreTerminalSession();
refreshSessionsRef.current = () => {
void refreshSessions().catch(() => undefined);
};
const refreshTimer = window.setInterval(() => {
void refreshSessions().catch(() => undefined);
}, 15_000);
const refreshOnFocus = () => {
void refreshSessions().catch(() => undefined);
};
window.addEventListener("focus", refreshOnFocus);
return () => {
cancelled = true;
@@ -606,7 +665,11 @@ export function TerminalPage({
titleChange.dispose();
resizeObserver.disconnect();
window.clearInterval(refreshTimer);
window.removeEventListener("focus", refreshSessions);
window.removeEventListener("focus", refreshOnFocus);
window.clearTimeout(reconnectTimer);
window.clearTimeout(healthyTimer);
eventsSocket?.close();
eventsSocket = null;
refreshSessionsRef.current = () => {};
window.cancelAnimationFrame(resizeFrame);
detachTransport();