feat(notify): own webhook as a full notification channel
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m59s

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 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 19:02:46 +01:00
1 parent 6c154af251
commit 261579f0f2
10 files changed
+202 -56

No files matched your search

@@ -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<UserSettingsViewProps> = ({
const fileInputRef = useRef<HTMLInputElement>(null);
// --- Multi-Channel Alarms State ---
const [alarmChannels, setAlarmChannels] = useState<string[]>(
Array.isArray(currentUser.alarmChannels) ? currentUser.alarmChannels : []
);
const [notificationSettings, setNotificationSettings] = useState<NotificationSettings>(() =>
buildNotificationSettings(currentUser)
);
@@ -270,7 +273,6 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
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<UserSettingsViewProps> = ({
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<UserSettingsViewProps> = ({
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<UserSettingsViewProps> = ({
</div>
</div>
{/* 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 (
<>
<div className="p-4 bg-canvas border border-border space-y-3">
<div className="grid grid-cols-[1fr_auto_auto] items-center gap-4 pb-2 border-b border-border">
<div className="grid grid-cols-[1fr_auto_auto_auto] items-center gap-2 sm:gap-4 pb-2 border-b border-border">
<span className="font-bold text-main">Ereignis</span>
<span className="w-16 text-center font-semibold text-muted flex items-center justify-center gap-1">
<Mail className="w-3.5 h-3.5 text-accent" />
@@ -1135,6 +1133,10 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<MessageSquare className="w-3.5 h-3.5 text-accent" />
<span>Discord</span>
</span>
<span className="w-16 text-center font-semibold text-muted flex items-center justify-center gap-1">
<Globe className="w-3.5 h-3.5 text-accent" />
<span>Webhook</span>
</span>
</div>
<div className="divide-y divide-border">
@@ -1143,7 +1145,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
return (
<div
key={row.key}
className="grid grid-cols-[1fr_auto_auto] items-center gap-4 py-2.5"
className="grid grid-cols-[1fr_auto_auto_auto] items-center gap-2 sm:gap-4 py-2.5"
>
<span className="font-medium text-main">{row.label}</span>
<div className="w-16 flex justify-center">
@@ -1165,6 +1167,18 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
}
/>
</div>
<div className="w-16 flex justify-center">
<ToggleSwitch
checked={routing.webhook}
disabled={!hasCustomWebhook}
onChange={(v) => setEventChannel(row.key, 'webhook', v)}
title={
hasCustomWebhook
? `${row.label} – Webhook`
: 'Bitte zuerst unten eine eigene Webhook URL eintragen'
}
/>
</div>
</div>
);
})}
@@ -1178,6 +1192,11 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
Discord-Schalter sind deaktiviert, solange unten kein Discord-Webhook eingetragen ist.
</p>
)}
{!hasCustomWebhook && (
<p className="text-[11px] text-faint">
Webhook-Schalter werden aktiv, sobald unten eine eigene Webhook-URL eingetragen ist.
</p>
)}
</div>
{/* Daily Summary */}
@@ -1250,6 +1269,24 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
}
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted">Webhook</span>
<ToggleSwitch
checked={notificationSettings.dailySummary.webhook}
disabled={!hasCustomWebhook}
onChange={(v) =>
setNotificationSettings((prev) => ({
...prev,
dailySummary: { ...prev.dailySummary, webhook: v },
}))
}
title={
hasCustomWebhook
? 'Tageszusammenfassung – Webhook'
: 'Bitte zuerst unten eine eigene Webhook URL eintragen'
}
/>
</div>
</div>
</div>
</div>
@@ -1359,17 +1396,11 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{/* Channel 3: Custom HTTP Webhook */}
<div
className={`p-4 border transition-colors space-y-3 bg-canvas ${
alarmChannels.includes('webhook') ? 'border-accent' : 'border-border'
customWebhookUrl.trim() ? 'border-accent' : 'border-border'
}`}
>
<div className="flex items-center justify-between gap-2">
<label className="flex items-center gap-2.5 cursor-pointer select-none">
<input
type="checkbox"
checked={alarmChannels.includes('webhook')}
onChange={() => toggleAlarmChannel('webhook')}
className="w-4 h-4 accent-blue-600"
/>
<div className="flex items-center gap-2.5">
<Globe className="w-4 h-4 text-accent" />
<div>
<span className="font-bold text-main block">
@@ -1377,10 +1408,10 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</span>
<span className="text-[11px] text-muted">
Für eigene Empfänger-Skripte, Ntfy, Gotify, Slack, Mattermost oder
Automatisierungen.
Automatisierungen. Welche Ereignisse hierher gehen, stellst du oben in der Spalte „Webhook“ ein.
</span>
</div>
</label>
</div>
<button
type="button"