diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 162a7b02d9..279e960da9 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -17,6 +17,8 @@ import { useAppSettings, type AppSettings } from "@/hooks/use-settings"; import { useDaemonRegistry, type HostProfile, type HostConnection } from "@/contexts/daemon-registry-context"; import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons"; import { confirmDialog } from "@/utils/confirm-dialog"; +import { buildDaemonWebSocketUrl } from "@/utils/daemon-endpoints"; +import type { DoctorReport } from "@server/server/doctor/types"; import { MenuHeader } from "@/components/headers/menu-header"; import { useSessionStore } from "@/stores/session-store"; import { @@ -980,6 +982,9 @@ function HostDetailModal({ const activeConnection = runtimeSnapshot?.activeConnection ?? null; const lastError = runtimeSnapshot?.lastError ?? null; const [isRestarting, setIsRestarting] = useState(false); + const [doctorReport, setDoctorReport] = useState(null); + const [doctorLoading, setDoctorLoading] = useState(false); + const [doctorError, setDoctorError] = useState(null); const isHostConnected = useCallback(() => { if (!host) { return false; @@ -1093,6 +1098,30 @@ function HostDetailModal({ setDraftLabel(nextValue); }, []); + const runHealthCheck = useCallback(async () => { + const endpoint = + activeConnection?.endpoint ?? + host?.connections.find((c) => c.type === "directTcp")?.endpoint ?? + null; + if (!endpoint) return; + + setDoctorLoading(true); + setDoctorError(null); + try { + const parsed = new URL(buildDaemonWebSocketUrl(endpoint)); + parsed.protocol = parsed.protocol === "wss:" ? "https:" : "http:"; + const baseUrl = parsed.toString().replace(/\/ws\/?$/, ""); + const res = await fetch(`${baseUrl}/api/doctor`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const report = (await res.json()) as DoctorReport; + setDoctorReport(report); + } catch (err) { + setDoctorError(err instanceof Error ? err.message : "Health check failed"); + } finally { + setDoctorLoading(false); + } + }, [activeConnection, host]); + useEffect(() => { if (!visible || !host) return; // Initialize once per modal open / host switch; keep user edits fully local while typing. @@ -1103,6 +1132,9 @@ function HostDetailModal({ if (!visible) { setIsRestarting(false); setDraftLabel(""); + setDoctorReport(null); + setDoctorLoading(false); + setDoctorError(null); } }, [visible]); @@ -1186,6 +1218,75 @@ function HostDetailModal({ ) : null} + {/* Diagnostics */} + {host ? ( + + Diagnostics + + {doctorError ? ( + + {doctorError} + + ) : null} + {doctorReport ? ( + + {/* Summary */} + + {doctorReport.summary.ok > 0 ? ( + {doctorReport.summary.ok} passed + ) : null} + {doctorReport.summary.ok > 0 && (doctorReport.summary.warn > 0 || doctorReport.summary.error > 0) ? " · " : ""} + {doctorReport.summary.warn > 0 ? ( + {doctorReport.summary.warn} warning{doctorReport.summary.warn !== 1 ? "s" : ""} + ) : null} + {doctorReport.summary.warn > 0 && doctorReport.summary.error > 0 ? " · " : ""} + {doctorReport.summary.error > 0 ? ( + {doctorReport.summary.error} error{doctorReport.summary.error !== 1 ? "s" : ""} + ) : null} + + {/* Grouped checks */} + {(["provider", "config", "runtime"] as const).map((prefix) => { + const groupChecks = doctorReport.checks.filter((c) => c.id.startsWith(`${prefix}-`)); + if (groupChecks.length === 0) return null; + const groupLabel = prefix === "provider" ? "Providers" : prefix === "config" ? "Config" : "Runtime"; + return ( + + + {groupLabel} + + {groupChecks.map((check) => ( + + + {check.status === "ok" ? "✓" : check.status === "warn" ? "⚠" : "✗"} + + + {check.label} + {check.detail} + + + ))} + + ); + })} + + ) : null} + + ) : null} + {/* Save/Cancel + Advanced */}