- page_size -> size to match API schema
- reverse -> filters.reverse to match SessionGet body type
- session.name -> session.metadata?.name (Session type has no name field)
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.
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).
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.
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.
/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.
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.
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.
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.
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).
# [0.14.0](https://github.com/offendingcommit/openconcho/compare/v0.13.1...v0.14.0) (2026-06-02)
### Bug Fixes
* **docker:** derive nginx resolver from container DNS ([66b299a](66b299a28e))
* **docker:** drop dead HONCHO_UPSTREAM and same-origin default ([a2854ab](a2854ab8ea))
* **web:** enforce upstream allowlist in vite dev proxy ([b4fac95](b4fac95f37))
* **web:** raise connection-test timeout for cold upstreams ([409d7d8](409d7d8be7))
* **web:** strip content-encoding from vite dev proxy responses ([6b602c0](6b602c05bb))
### Features
* **docker:** header-driven /api reverse proxy in nginx ([753c978](753c978f56))
* **docker:** render SSRF allowlist map from env ([0af1ad9](0af1ad923c))
* **docker:** split compose into dev-forward build and prod pull ([c9bd2db](c9bd2db07d))
* **web:** add dispatchFor transport helper for same-origin proxy ([9945e4c](9945e4cf14))
* **web:** dev /api proxy middleware mirroring nginx ([ab8a1ba](ab8a1ba866))
* **web:** route checkConnection and discovery through the proxy ([9893230](9893230cde))
* **web:** route web build through same-origin /api proxy ([0935099](0935099bc2))
Swap the 100.x CGNAT example for 192.0.2.10 (RFC 5737 TEST-NET-1) in the
token-transport guard tests — keeps the non-loopback-HTTP assertion, drops an
environment-specific address.
A cold/idle self-hosted Honcho can take ~5s on its first request (DB pool, tunnel
wake); the hardcoded 5s budget aborted just before the response and reported a
live instance as 'Connection timed out'. Extract CONNECTION_TIMEOUT_MS (15s),
make checkConnection's timeout injectable, and cover the budget behavior.
undici fetch auto-decompresses the body, so re-sending the upstream
content-encoding/length would cause ERR_CONTENT_DECODING_FAILED if Honcho
gzips. Drop those and hop-by-hop headers when relaying. nginx is unaffected.
Mirrors the nginx allowlist (spec section D) so make dev-web matches prod: when
OPENCONCHO_UPSTREAM_ALLOWLIST is set, non-matching upstreams get 403 +
X-Honcho-Proxy-Reject; unset stays open.
client.current and createScopedClient resolve transport via dispatchFor:
web -> absolute origin + /api base with an X-Honcho-Upstream header; Tauri ->
absolute instance URL + reqwest. Absolute base (not bare "/api") so openapi-fetch
can construct a Request under node/undici and in the browser alike. Fleet fan-out
is unchanged; fleet.test.tsx now asserts the proxy contract.
Make the web image drop-in for a Honcho docker-compose stack:
- nginx reverse-proxies /v3 and /health to $HONCHO_UPSTREAM (variable +
Docker resolver so it starts even before the upstream resolves), giving
the SPA a same-origin path to Honcho with no browser CORS.
- Runtime config: an entrypoint writes config.js from
OPENCONCHO_DEFAULT_HONCHO_URL, so one prebuilt image targets any backend
("same-origin" | absolute URL | empty). SPA seeds a first-run default
instance from it (additive; no-op in dev/desktop).
- docker-compose.yml example service + GHCR multi-arch publish workflow on
release.
- nginx.conf -> envsubst template; docs rewritten.
Closes#21. Closes#31.
Two focused Playwright tests, mirroring the existing sidebar.spec
pattern (no backend dependency — instances point at unreachable ports
and we only assert on rendered names + row count, not health):
- Fleet link in the sidebar navigates to /fleet
- /fleet renders one row per configured instance under the table role
Also adds a Fleet link assertion to the existing sidebar.spec so the
nav entry is covered on the dashboard route alongside the other top
links. Both new tests run under the existing `pnpm test:e2e` (not
gated in CI by design — matches the current Compare/Dashboard E2E
posture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Human peers surface as raw ids (WhatsApp `…-lid`, UUIDs). Read an
optional `display_name` from peer metadata and prefer it over the id in
the peer header and breadcrumb, with the raw id kept as a sub-caption.
An inline edit on the header writes `display_name` via useUpdatePeer
(merging existing metadata); blank clears it. Falls back to the id when
unset.
Closes#32.