From 261579f0f2e855d04412c503bd3a52f3bcd25048 Mon Sep 17 00:00:00 2001 From: Sandro Date: Fri, 30 Oct 2026 19:02:46 +0100 Subject: [PATCH] feat(notify): own webhook as a full notification channel The custom webhook could be entered but never switched on since the notification matrix replaced the old alarm channels. It is now a third column next to e-mail and Discord for every event and the daily summary; bundles go out as JSON (event "digest"). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 + Doku/architecture.md | 2 +- .../features/settings/UserSettingsView.tsx | 79 +++++++++++----- .../UserSettingsView.alarms.test.tsx | 42 ++++++++- frontend/src/lib/types.ts | 2 + internal/api/alarms.go | 6 +- internal/api/notify.go | 94 ++++++++++++++----- internal/api/notify_test.go | 22 ++++- internal/api/server.go | 1 + internal/domain/models.go | 6 ++ 10 files changed, 202 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b8dbf4..796fee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,16 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +### Added +- Benachrichtigungen: Der eigene Webhook ist jetzt ein vollwertiger Kanal neben E-Mail und Discord – eigene Spalte „Webhook“ für jedes Ereignis und die Tageszusammenfassung; gebündelte Nachrichten kommen als JSON (`event: "digest"` mit Liste der Ereignisse). + ### Changed - Schrift: MindSift liefert die Schrift Inter jetzt selbst aus (ohne externen Dienst), damit die Oberfläche auf allen Geräten gleich aussieht. Notiztitel stehen in einer feinen, dünnen Schrift und etwas größer. - Notizen: Der Titel in der Kopfleiste erscheint erst, wenn der große Titel aus dem Bild gescrollt ist; am Handy passt die Kopfleiste in eine Zeile. - Das Bild für `main` (`latest`) zeigt die Versionsnummer ohne `-dev`; nur `dev` und andere Zweige behalten den Zusatz. ### Fixed +- Kontoeinstellungen: Der eigene Webhook ließ sich zwar eintragen, aber nicht mehr einschalten (seit dem Umbau der Benachrichtigungen in 0.8.0). - Workspace: kein doppelter Scrollbalken mehr – die Seite selbst scrollt nicht, nur die Notiz; am Handy scrollt die Notiz nicht mehr getrennt von der Seite. - Workspace: Ein abgebrochenes Ziehen in der Seitenleiste lässt „Auf Hauptebene verschieben“ nicht mehr stehen. - Workspace: Der Knopf zum Ein-/Ausklappen der Ordnerleiste ist im eingeklappten Zustand dezent (nur das Symbol in Akzentfarbe). diff --git a/Doku/architecture.md b/Doku/architecture.md index fb266ce..46b60f2 100644 --- a/Doku/architecture.md +++ b/Doku/architecture.md @@ -106,7 +106,7 @@ Bilder und Audio liegen in `$DATA_DIR/media//.`, beschrieben in ## Benachrichtigungen -`internal/api/notify.go`, `internal/storage/notifications.go` (Migration 7). Jede Person wählt pro Ereignisart die Kanäle – eigene E-Mail (Alarm-Adresse, sonst Konto-Adresse) und eigener Discord-Webhook aus den Alarmen –, gespeichert als JSON in `users.notification_settings` (`GET /auth/me`, `PATCH /auth/settings` → `notificationSettings`). Ereignisse: `due` (Fälligkeits-Alarm, sofort über den Alarm-Ticker), `shared` (Notiz, Ordner, Aufgabenliste oder Liste geteilt; bei Gruppen alle Mitglieder), `taskDone` (Aufgabe einer geteilten Liste erledigt), `watched` (jede Änderung an einer beobachteten Aufgabe, `task_watchers`, `POST/DELETE /tasks/{id}/watch`). Alles außer `due` landet in `notification_queue`; ein Job im Minutentakt schickt je Person eine gebündelte Nachricht, sobald das älteste Ereignis `NOTIFY_DIGEST_MINUTES` (Standard 5) alt ist (E-Mail als Text + HTML, Discord als ein Embed mit höchstens 10 Zeilen), nach 3 Fehlversuchen wird verworfen. Eigene Änderungen benachrichtigen nie den Handelnden, außer bei beobachteten Aufgaben mit `includeOwnWatched`. `paused` stoppt alles, auch Alarme. **Tageszusammenfassung:** zur gewählten Uhrzeit (Server-Zeitzone, innerhalb von 30 Minuten, einmal pro Tag) überfällige, heutige und morgige offene Aufgaben samt Anzahl der folgenden 7 Tage; nichts fällig → keine Nachricht. Im Demo-Modus laufen die Jobs nicht. +`internal/api/notify.go`, `internal/storage/notifications.go` (Migration 7). Jede Person wählt pro Ereignisart die Kanäle – eigene E-Mail (Alarm-Adresse, sonst Konto-Adresse) eigener Discord-Webhook und eigener Webhook (`customWebhookUrl`, JSON per POST) aus den Alarmen –, gespeichert als JSON in `users.notification_settings` (`GET /auth/me`, `PATCH /auth/settings` → `notificationSettings`). Ereignisse: `due` (Fälligkeits-Alarm, sofort über den Alarm-Ticker), `shared` (Notiz, Ordner, Aufgabenliste oder Liste geteilt; bei Gruppen alle Mitglieder), `taskDone` (Aufgabe einer geteilten Liste erledigt), `watched` (jede Änderung an einer beobachteten Aufgabe, `task_watchers`, `POST/DELETE /tasks/{id}/watch`). Alles außer `due` landet in `notification_queue`; ein Job im Minutentakt schickt je Person eine gebündelte Nachricht, sobald das älteste Ereignis `NOTIFY_DIGEST_MINUTES` (Standard 5) alt ist (E-Mail als Text + HTML, Discord als ein Embed mit höchstens 10 Zeilen, eigener Webhook als JSON `{"event":"digest","heading","items":[{"kind","title","text"}],"link","timestamp"}`; fällige Aufgaben gehen dort als `{"event":"task_due",…}` einzeln raus), nach 3 Fehlversuchen wird verworfen. Eigene Änderungen benachrichtigen nie den Handelnden, außer bei beobachteten Aufgaben mit `includeOwnWatched`. `paused` stoppt alles, auch Alarme. **Tageszusammenfassung:** zur gewählten Uhrzeit (Server-Zeitzone, innerhalb von 30 Minuten, einmal pro Tag) überfällige, heutige und morgige offene Aufgaben samt Anzahl der folgenden 7 Tage; nichts fällig → keine Nachricht. Im Demo-Modus laufen die Jobs nicht. ## Link-Vorschau diff --git a/frontend/src/features/settings/UserSettingsView.tsx b/frontend/src/features/settings/UserSettingsView.tsx index 19665d5..3a8de5c 100644 --- a/frontend/src/features/settings/UserSettingsView.tsx +++ b/frontend/src/features/settings/UserSettingsView.tsx @@ -51,6 +51,7 @@ export function buildNotificationSettings(user: User): NotificationSettings { const hasLegacy = Array.isArray(user.alarmChannels); const dueEmailDefault = hasLegacy ? user.alarmChannels!.includes('email') : true; const dueDiscordDefault = hasLegacy ? user.alarmChannels!.includes('discord') : true; + const dueWebhookDefault = hasLegacy ? user.alarmChannels!.includes('webhook') : false; const s = user.notificationSettings; return { paused: Boolean(s?.paused), @@ -59,18 +60,22 @@ export function buildNotificationSettings(user: User): NotificationSettings { due: { email: s?.events?.due?.email ?? dueEmailDefault, discord: s?.events?.due?.discord ?? dueDiscordDefault, + webhook: s?.events?.due?.webhook ?? dueWebhookDefault, }, shared: { email: s?.events?.shared?.email ?? true, discord: s?.events?.shared?.discord ?? false, + webhook: s?.events?.shared?.webhook ?? false, }, taskDone: { email: s?.events?.taskDone?.email ?? false, discord: s?.events?.taskDone?.discord ?? false, + webhook: s?.events?.taskDone?.webhook ?? false, }, watched: { email: s?.events?.watched?.email ?? true, discord: s?.events?.watched?.discord ?? true, + webhook: s?.events?.watched?.webhook ?? false, }, }, dailySummary: { @@ -78,6 +83,7 @@ export function buildNotificationSettings(user: User): NotificationSettings { time: s?.dailySummary?.time || '07:00', email: s?.dailySummary?.email ?? true, discord: s?.dailySummary?.discord ?? false, + webhook: s?.dailySummary?.webhook ?? false, }, }; } @@ -250,9 +256,6 @@ export const UserSettingsView: React.FC = ({ const fileInputRef = useRef(null); // --- Multi-Channel Alarms State --- - const [alarmChannels, setAlarmChannels] = useState( - Array.isArray(currentUser.alarmChannels) ? currentUser.alarmChannels : [] - ); const [notificationSettings, setNotificationSettings] = useState(() => buildNotificationSettings(currentUser) ); @@ -270,7 +273,6 @@ export const UserSettingsView: React.FC = ({ setDisplayName(currentUser.displayName || ''); setEmail(currentUser.email || ''); setAvatarUrl(currentUser.avatarUrl || ''); - setAlarmChannels(Array.isArray(currentUser.alarmChannels) ? currentUser.alarmChannels : []); setNotificationSettings(buildNotificationSettings(currentUser)); setAlarmEmail(currentUser.alarmEmail || currentUser.email || ''); setDiscordWebhookUrl(currentUser.discordWebhookUrl || ''); @@ -278,13 +280,8 @@ export const UserSettingsView: React.FC = ({ setNotifyBeforeMinutes(currentUser.notifyBeforeMinutes ?? 30); }, [currentUser]); - const toggleAlarmChannel = (ch: string) => { - setAlarmChannels((prev) => - prev.includes(ch) ? prev.filter((item) => item !== ch) : [...prev, ch] - ); - }; - const setEventChannel = (eventKey: keyof NotificationEvents, channel: 'email' | 'discord', value: boolean) => { + const setEventChannel = (eventKey: keyof NotificationEvents, channel: 'email' | 'discord' | 'webhook', value: boolean) => { setNotificationSettings((prev) => ({ ...prev, events: { @@ -386,7 +383,7 @@ export const UserSettingsView: React.FC = ({ const nextAlarmChannels = [ ...(notificationSettings.events.due.email ? ['email'] : []), ...(notificationSettings.events.due.discord ? ['discord'] : []), - ...(alarmChannels.includes('webhook') ? ['webhook'] : []), + ...(notificationSettings.events.due.webhook ? ['webhook'] : []), ]; const updated = await api.updateSettings({ alarmChannels: nextAlarmChannels, @@ -1119,13 +1116,14 @@ export const UserSettingsView: React.FC = ({ - {/* Routing Matrix: Events x (E-Mail / Discord) */} + {/* Routing Matrix: Events x (E-Mail / Discord / own webhook) */} {(() => { const hasDiscordWebhook = Boolean(discordWebhookUrl.trim()); + const hasCustomWebhook = Boolean(customWebhookUrl.trim()); return ( <>
-
+
Ereignis @@ -1135,6 +1133,10 @@ export const UserSettingsView: React.FC = ({ Discord + + + Webhook +
@@ -1143,7 +1145,7 @@ export const UserSettingsView: React.FC = ({ return (
{row.label}
@@ -1165,6 +1167,18 @@ export const UserSettingsView: React.FC = ({ } />
+
+ setEventChannel(row.key, 'webhook', v)} + title={ + hasCustomWebhook + ? `${row.label} – Webhook` + : 'Bitte zuerst unten eine eigene Webhook URL eintragen' + } + /> +
); })} @@ -1178,6 +1192,11 @@ export const UserSettingsView: React.FC = ({ Discord-Schalter sind deaktiviert, solange unten kein Discord-Webhook eingetragen ist.

)} + {!hasCustomWebhook && ( +

+ Webhook-Schalter werden aktiv, sobald unten eine eigene Webhook-URL eingetragen ist. +

+ )}
{/* Daily Summary */} @@ -1250,6 +1269,24 @@ export const UserSettingsView: React.FC = ({ } />
+
+ Webhook + + setNotificationSettings((prev) => ({ + ...prev, + dailySummary: { ...prev.dailySummary, webhook: v }, + })) + } + title={ + hasCustomWebhook + ? 'Tageszusammenfassung – Webhook' + : 'Bitte zuerst unten eine eigene Webhook URL eintragen' + } + /> +
@@ -1359,17 +1396,11 @@ export const UserSettingsView: React.FC = ({ {/* Channel 3: Custom HTTP Webhook */}
-