Skip to content

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

<OverviewSection tick={tick} onLoaded={() => setLastUpdated(Date.now())} />

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 -- useCallback for onLoaded.
  • 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.

Upgrade

cd ~/argos-edge && git pull
make deploy-prod        # sync + build-prod-image + force-recreate + verify-deploy
# EFFECT: docker inspect argos-prod-panel --format '{{.Image}}' must differ from before
sudo scripts/smoke/dashboard-refetch-loop.sh   # with the Dashboard open in a browser