Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 85 additions & 73 deletions control-plane/web/client/src/components/HealthStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,18 @@ export function HealthStrip({ className }: HealthStripProps) {
refreshAllLiveQueries,
} = useSSESync();
const llmLoading = llmHealth.isLoading;
const llmOk = llmHealth.data
? llmHealth.data.healthy &&
!llmHealth.data.endpoints?.some((ep) => !ep.healthy)
// LLM health monitoring is opt-in (`agentfield.llm_health` in the server
// config). Until the server reports it as enabled nothing is being measured,
// so the LLM indicator is not shown at all, rather than reading "Degraded"
// on every install that never configured it.
const llmMonitored = llmHealth.data?.enabled === true;
const llmOk = llmMonitored
? llmHealth.data!.healthy &&
!llmHealth.data!.endpoints?.some((ep) => !ep.healthy)
: undefined;
const llmAriaPart = llmMonitored
? `LLM ${llmOk === true ? "healthy" : llmOk === false ? "degraded" : "unknown"}, `
: "";

const nodes: AgentNodeSummary[] = agents.data?.nodes ?? [];
const totalAgents = agents.data?.count ?? nodes.length;
Expand Down Expand Up @@ -118,7 +126,7 @@ export function HealthStrip({ className }: HealthStripProps) {
variant="outline"
size="sm"
className={compactTriggerClass}
aria-label={`System status: LLM ${llmOk === true ? "healthy" : llmOk === false ? "degraded" : "unknown"}, ${onlineCount} of ${totalAgents} agents online, ${totalRunning} running, ${sseLabel}`}
aria-label={`System status: ${llmAriaPart}${onlineCount} of ${totalAgents} agents online, ${totalRunning} running, ${sseLabel}`}
>
<Activity className="size-3.5 shrink-0" aria-hidden />
<span className="tabular-nums text-muted-foreground">
Expand All @@ -142,36 +150,38 @@ export function HealthStrip({ className }: HealthStripProps) {
System status
</p>
<ul className="space-y-2.5 text-sm">
<li className="flex items-start gap-2">
{llmOk === true ? (
<CircleCheck
className="mt-0.5 size-4 shrink-0 text-green-500"
aria-hidden
/>
) : llmOk === false ? (
<CircleAlert
className="mt-0.5 size-4 shrink-0 text-destructive"
aria-hidden
/>
) : (
<CircleAlert
className="mt-0.5 size-4 shrink-0 text-amber-500"
aria-hidden
/>
)}
<div>
<div className="font-medium">LLM</div>
<div className="text-xs text-muted-foreground">
{llmOk === true
? "All LLM endpoints responding"
: llmOk === false
? "One or more LLM endpoints are unhealthy"
: llmLoading
? "Checking LLM health…"
: "LLM health status unavailable"}
{llmMonitored ? (
<li className="flex items-start gap-2">
{llmOk === true ? (
<CircleCheck
className="mt-0.5 size-4 shrink-0 text-green-500"
aria-hidden
/>
) : llmOk === false ? (
<CircleAlert
className="mt-0.5 size-4 shrink-0 text-destructive"
aria-hidden
/>
) : (
<CircleAlert
className="mt-0.5 size-4 shrink-0 text-amber-500"
aria-hidden
/>
)}
<div>
<div className="font-medium">LLM</div>
<div className="text-xs text-muted-foreground">
{llmOk === true
? "All LLM endpoints responding"
: llmOk === false
? "One or more LLM endpoints are unhealthy"
: llmLoading
? "Checking LLM health…"
: "LLM health status unavailable"}
</div>
</div>
</div>
</li>
</li>
) : null}
<li className="flex items-start gap-2">
<Server
className={cn(
Expand Down Expand Up @@ -251,46 +261,48 @@ export function HealthStrip({ className }: HealthStripProps) {
</div>

<div className="hidden items-center gap-2 text-xs sm:gap-3 xl:flex">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 sm:gap-1.5">
{llmOk === true ? (
<CircleCheck
className="size-3.5 shrink-0 text-green-500"
aria-hidden
/>
) : llmOk === false ? (
<CircleAlert
className="size-3.5 shrink-0 text-destructive"
aria-hidden
/>
) : (
<CircleAlert
className="size-3.5 shrink-0 text-amber-500"
aria-hidden
/>
)}
<span className="hidden text-muted-foreground lg:inline">
LLM
</span>
<Badge
variant={llmOk === true ? "secondary" : llmOk === false ? "destructive" : "outline"}
className="h-5 px-1.5 text-micro"
>
{llmOk === true ? "Healthy" : llmOk === false ? "Degraded" : "Unknown"}
</Badge>
</div>
</TooltipTrigger>
<TooltipContent>
{llmOk === true
? "All LLM endpoints responding"
: llmOk === false
? "One or more LLM endpoints are unhealthy"
: llmLoading
? "Checking LLM health…"
: "LLM health status unavailable"}
</TooltipContent>
</Tooltip>
{llmMonitored ? (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 sm:gap-1.5">
{llmOk === true ? (
<CircleCheck
className="size-3.5 shrink-0 text-green-500"
aria-hidden
/>
) : llmOk === false ? (
<CircleAlert
className="size-3.5 shrink-0 text-destructive"
aria-hidden
/>
) : (
<CircleAlert
className="size-3.5 shrink-0 text-amber-500"
aria-hidden
/>
)}
<span className="hidden text-muted-foreground lg:inline">
LLM
</span>
<Badge
variant={llmOk === true ? "secondary" : llmOk === false ? "destructive" : "outline"}
className="h-5 px-1.5 text-micro"
>
{llmOk === true ? "Healthy" : llmOk === false ? "Degraded" : "Unknown"}
</Badge>
</div>
</TooltipTrigger>
<TooltipContent>
{llmOk === true
? "All LLM endpoints responding"
: llmOk === false
? "One or more LLM endpoints are unhealthy"
: llmLoading
? "Checking LLM health…"
: "LLM health status unavailable"}
</TooltipContent>
</Tooltip>
) : null}

<Tooltip>
<TooltipTrigger asChild>
Expand Down
19 changes: 16 additions & 3 deletions control-plane/web/client/src/pages/NewDashboardPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ describe("NewDashboardPage", () => {
vi.clearAllMocks();
queryClient.clear();

// Default healthy state
// Default healthy state, with LLM health monitoring switched on.
vi.mocked(useLLMHealth).mockReturnValue({
data: { endpoints: [{ name: "test-llm", healthy: true }] },
data: { enabled: true, healthy: true, endpoints: [{ name: "test-llm", healthy: true }] },
isLoading: false,
});
vi.mocked(useQueueStatus).mockReturnValue({
Expand Down Expand Up @@ -203,10 +203,23 @@ describe("NewDashboardPage", () => {

expect(screen.getByText("System issues")).toBeInTheDocument();
expect(screen.getByText("LLM backend health")).toBeInTheDocument();
expect(screen.getByText("Healthy")).toBeInTheDocument();
expect(screen.getAllByText("Healthy").length).toBeGreaterThan(0);
expect(screen.getByText(/Queue at capacity for agent: agent-1/i)).toBeInTheDocument();
});

it("leaves out the LLM card when LLM health monitoring is not enabled", () => {
vi.mocked(useLLMHealth).mockReturnValue({
data: { enabled: false, healthy: false, endpoints: [] },
isLoading: false,
});
vi.mocked(useRuns).mockReturnValue({ isLoading: false, data: { workflows: [], total_count: 0 } });

render(<NewDashboardPage />, { wrapper });

expect(screen.queryByText("LLM backend health")).not.toBeInTheDocument();
expect(screen.queryByText("System issues")).not.toBeInTheDocument();
});

it("renders queue card disabled state when limiter is off", () => {
vi.mocked(useQueueStatus).mockReturnValue({
data: {
Expand Down
12 changes: 8 additions & 4 deletions control-plane/web/client/src/pages/NewDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -728,10 +728,14 @@ export function NewDashboardPage() {
/>
)}

<LLMHealthWidget
loading={llmHealthQuery.isLoading}
health={llmHealthQuery.data}
/>
{/* LLM health monitoring is opt-in; with it off there is nothing to
report, so the card is left out rather than saying "Disabled". */}
{llmHealthQuery.data?.enabled === true ? (
<LLMHealthWidget
loading={llmHealthQuery.isLoading}
health={llmHealthQuery.data}
/>
) : null}

<PrimaryRunFocus
loading={runsQuery.isLoading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe("HealthStrip", () => {
state.refreshAllLiveQueries.mockReset();
state.llmHealth.mockReturnValue({
isLoading: false,
data: { healthy: true, endpoints: [{ healthy: true }] },
data: { enabled: true, healthy: true, endpoints: [{ healthy: true }] },
});
state.queueStatus.mockReturnValue({
data: {
Expand Down Expand Up @@ -126,8 +126,9 @@ describe("HealthStrip", () => {

render(<HealthStrip />);

expect(screen.getByText("Unknown")).toBeInTheDocument();
expect(screen.getAllByText("Checking LLM health…")).toHaveLength(2);
// Until the server says monitoring is on, the LLM indicator is absent.
expect(screen.queryByText("LLM")).not.toBeInTheDocument();
expect(screen.queryByText("Checking LLM health…")).not.toBeInTheDocument();
expect(screen.getByText("0/1 online")).toBeInTheDocument();
expect(screen.getByText("Disconnected")).toBeInTheDocument();
expect(
Expand All @@ -137,4 +138,40 @@ describe("HealthStrip", () => {
await user.click(screen.getByRole("button", { name: "Refresh runs, agents, and dashboard data" }));
expect(state.refreshAllLiveQueries).toHaveBeenCalledTimes(1);
});

it("shows nothing about the LLM when monitoring is not enabled", () => {
state.llmHealth.mockReturnValue({
isLoading: false,
data: { enabled: false, healthy: false, endpoints: [] },
});

render(<HealthStrip />);

expect(screen.queryByText("LLM")).not.toBeInTheDocument();
expect(screen.queryByText("Degraded")).not.toBeInTheDocument();
expect(screen.queryByText("Healthy")).not.toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /^System status: 2 of 2 agents online, 3 running, Live$/i,
}),
).toBeInTheDocument();
});

it("shows Degraded when monitoring is enabled and an endpoint is down", () => {
state.llmHealth.mockReturnValue({
isLoading: false,
data: {
enabled: true,
healthy: true,
endpoints: [{ healthy: true }, { healthy: false }],
},
});

render(<HealthStrip />);

expect(screen.getByText("Degraded")).toBeInTheDocument();
expect(
screen.getAllByText("One or more LLM endpoints are unhealthy"),
).toHaveLength(2);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -272,5 +272,7 @@ describe("NewDashboardPage", () => {
expect(screen.getAllByText("No runs yet").length).toBeGreaterThan(0);
expect(screen.getByRole("button", { name: "View runs" })).toBeInTheDocument();
expect(screen.queryByText("System issues")).not.toBeInTheDocument();
// Monitoring is not enabled in this fixture, so no LLM card is drawn.
expect(screen.queryByText("LLM backend health")).not.toBeInTheDocument();
});
});
Loading