+
+
+ }
+ title={t("system.notifications.webhook.title")}
+ description={t("system.notifications.webhook.description")}
+ >
+
+
+ setDraft((current) => ({ ...current, webhookEnabled }))
+ }
+ />
+
+
+
+
+ }
+ title={t("system.notifications.webPush.title")}
+ description={t("system.notifications.webPush.description")}
+ >
+
+
+ setDraft((current) => ({ ...current, webPushEnabled }))
+ }
+ />
+
+
+ setDraft((current) => ({
+ ...current,
+ webPushSubject: event.target.value,
+ }))
+ }
+ />
+
+
+ {value.vapidPrivateKey.isConfigured
+ ? t("system.notifications.webPush.keysConfigured")
+ : t("system.notifications.webPush.keysGeneratedOnSave")}
+
+
+
+ {!isWebPushSupported() ? (
+
+ {t("system.notifications.webPush.unsupported")}
+
+ ) : null}
+
+ {subscriptions?.length ? (
+
+ {subscriptions.map((subscription) => (
+ -
+
+
+ {subscription.endpointOrigin}
+
+
+ {subscription.lastError ??
+ t("system.notifications.webPush.subscriptionActive")}
+
+
+
+
+ ))}
+
+ ) : null}
+
+
+
+
+
+
+
+ }
+ title={t("system.notifications.events.title")}
+ description={t("system.notifications.events.description")}
+ >
+
+ {eventTypes.map((eventType) => (
+
+ setDraft((current) => ({
+ ...current,
+ events: checked
+ ? [...current.events, eventType]
+ : current.events.filter((item) => item !== eventType),
+ }))
+ }
+ />
+ ))}
+
+
+
+ }
+ title={t("system.notifications.quiet.title")}
+ description={t("system.notifications.quiet.description")}
+ >
+
+
+
+ setDraft((current) => ({
+ ...current,
+ quietHoursStart: event.target.value || null,
+ }))
+ }
+ />
+
+
+
+ setDraft((current) => ({
+ ...current,
+ quietHoursEnd: event.target.value || null,
+ }))
+ }
+ />
+
+
+
+ setDraft((current) => ({
+ ...current,
+ timeZoneId: event.target.value,
+ }))
+ }
+ />
+
+
+
+
+ }
+ title={t("system.notifications.delivery.title")}
+ description={t("system.notifications.delivery.description")}
+ >
+ {!deliveries?.length ? (
+
+ {t("system.notifications.delivery.empty")}
+
+ ) : (
+
+ {deliveries.map((delivery) => (
+ -
+
+
+ {t(`system.notifications.events.items.${delivery.type}`, {
+ defaultValue: delivery.type,
+ })}
+
+
+ {t(
+ `system.notifications.delivery.channel.${delivery.channel}`,
+ )}
+
+ {delivery.lastError ? (
+
{delivery.lastError}
+ ) : null}
+
+
+ {t(`system.notifications.delivery.status.${delivery.status}`)}{" "}
+ ·{" "}
+ {new Date(delivery.occurredAt).toLocaleString(
+ i18n.resolvedLanguage,
+ )}
+
+
+ ))}
+
+ )}
+
+
+ void save()}
+ />
+
+ );
+};
diff --git a/SecondDimensionWatcherReDive.Client/src/components/settings/SettingsNavigation.tsx b/SecondDimensionWatcherReDive.Client/src/components/settings/SettingsNavigation.tsx
index 4fb1d4d3..678ea788 100644
--- a/SecondDimensionWatcherReDive.Client/src/components/settings/SettingsNavigation.tsx
+++ b/SecondDimensionWatcherReDive.Client/src/components/settings/SettingsNavigation.tsx
@@ -1,7 +1,14 @@
import React from "react";
import { useTranslation } from "react-i18next";
-import { Activity, Bot, Database, Download, Network } from "lucide-react";
+import {
+ Activity,
+ BellRing,
+ Bot,
+ Database,
+ Download,
+ Network,
+} from "lucide-react";
import { cn } from "../../lib/cn";
import { Select } from "./SettingsControls";
@@ -11,6 +18,7 @@ export const settingsSectionIds = [
"downloads",
"media",
"health",
+ "notifications",
"access",
] as const;
@@ -21,6 +29,7 @@ const sectionIcons: Record