Skip to content

fix(health-freshness): dashboard calls gated /api/health anonymously — 401 every 60s per tab + seed-health freshness silently dead since #4715 #4902

Description

@koala73

Symptom

Opening https://worldmonitor.app/desktop (any variant, any anonymous user) floods the console with:

GET https://api.worldmonitor.app/api/health 401 (Unauthorized)
[App] Refresh health-freshness failed: Error: health freshness fetch failed: 401

Repeats every 60s per open tab (REFRESH_INTERVALS.healthFreshness = 60_000, scheduled with runImmediately: true for ALL variants including happy — src/App.ts:1971). Each failure is also reported through the Sentry-instrumented fetch → Sentry noise.

Root cause

#4715 (merged 2026-07-04) gated detailed /api/health behind an operator/enterprise key server-side, but never swept the dashboard client caller. api/health.js now returns:

{"error":"API key required","hint":"Detailed health requires an operator/enterprise API key. Public status: /api/health?compact=1"}

with WWW-Authenticate: ApiKey realm="worldmonitor-health".

The dashboard client src/services/health-freshness.ts:80 still defaults to the bare, now-gated /api/health and sends no key (correctly — it's an anonymous browser). #4715's diff touched only api/health.js, docs, and server tests — zero client files.

This is the same class as the classify-event 4xx flood (#4779#4865 → fix #4869): server-side gating without a client-caller sweep. Issue #4856 caught the advertised status endpoint (api-catalog + Link rel=status) but not this in-app caller.

Impact (beyond console noise)

The seed-health → data-freshness pipeline is silently dead for every visitor since the gate deployed: refreshDataFreshnessFromHealth() throws before dataFreshness.recordSeedHealth(), so panel freshness/staleness indicators never receive seed-health state (STALE_SEED, REDIS_DOWN outage flagging, etc.).

Fix direction

/api/health?compact=1 is public (probed 200 anon) and returns {status, summary, checkedAt, problems} where problems entries carry the same per-check shape (status, records, seedAgeMin, maxStaleMin) under the same check names the client already maps via HEALTH_CHECK_SOURCE_MAP. So:

  1. Point health-freshness.ts at ?compact=1 and adapt the parser: treat problems as the degraded subset, mark all other mapped sources OK (checks absent from problems are healthy by construction).
  2. Preserve the existing REDIS_DOWN/503 body-first handling (health-freshness.ts:89-102).
  3. Consider a 401/403 suppress-with-backoff in the scheduler path as belt-and-braces so a future re-gate degrades quietly instead of erroring every 60s (pattern from fix(classify): stop free/anon users flooding the premium-gated classify-event endpoint (#4865) #4869).
  4. Regression test: parser handles the compact payload; guard that the client endpoint stays on a keyless-accessible variant (the fix(agent-readiness): post-#4867 review findings — honest health-401/product-catalog contracts, pinned pricing/support markdown, API annual plan #4874 contract test pins the server side, nothing pins the client side).

Evidence

  • Prod probe 2026-07-05: bare /api/health → 401 + WWW-Authenticate; ?compact=1 → 200 with problems payload.
  • git show --stat 59e34e607 (fix(security): require auth for detailed health metadata #4715): no src/ files.
  • Gate: api/health.js:782-807; client: src/services/health-freshness.ts:80; scheduler: src/App.ts:1971, src/config/variants/base.ts:42.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions