import { useEffect, useState } from "react"; import { api, type AppSettings, type GotifySettings, type NtfySettings, type SmtpSettings, type WebhookSettings, type NotificationEvents, } from "../../api/client"; const TIMEZONES = [ "UTC", "Europe/Stockholm", "Europe/London", "Europe/Berlin", "Europe/Paris", "Europe/Oslo", "Europe/Helsinki", "Europe/Amsterdam", "Europe/Copenhagen", "America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles", "America/Toronto", "America/Sao_Paulo", "Asia/Tokyo", "Asia/Shanghai", "Asia/Singapore", "Asia/Dubai", "Australia/Sydney", ]; const DEFAULT_GOTIFY: GotifySettings = { enabled: false, url: "", token: "", priority: 5 }; const DEFAULT_NTFY: NtfySettings = { enabled: false, url: "https://ntfy.sh", topic: "", token: "", priority: 3 }; const DEFAULT_SMTP: SmtpSettings = { enabled: false, host: "", port: 587, secure: false, username: "", password: "", from: "", to: "" }; const DEFAULT_WEBHOOK: WebhookSettings = { enabled: false, url: "", secret: "" }; const DEFAULT_NOTIFICATIONS: NotificationEvents = { dnsAdd: true, dnsUpdate: true, dnsDelete: true, secretCheck: true, tailscaleKeyCheck: true, secretCheckTime: "08:00", timezone: "UTC", }; type TestResult = { ok: boolean; message: string } | null; function TestBadge({ result }: { result: TestResult }) { if (!result) return null; return (
{result.ok ? "✓" : "✕"} {result.message}
); } export default function NotificationSettings() { const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [saveError, setSaveError] = useState(null); const [gotify, setGotify] = useState(DEFAULT_GOTIFY); const [ntfy, setNtfy] = useState(DEFAULT_NTFY); const [smtp, setSmtp] = useState(DEFAULT_SMTP); const [webhook, setWebhook] = useState(DEFAULT_WEBHOOK); const [notifications, setNotifications] = useState(DEFAULT_NOTIFICATIONS); const [testingGotify, setTestingGotify] = useState(false); const [testGotifyResult, setTestGotifyResult] = useState(null); const [testingNtfy, setTestingNtfy] = useState(false); const [testNtfyResult, setTestNtfyResult] = useState(null); const [testingSmtp, setTestingSmtp] = useState(false); const [testSmtpResult, setTestSmtpResult] = useState(null); const [testingWebhook, setTestingWebhook] = useState(false); const [testWebhookResult, setTestWebhookResult] = useState(null); useEffect(() => { api.settings .get() .then((res: { settings: AppSettings }) => { setGotify(res.settings.gotify); setNtfy(res.settings.ntfy); setSmtp(res.settings.smtp); setWebhook(res.settings.webhook); setNotifications(res.settings.notifications); }) .catch((err) => setLoadError(err instanceof Error ? err.message : String(err))) .finally(() => setLoading(false)); }, []); async function handleSave() { setSaving(true); setSaveError(null); setSaved(false); try { await api.settings.update({ gotify, ntfy, smtp, webhook, notifications }); setSaved(true); setTimeout(() => setSaved(false), 3000); } catch (err) { setSaveError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } async function handleTestGotify() { setTestingGotify(true); setTestGotifyResult(null); try { await api.settings.testGotify(gotify); setTestGotifyResult({ ok: true, message: "Notification sent successfully." }); } catch (err) { setTestGotifyResult({ ok: false, message: err instanceof Error ? err.message : String(err) }); } finally { setTestingGotify(false); } } async function handleTestNtfy() { setTestingNtfy(true); setTestNtfyResult(null); try { await api.settings.testNtfy(ntfy); setTestNtfyResult({ ok: true, message: "Notification sent successfully." }); } catch (err) { setTestNtfyResult({ ok: false, message: err instanceof Error ? err.message : String(err) }); } finally { setTestingNtfy(false); } } async function handleTestSmtp() { setTestingSmtp(true); setTestSmtpResult(null); try { await api.settings.testSmtp(smtp); setTestSmtpResult({ ok: true, message: "Test email sent successfully." }); } catch (err) { setTestSmtpResult({ ok: false, message: err instanceof Error ? err.message : String(err) }); } finally { setTestingSmtp(false); } } async function handleTestWebhook() { setTestingWebhook(true); setTestWebhookResult(null); try { await api.settings.testWebhook(webhook); setTestWebhookResult({ ok: true, message: "Webhook delivered successfully." }); } catch (err) { setTestWebhookResult({ ok: false, message: err instanceof Error ? err.message : String(err) }); } finally { setTestingWebhook(false); } } if (loading) return
Loading…
; return ( <>

Notifications

{saveError && {saveError}} {saved && ✓ Settings saved}
{loadError &&
{loadError}
}

Gotify

Send push notifications to a self-hosted Gotify instance.

setGotify((g) => ({ ...g, url: e.target.value }))} />
setGotify((g) => ({ ...g, token: e.target.value }))} />
setGotify((g) => ({ ...g, priority: Number(e.target.value) }))} />

ntfy

Send notifications via ntfy.sh or a self-hosted instance.

setNtfy((n) => ({ ...n, url: e.target.value }))} />
setNtfy((n) => ({ ...n, priority: Number(e.target.value) }))} />
setNtfy((n) => ({ ...n, topic: e.target.value }))} />
setNtfy((n) => ({ ...n, token: e.target.value }))} />

Email (SMTP)

Send notifications via email using any SMTP server.

setSmtp((s) => ({ ...s, host: e.target.value }))} />
setSmtp((s) => ({ ...s, port: Number(e.target.value) }))} />
setSmtp((s) => ({ ...s, username: e.target.value }))} />
setSmtp((s) => ({ ...s, password: e.target.value }))} />
setSmtp((s) => ({ ...s, from: e.target.value }))} />
setSmtp((s) => ({ ...s, to: e.target.value }))} />

Webhook

POST a JSON payload to any HTTP endpoint. Uses Discord/Slack-compatible {"{ \"content\": \"…\" }"} format.

setWebhook((w) => ({ ...w, url: e.target.value }))} />
setWebhook((w) => ({ ...w, secret: e.target.value }))} />

Notify on

Controls which events trigger a notification on every enabled channel above.

{[ { key: "dnsAdd" as const, label: "DNS record added" }, { key: "dnsUpdate" as const, label: "DNS record updated" }, { key: "dnsDelete" as const, label: "DNS record deleted" }, { key: "secretCheck" as const, label: "Secret expiry reminder" }, { key: "tailscaleKeyCheck" as const, label: "Tailscale key expiry reminder" }, ].map(({ key, label }) => ( ))} {(() => { const dailyChecksEnabled = notifications.secretCheck || notifications.tailscaleKeyCheck; return (
setNotifications((n) => ({ ...n, secretCheckTime: e.target.value }))} />
Shared by the secret and Tailscale key expiry reminders above.
); })()}
); }