v1.3.38.0 -- Dashboard refetch loop fix¶
First release of the v1.3.38 series ("dashboard paints in under a second"). The full scope came out of the PHASE 0 review in docs/operations/ux-performance-review.md (section 7). This release ships only the one-line bug fix that goes first, its smoke, and the version bump. Everything else in the series (bounded RecentErrors, grouped security overview, stale-while- revalidate cache, shared cert-probe cache, 7d ranges on covering indexes, one /auth/me per session, Threats pagination) follows in separate small releases.
argosVersion 1.3.35.4 -> 1.3.38.0, frontend/package.json 1.3.35.4 -> 1.3.38.0. Panel binary changes.
The bug¶
frontend/src/pages/Dashboard.tsx rendered
and OverviewSection fetched inside
useEffect(() => { ... api.dashboardOverview().then(() => { ...; onLoaded(); }) }, [tick, onLoaded]);
Every completed fetch called onLoaded, which set state on the parent, which re-rendered and created a new arrow function, which changed the effect dependency, which re-ran the effect and fetched again. The loop ran at network latency for as long as the tab was open: 2,896 /api/* calls in 240 s from one headless tab during the review (~12 requests/s). The 30 s server cache kept each hit cheap, which is why it went unnoticed; the cost was a browser tab that never went idle, a panel that got hit the instant every cache entry expired, and CPU on a 2-vCPU host.
The fix¶
const onOverviewLoaded = useCallback(() => setLastUpdated(Date.now()), []);
...
<OverviewSection tick={tick} onLoaded={onOverviewLoaded} />
Stable identity; the effect now re-runs only when tick changes (every 30 s unless paused), which was the intent.
Smoke gate¶
scripts/smoke/dashboard-refetch-loop.sh (new). Runs on the host as root; the operator opens the Dashboard in a browser and keeps the tab in the foreground; the script counts GET /api/dashboard/overview requests addressed to the panel container on its docker bridge for 60 s. PASS when <= 10.
Why a packet tap and not a log: the prod panel runs in ARGOS_PANEL_MODE=lan, so panel API traffic never crosses Caddy and the Caddy access log has no record of it; the panel binary does not log requests either. The tap is passive (python3 stdlib AF_PACKET, bound to the compose network bridge, post-DNAT so LAN and host-originated requests are each seen once), sends nothing and modifies nothing. Self-test: 5 host-side curls in a 6 s window count as 5; a /api/hosts call in the same window is not counted.
Deploy EFFECT (v1.3.34.3 rule, no sentinel rebuild this time): image id sha256:d644aef5... (1.3.35) -> sha256:99f02c29... (1.3.38.0), /api/system/version 1.3.35/d6a32b3 -> 1.3.38.0/5ca2c90, only the panel container recreated (Caddy and CrowdSec kept their 2026-09-13 start time). Build + recreate took 95 s on the 2-vCPU host, peak load 6.1, minimum free RAM 273 MB.
Headless Chromium against prod is not used by this or any future smoke (operator rule, 2026-09-25: a browser pass drove the 2-vCPU LXC to load 11).
Results against ~/argos-prod, operator's own browser tab in the foreground for the whole 60 s window:
| Panel | GET /api/dashboard/overview in 60 s | Verdict |
|---|---|---|
| 1.3.35 (before) | 1,293 (~21.5/s) | FAIL |
| 1.3.38.0 (after, hard-refreshed tab) | 2 (the 30 s auto-refresh) | PASS |
Files changed¶
frontend/src/pages/Dashboard.tsx--useCallbackforonLoaded.backend/cmd/argos/main.go,frontend/package.json-- version.scripts/smoke/dashboard-refetch-loop.sh-- new smoke.CHANGELOG.md,docs/release-notes/v1.3.38.0.md,mkdocs.yml.