import type { ReactNode } from "react"; import { useMemo, useState } from "react"; import type { MonitoringMachine, MonitoringMachineInput, SSHKey, SSHKeyInput, } from "../types"; import { useDeleteMonitoringMachine, useDeleteSSHKey, useGenerateSSHKey, useMonitoringSettings, useResetLocalDatabase, useSSHKeys, useSaveMonitoringMachine, useSaveSSHKey, useTestMonitoringMachineSSH, } from "../hooks/useSettings"; import { DialogFooter } from "../components/DialogFooter"; import { HoverEditButton } from "../components/HoverEditButton"; import { SectionCard } from "../components/SectionCard"; import { SelectionRailCard } from "../components/SelectionRailCard"; import { TabbedCard } from "../components/TabbedCard"; import { ConfirmDialog } from "../components/ConfirmDialog"; import { cn } from "@/lib/utils"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; const SERVICE_OPTIONS = [ { value: "monitoring", label: "Monitoring" }, { value: "files", label: "Files" }, { value: "nextcloud", label: "Nextcloud" }, ]; // Radix Select disallows empty-string item values, so the "no selection" option // maps to this sentinel and converts back to "" at the draft boundary. const NONE = "__none__"; type SettingsTab = "machines" | "ssh-keys" | "danger"; /** Small labeled-field wrapper replacing the MUI `` shell. */ function FormField({ label, htmlFor, helperText, children, }: { label: string; htmlFor?: string; helperText?: string; children: ReactNode; }) { return (
{children} {helperText ? (

{helperText}

) : null}
); } /** Overline section label (replaces MUI `Typography variant="overline"`). */ function SectionLabel({ title, description, }: { title: string; description: string; }) { return (

{title}

{description}

); } function emptyMachine( mode: MonitoringMachineInput["mode"] = "local", ): MonitoringMachineInput { return { id: null, name: mode === "local" ? "This machine" : "", mode, enabled: true, services: mode === "local" ? ["monitoring", "files"] : [], host: "", port: 22, username: "", key_directory: "", key_name: "", ssh_key_id: "", ssh_private_key: "", ssh_private_key_passphrase: "", password: "", media_root: "", path_prefix: "", notes: "", }; } function MachineEditor({ title, hint, machine, sshKeys, editingMachine, onChange, onValidateSSH, isValidatingSSH, sshValidationMessage, sshValidationError, sshValidationStatus, }: { title: string; hint?: string; machine: MonitoringMachineInput; sshKeys: SSHKey[]; editingMachine?: MonitoringMachine | null; onChange: ( draft: | MonitoringMachineInput | ((current: MonitoringMachineInput) => MonitoringMachineInput), ) => void; onValidateSSH: () => void; isValidatingSSH: boolean; sshValidationMessage: string; sshValidationError: string; sshValidationStatus: string; }) { const draft = machine; const setDraft = onChange; const isLocal = draft.mode === "local"; const selectedSSHKey = sshKeys.find((key) => key.id === draft.ssh_key_id); const enabledServices = draft.services.length; const placeholderIfSet = (isSet: boolean | undefined) => isSet ? "Set, not shown" : undefined; return (

{title}

{hint ? (

{hint}

) : null}
{draft.mode} {draft.enabled ? "enabled" : "disabled"} {`${enabledServices} services`}
setDraft((current) => ({ ...current, name: e.target.value })) } />
{editingMachine ? ( ) : ( )}
setDraft((current) => ({ ...current, enabled: checked })) } />
{SERVICE_OPTIONS.map((option) => { const checked = draft.services.includes(option.value); return ( ); })}
{!isLocal && ( <>
setDraft((current) => ({ ...current, host: e.target.value, })) } />
setDraft((current) => ({ ...current, port: Number(e.target.value || 22), })) } />
setDraft((current) => ({ ...current, username: e.target.value, })) } />
0 ? "Select a saved SSH key." : "No SSH keys are saved yet. Add one in the SSH Keys tab." } >
setDraft((current) => ({ ...current, password: e.target.value, })) } />
)}
setDraft((current) => ({ ...current, media_root: e.target.value, })) } />
{isLocal && (
)}
setDraft((current) => ({ ...current, notes: e.target.value })) } />
{selectedSSHKey ? ( Selected key: {selectedSSHKey.name} {selectedSSHKey.fingerprint ? ` · ${selectedSSHKey.fingerprint}` : ""} ) : !isLocal && sshKeys.length === 0 ? ( No SSH keys have been saved yet. Add one before configuring SSH machines. ) : draft.ssh_key_id ? ( The selected SSH key was not found. ) : null} {!isLocal && enabledServices === 0 && ( SSH machines usually need monitoring or files enabled. )} {!isLocal && (
Validate SSH before saving: this records the first trusted host key in the backend-managed known_hosts file, then checks SSH auth.

SSH status: {sshValidationStatus || "Not tested yet"}

{sshValidationMessage && ( {sshValidationMessage} )} {sshValidationError && ( {sshValidationError} )}
)}
); } function SSHKeyManager({ sshKeys, selectedKeyId, onSelectKeyId, }: { sshKeys: SSHKey[]; selectedKeyId: string; onSelectKeyId: (id: string) => void; }) { const saveKey = useSaveSSHKey(); const generateKey = useGenerateSSHKey(); const deleteKey = useDeleteSSHKey(); const [draft, setDraft] = useState({ id: null, name: "", private_key: "", public_key: "", fingerprint: "", passphrase: "", notes: "", }); const selectedKey = useMemo( () => sshKeys.find((key) => key.id === selectedKeyId) ?? null, [sshKeys, selectedKeyId], ); const editing = Boolean(draft.id); const clear = () => { setDraft({ id: null, name: "", private_key: "", passphrase: "", public_key: "", fingerprint: "", notes: "", }); onSelectKeyId(""); }; const loadKey = (key: SSHKey) => { onSelectKeyId(key.id); setDraft({ id: key.id, name: key.name, private_key: "", passphrase: "", public_key: key.public_key, fingerprint: key.fingerprint, notes: key.notes, }); }; return (
{ clear(); }} > New key } > {sshKeys.length === 0 && (

No SSH keys saved yet.

)} {sshKeys.map((key) => { const active = key.id === selectedKeyId; return (
loadKey(key)} className={cn( "group grid w-full cursor-pointer grid-cols-[minmax(0,1fr)_auto] gap-2 border-t border-border px-3 py-2.5 group-hover:[&_.rail-edit]:opacity-100", active ? "bg-muted" : "bg-card hover:bg-muted/50", )} >

{key.name}

{key.private_key_set ? "key saved" : "no key"} ·{" "} {key.usage_count} machine {key.usage_count === 1 ? "" : "s"}

loadKey(key)} />
); })}
{`${selectedKey.usage_count} machine${selectedKey.usage_count === 1 ? "" : "s"}`} ) : undefined } >
setDraft((current) => ({ ...current, name: e.target.value, })) } />
setDraft((current) => ({ ...current, passphrase: e.target.value, })) } />
setDraft((current) => ({ ...current, notes: e.target.value, })) } />