feat: container monitoring frontend UI (PR-2)
- Custom ToastContext + ToastProvider + ToastContainer (~170 lines, no deps) - useEvents() SSE hook with exponential backoff reconnect - EventProvider context for app-wide SSE stream sharing - Event-to-toast bridge with severity mapping and deduplication - Real-time status badge updates replacing 30s polling - EventSource auth probe (401/429 detection via fetch) - 14 frontend tests (useEvents + toast-rules) Quality gates: vitest 14 passed, tsc clean, eslint clean
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { toast } from "../state/toast";
|
||||
import type { InstanceEventPayload } from "../types/events";
|
||||
|
||||
const DEDUP_WINDOW_MS = 1000;
|
||||
const lastToastTime = new Map<string, number>();
|
||||
|
||||
function makeDedupKey(instanceId: string, eventType: string): string {
|
||||
return `${instanceId}:${eventType}`;
|
||||
}
|
||||
|
||||
function shouldShowToast(instanceId: string, eventType: string): boolean {
|
||||
const key = makeDedupKey(instanceId, eventType);
|
||||
const now = Date.now();
|
||||
const last = lastToastTime.get(key);
|
||||
if (last && now - last < DEDUP_WINDOW_MS) {
|
||||
return false;
|
||||
}
|
||||
lastToastTime.set(key, now);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function handleEventToast(event: InstanceEventPayload): void {
|
||||
const { event: eventType, instance_id, status, message, metadata } = event;
|
||||
|
||||
switch (eventType) {
|
||||
case "instance.started":
|
||||
if (shouldShowToast(instance_id, eventType)) {
|
||||
toast.info("Container starting...", { duration: 3000 });
|
||||
}
|
||||
break;
|
||||
case "instance.health_changed":
|
||||
if (status === "running" && shouldShowToast(instance_id, eventType)) {
|
||||
toast.success("Container running", { duration: 3000 });
|
||||
} else if (
|
||||
status === "unhealthy" &&
|
||||
shouldShowToast(instance_id, eventType)
|
||||
) {
|
||||
toast.warning("Container unhealthy", { duration: 5000 });
|
||||
}
|
||||
break;
|
||||
case "instance.error":
|
||||
if (shouldShowToast(instance_id, eventType)) {
|
||||
const msg = message ?? "Container error";
|
||||
const exitCode = metadata?.exit_code;
|
||||
const fullMsg =
|
||||
exitCode !== undefined ? `${msg} (exit code: ${exitCode})` : msg;
|
||||
toast.error(fullMsg, { duration: 10000 });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearToastDedup(): void {
|
||||
lastToastTime.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user