feat(web): merge Fleet into a server-filterable Dashboard (UI nav Phase 1)#61
Open
offendingcommit wants to merge 12 commits into
Open
feat(web): merge Fleet into a server-filterable Dashboard (UI nav Phase 1)#61offendingcommit wants to merge 12 commits into
offendingcommit wants to merge 12 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
labelled
<workspace> (<server>), with a server filter (default "All servers").reuse
computeFleetAggregates— the former Fleet metrics.existing
/workspaces/$workspaceIddetail route — no new routes needed.ServerWorkspaceRowschild component(one per server → rules-of-hooks safe; mirrors the proven
FleetRowpattern).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 fromTOP_NAV.packages/web/src/test/dashboard.test.tsx— renders the route, asserts<server>labels + filter narrowing..github/actions/setup/action.yml— bumpactions/setup-nodev4 → v6.Validation
make ci-webgreen (lint + typecheck + tests + build).dashboard.test.tsxrenders the real Dashboard through the router (mocked transport)and asserts: both servers' workspaces appear labelled
(Neo)/(Iris); the filterlists each server; selecting one narrows to it.
Deferred (intentional)
/fleetroute +FleetDashboard/FleetRowremoval will be a follow-up PR nowthat feat(web): eliminate browser CORS via header-driven /api proxy #54 has merged (the merge conflict risk is gone).
/fleetstays reachable by URL(just unlinked from the sidebar) until the cleanup PR lands.
still need the brainstorming pass per the spec.
Independent of #54–#57 (touches none of their files).