feat(web): add Fleet dashboard view for cross-instance observability

Adds a new /fleet route that shows a fleet-wide overview of all
configured Honcho instances. Each row renders per-instance metrics
(workspace count, total conclusions, queue activity, last seen, health)
by fanning out scoped fetches via createScopedClient, and aggregates
into top-level metric cards.

Reuses the Phase 2 scoped-client pattern. Extends compareQueries.ts
with useScopedQueueStatus and useScopedConclusionsCount, plus option
builders so useQueries can fan out per-workspace requests inside
FleetRow without duplicating query logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Agents
2026-05-24 17:21:14 +01:00
committed by Offending Commit
parent dd2edbae99
commit 12712bb0b0
8 changed files with 659 additions and 0 deletions

View File

@@ -16,6 +16,9 @@ const CK = {
["compare", instId, "peer-representation", wsId, pId] as const,
peerCard: (instId: string, wsId: string, pId: string) =>
["compare", instId, "peer-card", wsId, pId] as const,
queueStatus: (instId: string, wsId: string) => ["compare", instId, "queue-status", wsId] as const,
conclusionsCount: (instId: string, wsId: string) =>
["compare", instId, "conclusions-count", wsId] as const,
};
export function useScopedWorkspaces(instance: Instance, page = 1, pageSize = 20) {
@@ -83,3 +86,43 @@ export function useScopedPeerCard(instance: Instance, workspaceId: string, peerI
enabled: Boolean(workspaceId) && Boolean(peerId),
});
}
// Option builders — used by both single-fetch hooks and useQueries fan-out (e.g. Fleet view).
export function scopedQueueStatusOptions(instance: Instance, workspaceId: string) {
return {
queryKey: CK.queueStatus(instance.id, workspaceId),
queryFn: async () => {
const client = createScopedClient(instance);
const { data, error } = await client.GET("/v3/workspaces/{workspace_id}/queue/status", {
params: { path: { workspace_id: workspaceId } },
});
return data ?? err(error);
},
enabled: Boolean(workspaceId),
refetchInterval: 10_000,
} as const;
}
export function scopedConclusionsCountOptions(instance: Instance, workspaceId: string) {
return {
queryKey: CK.conclusionsCount(instance.id, workspaceId),
queryFn: async () => {
const client = createScopedClient(instance);
const { data, error } = await client.POST("/v3/workspaces/{workspace_id}/conclusions/list", {
params: { path: { workspace_id: workspaceId }, query: { page: 1, size: 1 } },
body: {},
});
return data ?? err(error);
},
enabled: Boolean(workspaceId),
} as const;
}
export function useScopedQueueStatus(instance: Instance, workspaceId: string) {
return useQuery(scopedQueueStatusOptions(instance, workspaceId));
}
export function useScopedConclusionsCount(instance: Instance, workspaceId: string) {
return useQuery(scopedConclusionsCountOptions(instance, workspaceId));
}