import { useEffect, useState } from "react"; import { api, type AppSettings, type GotifySettings, type NtfySettings, type SmtpSettings, type WebhookSettings, type NotificationEvents, type QuietHoursSettings, type HealthCheckSettings, } 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, dockerUpdateCheck: true, proxmoxBackupCheck: true, healthAlerts: true, automationAlerts: true, secretCheckTime: "08:00", timezone: "UTC", integrationFailureAlerts: true, integrationFailureThreshold: 3, }; const DEFAULT_QUIET_HOURS: QuietHoursSettings = { enabled: false, start: "22:00", end: "07:00" }; const DEFAULT_HEALTH_CHECKS: HealthCheckSettings = { serverOfflineMinutes: 60, diskUsagePercent: 90 }; 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 [quietHours, setQuietHours] = useState(DEFAULT_QUIET_HOURS); const [healthChecks, setHealthChecks] = useState(DEFAULT_HEALTH_CHECKS); const [queuedCount, setQueuedCount] = useState(null); const [flushing, setFlushing] = useState(false); const [flushResult, setFlushResult] = useState(null); 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); setQuietHours(res.settings.quietHours); setHealthChecks(res.settings.healthChecks); }) .catch((err) => setLoadError(err instanceof Error ? err.message : String(err))) .finally(() => setLoading(false)); api.settings .quietHoursQueue() .then((res) => setQueuedCount(res.count)) .catch(() => {}); }, []); async function handleSave() { setSaving(true); setSaveError(null); setSaved(false); try { await api.settings.update({ gotify, ntfy, smtp, webhook, notifications, quietHours, healthChecks }); setSaved(true); setTimeout(() => setSaved(false), 3000); } catch (err) { setSaveError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } async function handleFlushNow() { setFlushing(true); setFlushResult(null); try { const res = await api.settings.flushQuietHours(); setFlushResult(res.flushed > 0 ? `Sent a digest of ${res.flushed} queued notification${res.flushed !== 1 ? "s" : ""}.` : "Nothing was queued."); setQueuedCount(0); } catch (err) { setFlushResult(err instanceof Error ? err.message : String(err)); } finally { setFlushing(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" }, { key: "dockerUpdateCheck" as const, label: "Docker image update available" }, { key: "proxmoxBackupCheck" as const, label: "Proxmox backup failed or a guest has no coverage" }, { key: "healthAlerts" as const, label: "Server offline, disk nearly full, or Synology volume/disk problem" }, { key: "automationAlerts" as const, label: "Semaphore template or Gitea workflow run failed" }, { key: "integrationFailureAlerts" as const, label: "Integration/DNS provider failing repeatedly" }, ].map(({ key, label }) => ( ))}
setNotifications((n) => ({ ...n, integrationFailureThreshold: Number(e.target.value) }))} /> consecutive failures
Based on the Diagnostic Log. A recovery notice is sent once it succeeds again.
setHealthChecks((h) => ({ ...h, serverOfflineMinutes: Number(e.target.value) }))} /> min
Agents report every 15 min by default.
setHealthChecks((h) => ({ ...h, diskUsagePercent: Number(e.target.value) }))} /> %
Servers, Proxmox storage, Synology volumes. Checked every 15 min; a recovery notice follows.
{(() => { const dailyChecksEnabled = notifications.secretCheck || notifications.tailscaleKeyCheck || notifications.dockerUpdateCheck || notifications.proxmoxBackupCheck; return (
setNotifications((n) => ({ ...n, secretCheckTime: e.target.value }))} />
Shared by the secret, Tailscale key, Docker update, and Proxmox backup reminders above.
); })()}

Quiet hours

Notifications that fire during this window are held instead of sent immediately, then delivered as one digest at the end time (in the timezone set above). Scheduled daily reminders already only fire once at a time you pick, so this mainly affects instant ones — DNS changes and integration failure/recovery alerts.

setQuietHours((q) => ({ ...q, start: e.target.value }))} />
setQuietHours((q) => ({ ...q, end: e.target.value }))} />
A start time later than the end time (e.g. 22:00 → 07:00) is treated as spanning midnight.
{queuedCount !== null && queuedCount > 0 && (
{queuedCount} notification{queuedCount !== 1 ? "s" : ""} currently queued.
)} {flushResult &&
{flushResult}
}
); }