diff --git a/backend/README.md b/backend/README.md index 22a66e5..3f9f8bf 100644 --- a/backend/README.md +++ b/backend/README.md @@ -126,7 +126,9 @@ docker compose up --build 2. After the API is running, open the app, go to **Settings**, and add machine entries: - **Local**: monitors the API host itself without SSH. - **SSH**: monitors another machine using a host, username, and a private key pasted directly into the machine settings, with an optional passphrase. - - Use **Validate SSH + trust host** in the machine editor before saving to test the banner/auth flow and record the first trusted host key into the backend-managed `known_hosts` file. + - The machine editor groups Connection, Monitoring / Files, Jellyfin, and Jellyseerr settings under separate headings so each service area is easier to scan. + - Saving an SSH machine now validates the banner/auth flow and records the first trusted host key into the backend-managed `known_hosts` file, surfacing any errors if the host cannot be reached or authenticated. + - Use **Validate SSH + trust host** in the machine editor before saving if you want to test the banner/auth flow explicitly. - The first successful SSH connection uses trust-on-first-use: the backend records that machine's host key into its managed `known_hosts` file automatically, then continues verifying it strictly on later connects. 3. Open **Monitoring** to see one section per configured machine. Each section uses its own collector state, disk path, metrics queries, and recent action history, which are populated automatically by the backend poller. diff --git a/backend/src/media_library_viewer_api/routers/settings.py b/backend/src/media_library_viewer_api/routers/settings.py index a8b65ff..bfac4d6 100644 --- a/backend/src/media_library_viewer_api/routers/settings.py +++ b/backend/src/media_library_viewer_api/routers/settings.py @@ -88,6 +88,42 @@ def _resolve_ssh_client( return client, host, port +def _raise_ssh_validation_error(host: str, port: int, exc: Exception) -> None: + message = str(exc) + lowered = message.lower() + if "protocol banner" in lowered: + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail=( + f"SSH banner not received from {host}:{port}; the backend could not complete the SSH handshake." + ), + ) from exc + if "no authentication methods available" in lowered or "authentication failed" in lowered: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=( + f"SSH authentication failed for {host}:{port}. " + "Check the selected SSH key, passphrase, username, or password." + ), + ) from exc + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail=f"SSH validation failed for {host}:{port}: {message}", + ) from exc + + +def _validate_saved_machine_ssh(machine: MonitoringMachineInput, store: SettingsStore) -> None: + if str(machine.mode or "").strip().lower() != "ssh": + return + client, host, port = _resolve_ssh_client(machine, store) + try: + client.connect() + except Exception as exc: + _raise_ssh_validation_error(host, port, exc) + finally: + client.close() + + @router.post("/machines/test-ssh") def test_machine_ssh( machine: MonitoringMachineInput, @@ -146,8 +182,11 @@ def post_machine( ) -> dict[str, Any]: saved = store.upsert_machine(machine.model_dump(exclude_none=True), machine.id) poller = get_monitoring_poller() - poller.start() - poller.kick() + try: + _validate_saved_machine_ssh(MonitoringMachineInput.model_validate(saved), store) + finally: + poller.start() + poller.kick() return saved @@ -161,8 +200,11 @@ def put_machine( raise HTTPException(status_code=404, detail="Machine not found") saved = store.upsert_machine(machine.model_dump(exclude_none=True), machine_id) poller = get_monitoring_poller() - poller.start() - poller.kick() + try: + _validate_saved_machine_ssh(MonitoringMachineInput.model_validate(saved), store) + finally: + poller.start() + poller.kick() return saved diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 6c3411e..c70b284 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -277,3 +277,5 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - 2026-05-07: The shell should display both frontend and backend version labels so deployed builds are easy to identify without opening a separate diagnostics screen. - 2026-05-07: SSH host verification should use trust-on-first-use for new machines by recording the first observed host key into the backend-managed known_hosts file, while still rejecting later key mismatches. - 2026-05-07: The SSH machine editor should expose a validation button that tests banner/auth flow and records the host key before save so users get clear feedback when a host is unreachable. +- 2026-05-07: Saving an SSH machine should also validate the banner/auth flow and update the backend-managed known_hosts entry for the current host, surfacing any save-time SSH errors to the user. +- 2026-05-07: Machine settings should visually separate Connection, Monitoring / Files, Jellyfin, Jellyseerr, and Notes into clearly labeled sections. diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts index 233559e..e2faaeb 100644 --- a/frontend/src/hooks/useSettings.ts +++ b/frontend/src/hooks/useSettings.ts @@ -129,7 +129,7 @@ export function useSaveMonitoringMachine() { return useMutation({ mutationFn: (machine: MonitoringMachineInput) => saveMonitoringMachine(machine), - onSuccess: () => { + onSettled: () => { queryClient.invalidateQueries({ queryKey: ["settings"] }); queryClient.invalidateQueries({ queryKey: ["monitoring"] }); }, diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 1703b37..ce0c8f0 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -147,6 +147,23 @@ function MachineEditor({ + + + + General + + + Name, mode, enabled state, and service roles. + + + {!isLocal && ( <> + + + + Connection + + + SSH host, port, username, and credentials. + + + )} + + + + Monitoring / Files + + + Media root and path mapping used by monitoring and browsing. + + + {hasJellyfin && ( <> + + + + Jellyfin + + + Library host and user selection for media browsing. + + + + + + + Jellyseerr + + + Optional request-manager enrichment for users and + requests. + + + )} + + + + Notes + + + Free-form administrator notes for this machine. + + +