From d323b1d68aa98cfd183f3fd3bb092bef59409a61 Mon Sep 17 00:00:00 2001 From: Anand Date: Thu, 10 Sep 2026 15:16:17 +0530 Subject: [PATCH] Wire wave-3 backend features into the UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SettingsPage: DigestSettingsCard (frequency, recipients, send-now) and LifecycleSettingsCard (retention window, enforce toggle) — live-verified save round-trips - ConnectionsPage: connection type selector (PVE/PBS, port auto-follows type), type badge on each row, Terraform/Ansible export dropdown on PVE connections - api.ts: Connection.type field - Live-verified end-to-end against a running backend: settings save, connection create for both types, export request/error-handling path --- .../settings/DigestSettingsCard.tsx | 130 ++++++++++++++++++ .../settings/LifecycleSettingsCard.tsx | 100 ++++++++++++++ web/src/lib/api.ts | 1 + web/src/pages/ConnectionsPage.tsx | 70 +++++++++- web/src/pages/SettingsPage.tsx | 4 + 5 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 web/src/components/settings/DigestSettingsCard.tsx create mode 100644 web/src/components/settings/LifecycleSettingsCard.tsx diff --git a/web/src/components/settings/DigestSettingsCard.tsx b/web/src/components/settings/DigestSettingsCard.tsx new file mode 100644 index 0000000..22b8b59 --- /dev/null +++ b/web/src/components/settings/DigestSettingsCard.tsx @@ -0,0 +1,130 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { ErrorState } from "@/components/ui/error-state" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Skeleton } from "@/components/ui/skeleton" +import { Switch } from "@/components/ui/switch" +import { Textarea } from "@/components/ui/textarea" +import { api, ApiError } from "@/lib/api" + +// Mirrors internal/api/digest_settings.go's digestSettingsResponse. +interface DigestSettings { + enabled: boolean + intervalHours: number + recipients: string[] + lastSentAt?: string +} + +const INTERVAL_OPTIONS = [ + { value: "24", label: "Daily" }, + { value: "168", label: "Weekly" }, + { value: "720", label: "Monthly" }, +] + +/** + * A periodic email summary of fleet health — uptime, backup success rate, + * active alerts, capacity trend — sent via the same SMTP config as the + * Notifications card above (internal/digest reuses notify.Notifier, no + * separate delivery channel). + */ +export function DigestSettingsCard() { + const query = useQuery({ + queryKey: ["admin", "settings", "digest"], + queryFn: () => api.get("/admin/settings/digest"), + }) + + return ( + + + Fleet health digest + A periodic email summary of uptime, active alerts, and capacity trend across every connection. + + + {query.isError ? ( + + ) : query.isLoading ? ( +
+ + +
+ ) : ( + + )} +
+
+ ) +} + +function DigestForm({ initial }: { initial: DigestSettings }) { + const queryClient = useQueryClient() + const [enabled, setEnabled] = useState(initial.enabled) + const [intervalHours, setIntervalHours] = useState(String(initial.intervalHours || 168)) + const [recipients, setRecipients] = useState(initial.recipients.join(", ")) + + const save = useMutation({ + mutationFn: () => + api.put("/admin/settings/digest", { + enabled, + intervalHours: Number(intervalHours), + recipients: recipients.split(",").map((r) => r.trim()).filter(Boolean), + }), + onSuccess: (data) => { + toast.success("Digest settings saved") + queryClient.setQueryData(["admin", "settings", "digest"], data) + }, + onError: (err) => toast.error(err instanceof ApiError ? err.message : "Failed to save digest settings"), + }) + + const sendNow = useMutation({ + mutationFn: () => api.post("/admin/settings/digest/send-now", {}), + onSuccess: () => toast.success("Digest sent"), + onError: (err) => toast.error(err instanceof ApiError ? err.message : "Failed to send digest"), + }) + + return ( + <> +
+
+

Send digest

+

+ {initial.lastSentAt ? `Last sent ${new Date(initial.lastSentAt).toLocaleString()}` : "Never sent yet."} +

+
+ +
+
+
+ + +
+
+ +