mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 10:42:45 +00:00
feat: add diagnostics section to HostDetailModal
Add a "Diagnostics" section that lets users run a health check against their connected daemon via GET /api/doctor. Displays grouped check results with status icons and a summary line.
This commit is contained in:
@@ -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<DoctorReport | null>(null);
|
||||
const [doctorLoading, setDoctorLoading] = useState(false);
|
||||
const [doctorError, setDoctorError] = useState<string | null>(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({
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Diagnostics */}
|
||||
{host ? (
|
||||
<View style={styles.formField}>
|
||||
<Text style={styles.label}>Diagnostics</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onPress={() => { void runHealthCheck(); }}
|
||||
disabled={doctorLoading || !isConnected}
|
||||
>
|
||||
{doctorLoading ? "Running..." : doctorReport ? "Re-run" : "Run Health Check"}
|
||||
</Button>
|
||||
{doctorError ? (
|
||||
<Text style={{ color: theme.colors.destructive, fontSize: theme.fontSize.xs, marginTop: theme.spacing[2] }}>
|
||||
{doctorError}
|
||||
</Text>
|
||||
) : null}
|
||||
{doctorReport ? (
|
||||
<View style={{ marginTop: theme.spacing[2], gap: theme.spacing[3] }}>
|
||||
{/* Summary */}
|
||||
<Text style={{ fontSize: theme.fontSize.sm }}>
|
||||
{doctorReport.summary.ok > 0 ? (
|
||||
<Text style={{ color: theme.colors.palette.green[400] }}>{doctorReport.summary.ok} passed</Text>
|
||||
) : null}
|
||||
{doctorReport.summary.ok > 0 && (doctorReport.summary.warn > 0 || doctorReport.summary.error > 0) ? " · " : ""}
|
||||
{doctorReport.summary.warn > 0 ? (
|
||||
<Text style={{ color: theme.colors.palette.amber[500] }}>{doctorReport.summary.warn} warning{doctorReport.summary.warn !== 1 ? "s" : ""}</Text>
|
||||
) : null}
|
||||
{doctorReport.summary.warn > 0 && doctorReport.summary.error > 0 ? " · " : ""}
|
||||
{doctorReport.summary.error > 0 ? (
|
||||
<Text style={{ color: theme.colors.destructive }}>{doctorReport.summary.error} error{doctorReport.summary.error !== 1 ? "s" : ""}</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
{/* 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 (
|
||||
<View key={prefix} style={{ gap: theme.spacing[1] }}>
|
||||
<Text style={{ color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, textTransform: "uppercase" }}>
|
||||
{groupLabel}
|
||||
</Text>
|
||||
{groupChecks.map((check) => (
|
||||
<View key={check.id} style={{ flexDirection: "row", alignItems: "flex-start", gap: theme.spacing[2] }}>
|
||||
<Text style={{
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: check.status === "ok"
|
||||
? theme.colors.palette.green[400]
|
||||
: check.status === "warn"
|
||||
? theme.colors.palette.amber[500]
|
||||
: theme.colors.destructive,
|
||||
}}>
|
||||
{check.status === "ok" ? "✓" : check.status === "warn" ? "⚠" : "✗"}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ fontSize: theme.fontSize.sm, color: theme.colors.foreground }}>{check.label}</Text>
|
||||
<Text style={{ fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted }}>{check.detail}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Save/Cancel + Advanced */}
|
||||
<View style={{ borderTopWidth: 1, borderTopColor: theme.colors.border, marginTop: theme.spacing[2], paddingTop: theme.spacing[4] }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
|
||||
Reference in New Issue
Block a user