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
This commit is contained in:
@@ -1,19 +1,77 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEventContext } from "../state/events";
|
||||
import { handleEventToast } from "./toast-rules";
|
||||
import {
|
||||
handleEventToast,
|
||||
mapEventToCategory,
|
||||
mapEventToSeverity,
|
||||
} from "./toast-rules";
|
||||
import { getUserConfig } from "../api/settings";
|
||||
import type { UserConfig } from "../api/settings";
|
||||
|
||||
interface ToastConfig {
|
||||
notification_toast_level: string;
|
||||
notification_mute_categories: string[];
|
||||
}
|
||||
|
||||
export function EventToastBridge(): JSX.Element | null {
|
||||
const { events } = useEventContext();
|
||||
const processedRef = useRef<Set<string>>(new Set());
|
||||
const [config, setConfig] = useState<ToastConfig | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getUserConfig()
|
||||
.then((c) => {
|
||||
setConfig({
|
||||
notification_toast_level: c.notification_toast_level ?? "all",
|
||||
notification_mute_categories: c.notification_mute_categories ?? [],
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setConfig({
|
||||
notification_toast_level: "all",
|
||||
notification_mute_categories: [],
|
||||
});
|
||||
});
|
||||
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent<Partial<UserConfig>>).detail;
|
||||
if (detail) {
|
||||
setConfig((prev) => ({
|
||||
notification_toast_level:
|
||||
detail.notification_toast_level ??
|
||||
prev?.notification_toast_level ??
|
||||
"all",
|
||||
notification_mute_categories:
|
||||
detail.notification_mute_categories ??
|
||||
prev?.notification_mute_categories ??
|
||||
[],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("userconfig:updated", handler);
|
||||
return () => window.removeEventListener("userconfig:updated", handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) return;
|
||||
|
||||
for (const event of events) {
|
||||
const key = `${event.correlation_id}:${event.timestamp}`;
|
||||
if (processedRef.current.has(key)) continue;
|
||||
processedRef.current.add(key);
|
||||
|
||||
const category = mapEventToCategory(event);
|
||||
const severity = mapEventToSeverity(event);
|
||||
|
||||
if (config.notification_toast_level === "none") continue;
|
||||
if (config.notification_toast_level === "errors" && severity !== "error")
|
||||
continue;
|
||||
if (config.notification_mute_categories.includes(category)) continue;
|
||||
|
||||
handleEventToast(event);
|
||||
}
|
||||
}, [events]);
|
||||
}, [events, config]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user