import { useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Bell, Clock, Plus, Send, Trash2, Users as UsersIcon, ShieldCheck, Power, Server, RefreshCw, HardDrive, } from "lucide-react"; import { toast } from "sonner"; import { Badge, Button, Card, Input, Spinner } from "@/components/ui"; import { settingsApi, usersApi, type Webhook, type WebhookInput, } from "@/api/settings"; import { destinationsApi, type BackupDestination } from "@/api/backups"; import { agentsApi } from "@/api/agents"; import { HostDot } from "@/components/hosts/HostDot"; import { apiErrorMessage } from "@/api/client"; import { useAuthStore } from "@/store/auth"; import type { Agent, User } from "@/types"; export function Settings() { const isAdmin = useAuthStore((s) => s.user?.role === "admin"); if (!isAdmin) { return (

Admin only

Settings are available to administrators only.

); } return (
); } /* -------------------------------------------------------------------------- */ /* Backup destinations */ /* -------------------------------------------------------------------------- */ const selectClass = "w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800"; const FIELDS: Record = { sftp: [ { key: "host", label: "Host" }, { key: "port", label: "Port", placeholder: "22" }, { key: "username", label: "Username" }, { key: "password", label: "Password", secret: true }, { key: "private_key", label: "Private key (optional, instead of password)", secret: true, area: true }, { key: "path", label: "Remote directory", placeholder: "/backups/stackpilot" }, ], s3: [ { key: "endpoint_url", label: "Endpoint URL (blank = AWS)", placeholder: "https://minio.example:9000" }, { key: "region", label: "Region", placeholder: "us-east-1" }, { key: "bucket", label: "Bucket" }, { key: "access_key", label: "Access key", secret: true }, { key: "secret_key", label: "Secret key", secret: true }, { key: "prefix", label: "Key prefix (optional)", placeholder: "stackpilot/" }, ], }; function DestinationsSection() { const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ["destinations"], queryFn: destinationsApi.list }); const [adding, setAdding] = useState(false); const invalidate = () => qc.invalidateQueries({ queryKey: ["destinations"] }); return (
}>Backup destinations
{isLoading ? ( ) : ( data?.map((d) => ) )} {data?.length === 0 && !adding && (

No destinations. Add an SFTP server or S3-compatible bucket to push stack backups off-box and restore from them.

)} {adding ? ( { setAdding(false); invalidate(); }} onCancel={() => setAdding(false)} /> ) : ( )}
); } function DestinationRow({ dest, onChange }: { dest: BackupDestination; onChange: () => void }) { const test = useMutation({ mutationFn: () => destinationsApi.test(dest.id), onSuccess: (r) => r.ok ? toast.success("Reachable") : toast.error(r.error || "Connection failed"), onError: (e) => toast.error(apiErrorMessage(e)), }); const remove = useMutation({ mutationFn: () => destinationsApi.remove(dest.id), onSuccess: () => { toast.success("Destination removed"); onChange(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const summary = dest.type === "s3" ? `${dest.config.bucket}${dest.config.prefix ? "/" + dest.config.prefix : ""}` : `${dest.config.username}@${dest.config.host}:${dest.config.path || "."}`; return (
{dest.name} {dest.type}

{summary}

); } function DestinationForm({ onDone, onCancel }: { onDone: () => void; onCancel: () => void }) { const [name, setName] = useState(""); const [type, setType] = useState("sftp"); const [config, setConfig] = useState>({}); const create = useMutation({ mutationFn: () => destinationsApi.create({ name, type, config }), onSuccess: () => { toast.success("Destination added"); onDone(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const setField = (k: string, v: string) => setConfig((c) => ({ ...c, [k]: v })); return (
{FIELDS[type].map((f) => (