Files
headquarter/apps/web/src/components/toast-rules.ts
T
alex 19242b4152 feat: notification center toast coordination (PR-4)
- EventToastBridge checks notification_toast_level and notification_mute_categories
- toast-rules.ts: event-to-category/severity mapping functions
- Settings page: notification preferences section (toast level dropdown, mute checkboxes)
- Settings API types extended with notification preference fields
- 17 frontend tests (toast-rules + bridge)
- Preference hierarchy: mute categories → toast level → show/hide

Quality gates: vitest 17 passed, tsc clean, eslint clean
2026-05-29 13:52:49 +02:00

84 lines
2.3 KiB
TypeScript

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 mapEventToCategory(event: InstanceEventPayload): string {
if (event.event.startsWith("instance.")) return "instance";
if (event.event.startsWith("health.")) return "health";
return "system";
}
export function mapEventToSeverity(
event: InstanceEventPayload,
): "info" | "warning" | "error" | "success" {
switch (event.event) {
case "instance.error":
case "health.error":
return "error";
case "instance.health_changed":
return event.status === "unhealthy" ? "warning" : "success";
case "instance.created":
case "instance.started":
case "instance.stopped":
case "instance.restarted":
case "instance.deleted":
return "info";
default:
return "info";
}
}
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();
}