- 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).
Collapse minor+patch bumps into one PR per ecosystem (npm/cargo) with majors in
their own grouped PR; add a github-actions group so action bumps batch instead of
one PR each. Replaces the many narrow per-family groups.
# [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))
Extend the pre-commit secret-scan to catch environment-specific values
(*.ts.net MagicDNS names and 100.64.0.0/10 tailnet IPs) so live infra can't be
committed into code, docs, or examples. Verified: detects leaks, no false
positive on 192.0.2.x or non-CGNAT 100.x, and the script does not self-trip.
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.
Replace the real tailnet hostname and IP with honcho.example.net / a generic
tailnet reference — specs and PRs should carry examples, not live endpoints.
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.
Collapse docker-compose.yml + docker-compose.prod.yml into one file with dev/prod
profiles sharing a YAML anchor: dev builds from source, prod pulls ghcr latest.
make up/prod select the profile; down passes both so it stops either. Drops the
separate prod file and the !reset hack.
Concise verbs: make up (dev build+run), make prod (pull published image),
make down (stop regardless of mode), make clean (down + drop local image).
Updates all docs and compose-file comment headers to match.
README, docs/docker.md, and AGENTS.md now cover make compose-up (dev-forward,
builds from source) vs make compose-up-prod (pulls ghcr latest) and compose-down.
docker-compose.yml now builds from source (dev-forward: run your local changes);
docker-compose.prod.yml overrides it to pull ghcr latest (build reset via !reset).
Adds make compose-up / compose-up-prod / compose-down. Env, ports, and extra_hosts
stay defined once in the base file; the prod override only swaps build -> image.
make smoke-docker builds the image, stands up a stub upstream + the container
on a shared network, and asserts forward+prefix-strip, upstream-header cleared,
421 on missing header, and 403 + reject sentinel on allowlist miss. Self-
contained (no tailnet), idempotent, local-only (Docker) like cargo-check.