Skip to content

feat(web): merge Fleet into a server-filterable Dashboard (UI nav Phase 1)#61

Open
offendingcommit wants to merge 12 commits into
mainfrom
feat/dashboard-fleet-merge
Open

feat(web): merge Fleet into a server-filterable Dashboard (UI nav Phase 1)#61
offendingcommit wants to merge 12 commits into
mainfrom
feat/dashboard-fleet-merge

Conversation

@offendingcommit
Copy link
Copy Markdown
Owner

@offendingcommit offendingcommit commented Jun 2, 2026

Summary

Implements Phase 1 of the UI navigation rework spec
(docs/superpowers/specs/2026-06-02-ui-navigation-rework.md, PR #55): merge the
single-instance Dashboard and the multi-instance Fleet view into one
server-aware Dashboard
.

  • The Dashboard now lists every workspace across every configured server,
    labelled <workspace> (<server>), with a server filter (default "All servers").
  • Cross-server aggregate cards (workspaces, conclusions, healthy/unreachable)
    reuse computeFleetAggregates — the former Fleet metrics.
  • Opening a workspace activates its server (if not active) then drills into the
    existing /workspaces/$workspaceId detail route — no new routes needed.
  • Per-server data fan-out lives in a new ServerWorkspaceRows child component
    (one per server → rules-of-hooks safe; mirrors the proven FleetRow pattern).
  • Fleet removed from the sidebar nav.

Files

  • packages/web/src/components/dashboard/Dashboard.tsx — rewritten as the unified parent.
  • packages/web/src/components/dashboard/ServerWorkspaceRows.tsx — new per-server child.
  • packages/web/src/components/layout/Sidebar.tsx — drop Fleet from TOP_NAV.
  • packages/web/src/test/dashboard.test.tsx — renders the route, asserts <server> labels + filter narrowing.
  • .github/actions/setup/action.yml — bump actions/setup-node v4 → v6.

Validation

  • make ci-web green (lint + typecheck + tests + build).
  • dashboard.test.tsx renders the real Dashboard through the router (mocked transport)
    and asserts: both servers' workspaces appear labelled (Neo)/(Iris); the filter
    lists each server; selecting one narrows to it.

Deferred (intentional)

  • The /fleet route + FleetDashboard/FleetRow removal will be a follow-up PR now
    that feat(web): eliminate browser CORS via header-driven /api proxy #54 has merged (the merge conflict risk is gone). /fleet stays reachable by URL
    (just unlinked from the sidebar) until the cleanup PR lands.
  • Phases 2–3 (Server as a navigable route level; Fleet as an ops/settings view)
    still need the brainstorming pass per the spec.

Independent of #54#57 (touches none of their files).

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 <workspace> (<server>), 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).
…setup-node to v6

userEvent.selectOptions hangs in jsdom when firing pointer + change event
sequences — fireEvent.change fires the React-controlled onChange directly
and is deterministic. Removes userEvent import (no longer used).

Bump actions/setup-node from v4 to v6 to clear the Node.js 20 deprecation
warning on GitHub Actions runners.
Remove lastSeen from metricsEqual in ServerWorkspaceRows: computeFleetAggregates
never reads lastSeen, so comparing it in metricsEqual causes background TanStack
Query refetches (dataUpdatedAt changes) to trigger spurious onMetrics callbacks,
cascading into a Dashboard → ServerWorkspaceRows render loop that hits React's
100-render limit in CI.

Add staleTime: Infinity to the test QueryClient to prevent background refetches
from interfering with test assertions.
If the user filters to a specific server and that instance is then deleted
(e.g. from Settings), shownInstances becomes [] — empty table, no message.
A useEffect resets the filter to ALL_SERVERS whenever the selected ID
disappears from the instances list.
/fleet now redirects to / so bookmarks and muscle-memory links land on the
unified Dashboard rather than a 404-style dead route. Fleet tests updated
to assert the redirect and the per-instance rows the Dashboard renders.
…nder loop

lastSeen tracked workspacesQ.dataUpdatedAt which updates on every background
refetch, creating a new metrics object reference each cycle. The useEffect
dep on metrics then fired unconditionally, calling onMetrics → setMetricsById
→ Dashboard re-render → another refetch → infinite loop.

computeFleetAggregates never reads lastSeen, so reporting it upward was
pointless. Hardcode null and drop it from the dep array entirely.
…r loop

Using a metrics object as a useEffect dep causes the loop:
  onMetrics → setMetricsById → Dashboard re-renders → ServerWorkspaceRows
  re-renders → useQueries runs → TanStack Query cache subscriber fires
  (Sidebar's setNow) → query result objects are new references → metrics
  useMemo returns new object → effect dep changed → onMetrics again → ∞

Fix: depend on the five primitive values (workspaceCount, conclusionCount,
queueActive, queuePending, health) directly. React compares primitives by
value, so the effect only fires when actual data changes, not on reference
churn from re-renders.

Also adds server-workspace-rows.test.tsx with three focused unit tests:
correct health:ok report, stability after load (no re-fire), and health
transition coverage.
…loop

Even with primitive useEffect deps, TanStack Query or React concurrent
rendering can cause onMetrics to fire with identical values mid-render-cycle.
Add a ref-based equality check in Dashboard.onMetrics: if all five metric
values are unchanged, skip setMetricsById entirely — no state update, no
Dashboard re-render, loop terminates.

Also fixes vi.fn<TFunction>() typing in server-workspace-rows.test.tsx to
satisfy tsc (Vitest 4 single-type-arg signature).
setNow(Date.now()) was called on every query-cache event, including
events dispatched synchronously during ServerWorkspaceRows' render.
On CI, consecutive Date.now() calls cross millisecond boundaries so
each call returns a new value — React always re-renders Sidebar, which
re-renders the layout, which re-renders ServerWorkspaceRows, which
fires more cache events. After ~25 cycles React throws "Maximum update
depth exceeded."

Fix: remove setNow from the cache subscriber. setNow now fires only
from the 30s interval timer, where it correctly refreshes the "X ago"
display text without triggering a render loop.

Also adds a deterministic CI-repro test that mocks Date.now to return
incrementing values and asserts the filter-change path completes without
looping.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant