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
+3
View File
@@ -34,6 +34,7 @@ import type {
ResolvedPath,
ResetLocalDatabaseInput,
ResetLocalDatabaseResponse,
SSHValidationResult,
DashboardShortcut,
DashboardShortcutInput,
} from "../types";
@@ -330,6 +331,8 @@ export const saveMonitoringMachine = (machine: MonitoringMachineInput) =>
}
return response.json() as Promise<MonitoringMachine>;
});
export const testMonitoringMachineSSH = (machine: MonitoringMachineInput) =>
post<SSHValidationResult>("/api/settings/machines/test-ssh", machine);
export const deleteMonitoringMachine = (machineId: string) =>
del<{ status: string }>(
`/api/settings/machines/${encodeURIComponent(machineId)}`,
+8
View File
@@ -13,12 +13,14 @@ import {
saveTask,
deleteTask,
runTask,
testMonitoringMachineSSH,
} from "../api/client";
import type {
MonitoringMachineInput,
ResetLocalDatabaseInput,
SavedTaskInput,
SSHKeyInput,
SSHValidationResult,
} from "../types";
export function useMonitoringSettings() {
@@ -134,6 +136,12 @@ export function useSaveMonitoringMachine() {
});
}
export function useTestMonitoringMachineSSH() {
return useMutation<SSHValidationResult, Error, MonitoringMachineInput>({
mutationFn: testMonitoringMachineSSH,
});
}
export function useDeleteMonitoringMachine() {
const queryClient = useQueryClient();
return useMutation({
+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
+8
View File
@@ -283,6 +283,14 @@ export interface ResetLocalDatabaseResponse {
media_index_files: string[];
}
export interface SSHValidationResult {
status: string;
message: string;
host: string;
port: number;
known_hosts_updated: boolean;
}
export interface MonitoringPollerStatus {
worker_running: boolean;
stop_requested: boolean;