feat(notifications): täglicher Lauf über SMTP und Apprise
- Kanäle SMTP (HTML-Mail mit Logos als CID-Anhang) und Apprise, beide blockierenden Bibliotheken laufen in einem Thread - Vier Anlässe: Fälligkeiten im Vorlauf, Kündigungsfristen in drei Stufen, überschrittene Budgets je Monat, Vertragsverlängerungen im Folgemonat - APScheduler im Anwendungsprozess, täglich 07:00 Europe/Berlin, räumt zugleich abgelaufene Sitzungen auf - Duplikatsschutz über den Zieltag statt den Versandtag; fehlgeschlagener Versand wird beim nächsten Lauf erneut versucht - Endpunkte für Regeln, Protokoll, Testversand und sofortigen Lauf - Einstellungsseite mit Einrichtungsstand, Regelpflege und Protokoll - 25 neue Backend-Tests (260 gesamt), 9 neue Frontend-Tests (74 gesamt) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/** Benachrichtigungsregeln, Protokoll und Testversand. */
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
import { toast } from "@/store/toast";
|
||||
import type {
|
||||
MessageResponse,
|
||||
NotificationLogEntry,
|
||||
NotificationRule,
|
||||
NotificationRuleInput,
|
||||
NotificationRunResult,
|
||||
NotificationSettings,
|
||||
TestSendResponse,
|
||||
} from "@/types/api";
|
||||
|
||||
const KANAL_NAMEN = { smtp: "E-Mail", apprise: "Apprise" } as const;
|
||||
|
||||
export function useNotificationSettings() {
|
||||
return useQuery({
|
||||
queryKey: ["notifications", "settings"],
|
||||
queryFn: () => api.get<NotificationSettings>("/notifications/settings"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useNotificationRules() {
|
||||
return useQuery({
|
||||
queryKey: ["notifications", "rules"],
|
||||
queryFn: () => api.get<NotificationRule[]>("/notifications/rules"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useNotificationLog(limit = 50) {
|
||||
return useQuery({
|
||||
queryKey: ["notifications", "log", limit],
|
||||
queryFn: () => api.get<NotificationLogEntry[]>("/notifications/log", { limit }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveNotificationRule() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
daten,
|
||||
}: {
|
||||
id?: number;
|
||||
daten: NotificationRuleInput | Partial<NotificationRuleInput>;
|
||||
}) =>
|
||||
id
|
||||
? api.patch<NotificationRule>(`/notifications/rules/${id}`, daten)
|
||||
: api.post<NotificationRule>("/notifications/rules", daten),
|
||||
onSuccess: (_regel, variablen) => {
|
||||
void client.invalidateQueries({ queryKey: ["notifications"] });
|
||||
toast.success(variablen.id ? "Regel gespeichert." : "Regel angelegt.");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteNotificationRule() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => api.del<MessageResponse>(`/notifications/rules/${id}`),
|
||||
onSuccess: () => {
|
||||
void client.invalidateQueries({ queryKey: ["notifications"] });
|
||||
toast.success("Regel gelöscht.");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSendTestNotification() {
|
||||
return useMutation({
|
||||
mutationFn: (target?: string) =>
|
||||
api.post<TestSendResponse>("/notifications/test", { target: target || null }),
|
||||
onSuccess: (ergebnis) => {
|
||||
const zugestellt = ergebnis.results
|
||||
.filter((eintrag) => eintrag.sent)
|
||||
.map((eintrag) => KANAL_NAMEN[eintrag.channel]);
|
||||
const gescheitert = ergebnis.results.filter(
|
||||
(eintrag) => eintrag.configured && !eintrag.sent,
|
||||
);
|
||||
|
||||
if (zugestellt.length > 0) {
|
||||
toast.success(
|
||||
`Testnachricht versendet über ${zugestellt.join(" und ")}.`,
|
||||
gescheitert.length > 0
|
||||
? `Fehlgeschlagen: ${gescheitert
|
||||
.map((eintrag) => `${KANAL_NAMEN[eintrag.channel]} (${eintrag.error})`)
|
||||
.join(", ")}`
|
||||
: undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gescheitert.length > 0) {
|
||||
toast.error(
|
||||
"Der Testversand ist fehlgeschlagen.",
|
||||
gescheitert.map((eintrag) => eintrag.error).join(" · "),
|
||||
);
|
||||
return;
|
||||
}
|
||||
toast.info("Kein Kanal eingerichtet.", "Trage SMTP- oder Apprise-Daten in die Umgebung ein.");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRunNotifications() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => api.post<NotificationRunResult>("/notifications/run"),
|
||||
onSuccess: (ergebnis) => {
|
||||
void client.invalidateQueries({ queryKey: ["notifications"] });
|
||||
toast.success(
|
||||
ergebnis.sent > 0
|
||||
? `${ergebnis.sent} Benachrichtigung${ergebnis.sent === 1 ? "" : "en"} versendet.`
|
||||
: "Nichts zu melden.",
|
||||
`${ergebnis.checked} geprüft, ${ergebnis.skipped} bereits gemeldet.`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user