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 }
{saving ? "Saving…" : "Save Settings"}
{loadError && {loadError}
}
Gotify
{testingGotify ? "Sending…" : "Send test"}
ntfy
{testingNtfy ? "Sending…" : "Send test"}
Email (SMTP)
{testingSmtp ? "Sending…" : "Send test"}
Webhook
{testingWebhook ? "Sending…" : "Send test"}
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, [key]: e.target.checked }))}
/>
{label}
))}
{(() => {
const dailyChecksEnabled =
notifications.secretCheck || notifications.tailscaleKeyCheck || notifications.dockerUpdateCheck || notifications.proxmoxBackupCheck;
return (
Timezone
setNotifications((n) => ({ ...n, timezone: e.target.value }))}
>
{TIMEZONES.map((tz) => (
{tz}
))}
);
})()}
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, enabled: e.target.checked }))}
/>
Enable quiet hours
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.
{flushing ? "Sending…" : "Flush now"}
)}
{flushResult &&
{flushResult}
}
>
);
}