Add SSH connection validation for settings

This commit is contained in:
2026-05-07 18:31:49 +02:00
parent 2219a5730f
commit 84dc2bd2f6
9 changed files with 235 additions and 5 deletions
+74 -1
View File
@@ -28,6 +28,7 @@ import {
useSSHKeys,
useSaveMonitoringMachine,
useSaveSSHKey,
useTestMonitoringMachineSSH,
} from "../hooks/useSettings";
import { DialogFooter } from "../components/DialogFooter";
import { HoverEditButton } from "../components/HoverEditButton";
@@ -76,6 +77,10 @@ function MachineEditor({
machine,
sshKeys,
onChange,
onValidateSSH,
isValidatingSSH,
sshValidationMessage,
sshValidationError,
}: {
title: string;
hint?: string;
@@ -86,6 +91,10 @@ function MachineEditor({
| MonitoringMachineInput
| ((current: MonitoringMachineInput) => MonitoringMachineInput),
) => void;
onValidateSSH: () => void;
isValidatingSSH: boolean;
sshValidationMessage: string;
sshValidationError: string;
}) {
const draft = machine;
const setDraft = onChange;
@@ -417,6 +426,35 @@ function MachineEditor({
SSH machines usually need monitoring or files enabled.
</Alert>
)}
{!isLocal && (
<Stack spacing={1}>
<Alert severity="info">
Validate SSH before saving: this records the first trusted host key
in the backend-managed known_hosts file, then checks SSH auth.
</Alert>
<Stack direction="row" spacing={1} sx={{ flexWrap: "wrap" }}>
<Button
variant="outlined"
onClick={onValidateSSH}
disabled={
isValidatingSSH ||
!draft.host.trim() ||
!draft.username.trim()
}
>
{isValidatingSSH
? "Validating SSH..."
: "Validate SSH + trust host"}
</Button>
</Stack>
{sshValidationMessage && (
<Alert severity="success">{sshValidationMessage}</Alert>
)}
{sshValidationError && (
<Alert severity="error">{sshValidationError}</Alert>
)}
</Stack>
)}
{hasJellyseerr && !draft.jellyseerr_url && (
<Alert severity="info">
Jellyseerr is enabled, but no URL is configured yet.
@@ -828,7 +866,10 @@ export function Settings() {
const { data: sshKeys = [] } = useSSHKeys();
const saveMachine = useSaveMonitoringMachine();
const deleteMachine = useDeleteMonitoringMachine();
const testMachineSSH = useTestMonitoringMachineSSH();
const [tab, setTab] = useState<SettingsTab>("machines");
const [sshValidationMessage, setSSHValidationMessage] = useState("");
const [sshValidationError, setSSHValidationError] = useState("");
const [machineDialogOpen, setMachineDialogOpen] = useState(false);
const [machineDraft, setMachineDraft] = useState<MonitoringMachineInput>(
emptyMachine(),
@@ -848,26 +889,54 @@ export function Settings() {
const sshMachines = orderedMachines.filter(
(machine) => machine.mode === "ssh",
);
const clearSSHValidation = () => {
setSSHValidationMessage("");
setSSHValidationError("");
};
const beginLocal = () => {
clearSSHValidation();
setMachineDraft(emptyMachine("local"));
setMachineDialogOpen(true);
};
const beginRemote = () => {
clearSSHValidation();
setMachineDraft(emptyMachine("ssh"));
setMachineDialogOpen(true);
};
const openEditMachine = (machine: MonitoringMachineInput) => {
clearSSHValidation();
setMachineDraft(machine);
setMachineDialogOpen(true);
};
const closeMachineDialog = () => {
clearSSHValidation();
setMachineDialogOpen(false);
};
const updateMachineDraft = (
draft:
| MonitoringMachineInput
| ((current: MonitoringMachineInput) => MonitoringMachineInput),
) => {
clearSSHValidation();
setMachineDraft(draft);
};
const saveMachineDraft = async (draft: MonitoringMachineInput) => {
clearSSHValidation();
await saveMachine.mutateAsync(draft);
setMachineDialogOpen(false);
setMachineDraft(emptyMachine(draft.mode));
};
const validateMachineSSH = async () => {
clearSSHValidation();
try {
const result = await testMachineSSH.mutateAsync(machineDraft);
setSSHValidationMessage(result.message);
} catch (error) {
setSSHValidationError(
error instanceof Error ? error.message : String(error),
);
}
};
return (
<Stack spacing={2.25}>
<Stack spacing={0.5}>
@@ -1209,7 +1278,11 @@ export function Settings() {
}
machine={machineDraft}
sshKeys={sshKeys}
onChange={setMachineDraft}
onChange={updateMachineDraft}
onValidateSSH={validateMachineSSH}
isValidatingSSH={testMachineSSH.isPending}
sshValidationMessage={sshValidationMessage}
sshValidationError={sshValidationError}
/>
</DialogContent>
<DialogFooter