From e66f927f89cbfda6e900080369599fe0d4eeb5bf Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Tue, 2 Jun 2026 16:41:51 -0500 Subject: [PATCH 01/11] feat(web): merge Fleet into a server-filterable Dashboard Implements Phase 1 of the UI navigation rework (docs/superpowers/specs/ 2026-06-02-ui-navigation-rework.md): the Dashboard now lists every workspace across every configured server as (), filterable by server, with cross-server aggregate cards (reusing computeFleetAggregates). Opening a workspace activates its server then drills into the existing detail route. Per-server fan-out lives in a ServerWorkspaceRows child (rules-of-hooks safe, mirrors FleetRow). Fleet removed from the sidebar nav; the /fleet route is left intact for now (full removal deferred to avoid churning fleet.test.tsx while #54 is open). --- .../src/components/dashboard/Dashboard.tsx | 461 +++++++----------- .../dashboard/ServerWorkspaceRows.tsx | 209 ++++++++ .../web/src/components/layout/Sidebar.tsx | 2 - packages/web/src/test/dashboard.test.tsx | 89 ++++ 4 files changed, 477 insertions(+), 284 deletions(-) create mode 100644 packages/web/src/components/dashboard/ServerWorkspaceRows.tsx create mode 100644 packages/web/src/test/dashboard.test.tsx diff --git a/packages/web/src/components/dashboard/Dashboard.tsx b/packages/web/src/components/dashboard/Dashboard.tsx index dfafe7d..ac72827 100644 --- a/packages/web/src/components/dashboard/Dashboard.tsx +++ b/packages/web/src/components/dashboard/Dashboard.tsx @@ -1,159 +1,83 @@ -import { Link } from "@tanstack/react-router"; +import { Link, useNavigate } from "@tanstack/react-router"; import { motion } from "framer-motion"; -import { Activity, Boxes, ChevronRight, CircleDot, LayoutDashboard } from "lucide-react"; -import { useState } from "react"; -import { useQueueStatus, useWorkspaces } from "@/api/queries"; -import type { components } from "@/api/schema.d.ts"; -import { ErrorAlert } from "@/components/shared/ErrorAlert"; -import { Skeleton } from "@/components/shared/Skeleton"; -import { Body, Muted, PageTitle, SectionHeading } from "@/components/ui/typography"; -import { useDemo } from "@/hooks/useDemo"; +import { Boxes, LayoutDashboard, Network, Settings as SettingsIcon } from "lucide-react"; +import { useCallback, useMemo, useState } from "react"; +import { + computeFleetAggregates, + DEFAULT_ROW_METRICS, + type FleetRowMetrics, +} from "@/components/fleet/fleetAggregates"; +import { EmptyState } from "@/components/shared/EmptyState"; +import { Body, PageTitle, SectionHeading } from "@/components/ui/typography"; +import { useInstances } from "@/hooks/useInstances"; +import type { Instance } from "@/lib/config"; import { COLOR } from "@/lib/constants"; import { formatCount } from "@/lib/utils"; +import { ServerWorkspaceRows } from "./ServerWorkspaceRows"; -type QueueStatus = components["schemas"]["QueueStatus"]; +const ALL_SERVERS = "all"; -// ─── Per-workspace queue row ───────────────────────────────────────────────── - -function WorkspaceQueueRow({ workspaceId }: { workspaceId: string }) { - const { mask } = useDemo(); - const { data, isLoading } = useQueueStatus(workspaceId); - - const pending = data?.pending_work_units ?? 0; - const active = data?.in_progress_work_units ?? 0; - const done = data?.completed_work_units ?? 0; - const total = data?.total_work_units ?? 0; - const isActive = active > 0 || pending > 0; - - return ( - - - - - {mask(workspaceId)} - - - - - - - {isLoading ? ( - - … - - ) : ( -
- {isActive ? ( - - - - ) : ( - - )} - - {isActive ? `${formatCount(pending + active)} pending` : "Idle"} - -
- )} - - - {( - [ - { key: "total", val: total, color: "var(--text-2)" }, - { key: "done", val: done, color: COLOR.success }, - { key: "active", val: active, color: COLOR.warning }, - { key: "pending", val: pending, color: "var(--text-3)" }, - ] satisfies Array<{ key: string; val: number; color: string }> - ).map(({ key, val, color }) => ( - - {isLoading ? "—" : formatCount(val)} - - ))} - +/** + * Unified, server-aware dashboard: every workspace across every configured server, + * labelled ` ()` and filterable by server. Aggregates fold in the + * cross-server totals (formerly the standalone Fleet view). Opening a workspace + * activates its server, then drills into the existing workspace detail route. + */ +export function Dashboard() { + const { instances, activeId, activate } = useInstances(); + const navigate = useNavigate(); + const [serverFilter, setServerFilter] = useState(ALL_SERVERS); + const [metricsById, setMetricsById] = useState>({}); + + const onMetrics = useCallback((id: string, m: FleetRowMetrics) => { + setMetricsById((prev) => ({ ...prev, [id]: m })); + }, []); + + const onOpenWorkspace = useCallback( + (instance: Instance, workspaceId: string) => { + if (instance.id !== activeId) activate(instance.id); + navigate({ to: "/workspaces/$workspaceId", params: { workspaceId } as never }); + }, + [activeId, activate, navigate], ); -} -// ─── Aggregate banner ───────────────────────────────────────────────────────── -// Each workspace row already called useQueueStatus — TanStack Query deduplicates -// the fetches so calling the same hooks here just reads from cache. - -function GlobalQueueBanner({ workspaces }: { workspaces: Array<{ id: string }> }) { - const statuses = workspaces.map((ws) => { - const { data } = useQueueStatus(ws.id); - return data as QueueStatus | undefined; - }); - - const totalPending = statuses.reduce((s, d) => s + (d?.pending_work_units ?? 0), 0); - const totalActive = statuses.reduce((s, d) => s + (d?.in_progress_work_units ?? 0), 0); - const totalDone = statuses.reduce((s, d) => s + (d?.completed_work_units ?? 0), 0); - const allLoaded = statuses.every((d) => d !== undefined); - - return ( -
- {( - [ - { label: "Workspaces", value: workspaces.length, color: "var(--text-1)", always: true }, - { label: "Total done", value: totalDone, color: COLOR.success, always: false }, - { label: "Active", value: totalActive, color: COLOR.warning, always: false }, - { - label: "Pending", - value: totalPending, - color: totalPending > 0 ? COLOR.warning : "var(--text-3)", - always: false, - }, - ] as Array<{ label: string; value: number; color: string; always: boolean }> - ).map(({ label, value, color, always }) => ( -
-
- {allLoaded || always ? formatCount(value) : "—"} -
-
- {label} -
-
- ))} -
+ const shownInstances = useMemo( + () => + serverFilter === ALL_SERVERS ? instances : instances.filter((i) => i.id === serverFilter), + [instances, serverFilter], ); -} -// ─── Main dashboard ─────────────────────────────────────────────────────────── - -export function Dashboard() { - const [page] = useState(1); - const { data, isLoading, error } = useWorkspaces(page, 50); + const agg = useMemo( + () => + computeFleetAggregates(shownInstances.map((i) => metricsById[i.id] ?? DEFAULT_ROW_METRICS)), + [shownInstances, metricsById], + ); - const workspaces = - (data as { items?: Array<{ id: string; created_at?: string }> } | undefined)?.items ?? []; - const total = (data as { total?: number } | undefined)?.total ?? 0; + if (instances.length === 0) { + return ( +
+ + + Go to Settings + + } + /> +
+ ); + } return (
@@ -165,163 +89,136 @@ export function Dashboard() { strokeWidth={1.5} /> Dashboard - {total > 0 && ( - - {total} workspace{total !== 1 ? "s" : ""} - - )} -
- Overview of your Honcho instance - - - - {isLoading && } - - {!isLoading && workspaces.length > 0 && ( -
- {/* Aggregate stat row */} - - - - - {/* Per-workspace queue table */} - -
- - Queue Status - - all workspaces · live polling - -
- -
- - - - {["Workspace", "Status", "Total", "Done", "Active", "Pending"].map((h) => ( - - ))} - - - - {workspaces.map((ws) => ( - - ))} - -
- {h} -
-
-
- - {total > workspaces.length && ( -

- Showing {workspaces.length} of {total} workspaces.{" "} - - View all → - -

- )} -
- )} - - {!isLoading && workspaces.length === 0 && ( -
- - No workspaces found. + {agg.totalInstances} server{agg.totalInstances !== 1 ? "s" : ""} +
- )} - - ); -} + Workspaces across every configured server + -function DashboardSkeleton() { - return ( -