From ad6783b6a188f594228e3416dd960b74e6562acb Mon Sep 17 00:00:00 2001 From: Bobban Rydh Date: Tue, 15 Sep 2026 21:28:26 +0200 Subject: [PATCH] Extend badge color settings to cover integration types too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → DNS Badges only let you customize DNS provider badge colors. Expand it into a general Badges page with a second section for the six integration types (Tailscale, Proxmox, Synology, Semaphore, Gitea, Dockhand), applied to the type badge in the Manage integrations table. - New integrationColors key on AppSettings, stored/merged the same way as providerColors via the existing settingsStore. - New GET /api/settings/integration-colors — non-secret, any signed-in user, mirroring /provider-colors — so the badge color can be read without needing admin access to the full settings payload. - Renamed DnsBadgeSettings.tsx -> BadgeSettings.tsx (route /settings/dns-badges -> /settings/badges, sub-nav label "DNS Badges" -> "Badges") with both color sections saved together. Verified end-to-end against the real dev server: PUT persists integration colors independently of provider colors, the public integration-colors endpoint reflects updates immediately, and the change is audit-logged. Co-Authored-By: Claude Sonnet 5 --- README.md | 3 +- server/src/routes/settings.ts | 6 + server/src/services/settingsStore.ts | 3 + web/src/App.tsx | 4 +- web/src/api/client.ts | 2 + web/src/pages/Integrations.tsx | 19 ++- web/src/pages/Settings.tsx | 2 +- web/src/pages/settings/BadgeSettings.tsx | 142 ++++++++++++++++++++ web/src/pages/settings/DnsBadgeSettings.tsx | 94 ------------- 9 files changed, 175 insertions(+), 100 deletions(-) create mode 100644 web/src/pages/settings/BadgeSettings.tsx delete mode 100644 web/src/pages/settings/DnsBadgeSettings.tsx diff --git a/README.md b/README.md index 18de1c6..3c10e3c 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ All modules from the original plan are built: - **Settings** (admin-only) — notification channels (Gotify, ntfy, SMTP, generic webhook) with per-channel test buttons, per-event toggles (DNS record added/updated/deleted, daily secret-expiry reminder with a - configurable time/timezone), and DNS provider badge-color customization. + configurable time/timezone), and badge-color customization for both DNS + providers and integration types. All six integrations follow the same config-in-UI + encrypted-credentials pattern, added (and edited — e.g. to rotate an expired API token without diff --git a/server/src/routes/settings.ts b/server/src/routes/settings.ts index 002efc0..211aa42 100644 --- a/server/src/routes/settings.ts +++ b/server/src/routes/settings.ts @@ -22,6 +22,11 @@ settingsRouter.get("/provider-colors", asyncHandler(async (_req, res) => { res.json({ providerColors }); })); +settingsRouter.get("/integration-colors", asyncHandler(async (_req, res) => { + const { integrationColors } = await getSettings(); + res.json({ integrationColors }); +})); + const updateSchema = z.object({ gotify: z.object({ enabled: z.boolean(), url: z.string(), token: z.string(), priority: z.number() }).partial().optional(), ntfy: z.object({ enabled: z.boolean(), url: z.string(), topic: z.string(), token: z.string(), priority: z.number() }).partial().optional(), @@ -51,6 +56,7 @@ const updateSchema = z.object({ .partial() .optional(), providerColors: z.record(z.string()).optional(), + integrationColors: z.record(z.string()).optional(), }); settingsRouter.put("/", requireRole("admin"), asyncHandler(async (req, res) => { diff --git a/server/src/services/settingsStore.ts b/server/src/services/settingsStore.ts index c6008a3..8e16ce6 100644 --- a/server/src/services/settingsStore.ts +++ b/server/src/services/settingsStore.ts @@ -44,6 +44,7 @@ export interface NotificationEvents { } export type ProviderColors = Record; +export type IntegrationColors = Record; export interface AppSettings { gotify: GotifySettings; @@ -52,6 +53,7 @@ export interface AppSettings { webhook: WebhookSettings; notifications: NotificationEvents; providerColors: ProviderColors; + integrationColors: IntegrationColors; } const DEFAULTS: AppSettings = { @@ -68,6 +70,7 @@ const DEFAULTS: AppSettings = { timezone: "UTC", }, providerColors: {}, + integrationColors: {}, }; const KEYS = Object.keys(DEFAULTS) as (keyof AppSettings)[]; diff --git a/web/src/App.tsx b/web/src/App.tsx index 79276c4..c76482d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -13,7 +13,7 @@ import ServerDetail from "./pages/ServerDetail"; import Integrations from "./pages/Integrations"; import Settings from "./pages/Settings"; import NotificationSettings from "./pages/settings/NotificationSettings"; -import DnsBadgeSettings from "./pages/settings/DnsBadgeSettings"; +import BadgeSettings from "./pages/settings/BadgeSettings"; import CacheSettings from "./pages/settings/CacheSettings"; import AppShell from "./layout/AppShell"; @@ -95,7 +95,7 @@ export default function App() { > } /> } /> - } /> + } /> } /> diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 52ce216..611f1d5 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -126,6 +126,7 @@ export interface AppSettings { webhook: WebhookSettings; notifications: NotificationEvents; providerColors: Record; + integrationColors: Record; } export type AppSettingsPatch = { [K in keyof AppSettings]?: Partial }; @@ -653,6 +654,7 @@ export const api = { settings: { get: () => request<{ settings: AppSettings }>("/api/settings"), providerColors: () => request<{ providerColors: Record }>("/api/settings/provider-colors"), + integrationColors: () => request<{ integrationColors: Record }>("/api/settings/integration-colors"), update: (data: AppSettingsPatch) => request<{ settings: AppSettings }>("/api/settings", { method: "PUT", body: JSON.stringify(data) }), testGotify: (data: { url: string; token: string; priority?: number }) => diff --git a/web/src/pages/Integrations.tsx b/web/src/pages/Integrations.tsx index 23fd52c..6db0cb6 100644 --- a/web/src/pages/Integrations.tsx +++ b/web/src/pages/Integrations.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, type CSSProperties } from "react"; import { api, type CurrentUser, @@ -130,6 +130,12 @@ const TYPE_LABELS: Record = { dockhand: "Dockhand", }; +function typeBadgeStyle(colors: Record, type: IntegrationType): CSSProperties { + const color = colors[type]; + if (!color) return {}; + return { backgroundColor: `${color}22`, color, border: `1px solid ${color}55` }; +} + export default function Integrations({ user }: { user: CurrentUser }) { const isAdmin = user.role === "admin"; const canEdit = user.role === "admin" || user.role === "operator"; @@ -139,6 +145,11 @@ export default function Integrations({ user }: { user: CurrentUser }) { const [editingIntegration, setEditingIntegration] = useState(null); const [selectedId, setSelectedId] = useState(null); const [error, setError] = useState(null); + const [integrationColors, setIntegrationColors] = useState>({}); + + useEffect(() => { + api.settings.integrationColors().then((res) => setIntegrationColors(res.integrationColors)).catch(() => {}); + }, []); const [tailscaleData, setTailscaleData] = useState(null); const [tailscaleError, setTailscaleError] = useState(null); @@ -418,7 +429,11 @@ export default function Integrations({ user }: { user: CurrentUser }) { {integrations?.map((i) => ( {i.name} - {TYPE_LABELS[i.type]} + + + {TYPE_LABELS[i.type]} + + + + + + {loadError &&
{loadError}
} + +
+
+
+
+

DNS provider badge colors

+
+
+

Customize the color of each DNS provider's badge throughout the app.

+ setProviderColors((c) => ({ ...c, [id]: color }))} + /> +
+
+
+ +
+
+
+

Integration badge colors

+
+
+

Customize the color of each integration type's badge throughout the app.

+ setIntegrationColors((c) => ({ ...c, [id]: color }))} + /> +
+
+
+
+ + ); +} diff --git a/web/src/pages/settings/DnsBadgeSettings.tsx b/web/src/pages/settings/DnsBadgeSettings.tsx deleted file mode 100644 index 31cccaa..0000000 --- a/web/src/pages/settings/DnsBadgeSettings.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect, useState } from "react"; -import { api } from "../../api/client"; - -const PROVIDER_NAMES: Record = { - cloudflare: "Cloudflare", - loopia: "Loopia", - pihole: "Pi-hole", - azure: "Azure DNS", - cpanel: "cPanel", - technitium: "Technitium", -}; - -export default function DnsBadgeSettings() { - const [loading, setLoading] = useState(true); - const [loadError, setLoadError] = useState(null); - const [providerColors, setProviderColors] = useState>({}); - const [saving, setSaving] = useState(false); - const [saved, setSaved] = useState(false); - const [saveError, setSaveError] = useState(null); - - useEffect(() => { - api.settings - .providerColors() - .then((res) => setProviderColors(res.providerColors)) - .catch((err) => setLoadError(err instanceof Error ? err.message : String(err))) - .finally(() => setLoading(false)); - }, []); - - async function handleSave() { - setSaving(true); - setSaveError(null); - setSaved(false); - try { - const res = await api.settings.update({ providerColors }); - setProviderColors(res.settings.providerColors); - setSaved(true); - setTimeout(() => setSaved(false), 3000); - } catch (err) { - setSaveError(err instanceof Error ? err.message : String(err)); - } finally { - setSaving(false); - } - } - - if (loading) return
Loading…
; - - return ( - <> -
-

DNS Badges

-
- {saveError && {saveError}} - {saved && ✓ Colors saved} - -
-
- - {loadError &&
{loadError}
} - -
-
-

DNS provider badge colors

-
-
-

Customize the color of each DNS provider's badge throughout the app.

-
- {Object.entries(PROVIDER_NAMES).map(([id, name]) => { - const color = providerColors[id] ?? "#6366f1"; - return ( -
- - {name} - - setProviderColors((c) => ({ ...c, [id]: e.target.value }))} - /> - {color} -
- ); - })} -
-
-
- - ); -}