Skip to content

v1.3.38.1 -- go vet gate + two cold-path queries

Second release of the v1.3.38 series ("dashboard paints in under a second"; scope in docs/operations/ux-performance-review.md, section 7). Three items in the order the operator set:

  1. go vet ./... back to green (broken gate first).
  2. RecentErrors bounded to 24 h, two indexed sub-selects.
  3. Security overview per-host counters in one grouped query.

No schema change. Bans, reconcilers, AppSec and auth untouched. argosVersion 1.3.38.0 -> 1.3.38.1.

1. go vet

backend/internal/crowdsec/client_test.go held

mu := atomic.Int32{} // throwaway, ensures the slice address stays stable
_ = mu

which vet reports as "assignment copies lock value to _". The comment was wrong (a throwaway value cannot pin anything) and the lines had no effect. Removed, together with the now-unused sync/atomic import. go vet ./... exits 0 on the whole module.

2. RecentErrors (Dashboard health card)

Before (internal/dashboard/queries.go):

SELECT timestamp, source, level, message FROM log_entries
WHERE (source='caddy_error' AND level IN ('error','warn'))
   OR (source='caddy_access' AND status >= 500)
ORDER BY timestamp DESC
LIMIT ?
MULTI-INDEX OR
  INDEX 1: SEARCH log_entries USING INDEX idx_log_entries_source_ts (source=?)
  INDEX 2: SEARCH log_entries USING INDEX idx_log_entries_source_ts (source=?)
USE TEMP B-TREE FOR ORDER BY

Every caddy_error row and every caddy_access row (480k) is visited, each row page read to test level/status, then sorted. 39.4 s on a cold .backup copy of prod.

After:

SELECT timestamp, source, level, message FROM (
  SELECT ... FROM log_entries
  WHERE source = 'caddy_error' AND timestamp >= ? AND level IN ('error','warn')
  ORDER BY timestamp DESC LIMIT ?)
UNION ALL
SELECT timestamp, source, level, message FROM (
  SELECT ... FROM log_entries
  WHERE source = 'caddy_access' AND timestamp >= ? AND status >= 500
  ORDER BY timestamp DESC LIMIT ?)
ORDER BY timestamp DESC
LIMIT ?
MERGE (UNION ALL)
  LEFT:  SEARCH log_entries USING INDEX idx_log_entries_source_ts (source=? AND timestamp>?)
  RIGHT: SEARCH log_entries USING INDEX idx_log_entries_source_ts (source=? AND timestamp>?)

0.371 s on the same copy (cold cache). The bound is 24 h, which is what the card is for. Rows older than 24 h no longer appear; that is the one intentional behaviour change of this release.

3. Security overview

Before (internal/api/security_overview.go), per host:

SELECT COUNT(*) FROM log_entries
 WHERE source = 'waf_audit' AND host_id = ? AND timestamp >= ?
   AND waf_severity IN ('CRITICAL','ERROR');
SELECT MAX(timestamp) FROM log_entries
 WHERE source = 'waf_audit' AND host_id = ?;

Both plan through idx_log_entries_host_ts (host_id=?). The MAX one has no timestamp bound, so when a host has no waf_audit row (every host in prod: the source has 0 rows) it walks every row of that host: 21.9 s for the busiest host on the copy, 2.9 s for the bounded COUNT. Times 19 hosts, serialised on the single DB connection: 30-45 s cold for the endpoint.

After, one query for all hosts:

SELECT host_id,
       SUM(CASE WHEN timestamp >= ? AND waf_severity IN ('CRITICAL','ERROR') THEN 1 ELSE 0 END),
       MAX(timestamp)
  FROM log_entries
 WHERE source = 'waf_audit' AND host_id IS NOT NULL
 GROUP BY host_id
SEARCH log_entries USING INDEX idx_log_entries_source_ts (source=?)
USE TEMP B-TREE FOR GROUP BY

3 ms on the copy (0 waf_audit rows); 1.0 s when run against the 21k-row caddy_error source as a stand-in for a populated waf_audit. Semantics unchanged: blocked_24h counts CRITICAL/ERROR rows in the last 24 h, last_triggered_at is the newest waf_audit row for the host, hosts without rows get 0 and no timestamp. The aggregate's timestamp comes back from the driver as text, so it is parsed with the same lenient layouts the dashboard package uses.

Smoke gate

scripts/smoke/dashboard-latency.sh (new). With ARGOS_SESSION_TOKEN set, waits 40 s (cache TTL is 30 s), then one curl -w %{time_total} per endpoint. PASS when both are 200 and <= 2 s.

Panel dashboard/health cold security/overview cold Verdict
1.3.38.0 (before) 16.5 s 44.8 s FAIL
1.3.38.1 (after) 0.044 s 0.003 s PASS

Deploy EFFECT: image id sha256:99f02c29... (1.3.38.0) -> sha256:ec58cb56... (1.3.38.1), /api/system/version 1.3.38.0/5ca2c90 -> 1.3.38.1/9bcf244, only the panel container recreated (Caddy and CrowdSec kept their 2026-09-13 start time). Build + recreate 146 s with a cold build cache (the operator had pruned it after v1.3.38.0), peak load 10.9 on 2 vCPU, minimum free RAM 57 MB with 3.0 GB available.

go build ./..., go vet ./... (green), go test ./..., check-no-personal-data.sh, mkdocs build --strict all clean before commit.

Files changed

  • backend/internal/crowdsec/client_test.go -- vet fix.
  • backend/internal/dashboard/queries.go (+ queries_test.go) -- RecentErrors.
  • backend/internal/api/security_overview.go (+ security_overview_test.go) -- grouped query.
  • scripts/smoke/dashboard-latency.sh -- new smoke.
  • backend/cmd/argos/main.go, frontend/package.json -- version.
  • CHANGELOG.md, docs/release-notes/v1.3.38.1.md, mkdocs.yml, docs/operations/verification-report.md.

Upgrade

cd ~/argos-edge && git pull
make deploy-prod
# EFFECT: docker inspect argos-prod-panel --format '{{.Image}}' differs from before
ARGOS_SESSION_TOKEN=<cookie> scripts/smoke/dashboard-latency.sh