Build the Settings module: notification channels, event toggles, DNS badge colors

The Settings page was a "coming soon" placeholder. Port Sloth Manager's
settings feature set: Gotify/ntfy/SMTP/webhook notification channels
(each with its own test-send button), per-event toggles (DNS record
added/updated/deleted, a daily secret-expiry digest with configurable
time/timezone), and per-provider DNS badge color customization.

Settings persist in the existing `settings` key/value table via a new
settingsStore service; a notify service fans a message out to every
enabled channel. DNS record add/update/delete now fire notifications,
and a node-schedule job re-arms itself whenever the notification
settings change. Removed the now-superseded GOTIFY_URL/GOTIFY_TOKEN
env vars in favor of in-app configuration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-15 12:14:05 +02:00
1 parent 3a53a86ce0
commit 3255314402
16 files changed
+1272 -23

No files matched your search

+17 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useState, type CSSProperties } from "react";
import {
api,
type CurrentUser,
@@ -21,6 +21,12 @@ const PROVIDER_LABELS: Record<DnsProviderType, string> = {
const emptyRecordForm: DnsRecordInput = { type: "A", name: "", content: "", ttl: 3600, priority: undefined, proxied: undefined };
function providerBadgeStyle(colors: Record<string, string>, providerType: DnsProviderType): CSSProperties {
const color = colors[providerType];
if (!color) return {};
return { backgroundColor: `${color}22`, color, border: `1px solid ${color}55` };
}
export default function Dns({ user }: { user: CurrentUser }) {
const isAdmin = user.role === "admin";
const canEdit = user.role === "admin" || user.role === "operator";
@@ -29,6 +35,11 @@ export default function Dns({ user }: { user: CurrentUser }) {
const [managingProviders, setManagingProviders] = useState(false);
const [selectedProviderId, setSelectedProviderId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [providerColors, setProviderColors] = useState<Record<string, string>>({});
useEffect(() => {
api.settings.providerColors().then((res) => setProviderColors(res.providerColors)).catch(() => {});
}, []);
const [zones, setZones] = useState<DnsZone[] | null>(null);
const [zonesError, setZonesError] = useState<string | null>(null);
@@ -212,7 +223,11 @@ export default function Dns({ user }: { user: CurrentUser }) {
{providers?.map((p) => (
<tr key={p.id}>
<td>{p.name}</td>
<td>{PROVIDER_LABELS[p.providerType]}</td>
<td>
<span className="badge" style={providerBadgeStyle(providerColors, p.providerType)}>
{PROVIDER_LABELS[p.providerType]}
</span>
</td>
<td>
<button
className={`badge ${p.enabled ? "bg-green-lt text-green" : "bg-secondary-lt text-secondary"} border-0`}