| --- |
| title: "Health Endpoints" |
| description: "Two health endpoints World Monitor exposes for monitoring data pipeline integrity — Edge Runtime handlers querying Upstash Redis for system state." |
| --- |
|
|
| ## `/api/health` |
|
|
| Primary health endpoint. Checks all Redis-backed data keys and seed freshness metadata in a single pipeline call. |
|
|
| **Authentication:** Compact health (`?compact=1`) is public for uptime and keyword monitors. Detailed health (`/api/health` without `compact=1`) and the operator history view (`?history=1`) require a valid operator/enterprise API key because they expose canonical Redis key names, record counts, and freshness thresholds. Browser origins must still pass the CORS allowlist in `api/_cors.js`; requests with no `Origin` header, such as server-side monitors, are allowed only for compact health unless they include an operator key. Health responses are never cached (`Cache-Control: private, no-store, max-age=0` and `CDN-Cache-Control: no-store`). |
|
|
| **HTTP Method:** `GET` |
|
|
| ### Query Parameters |
|
|
| | Parameter | Values | Description | |
| |-----------|--------|-------------| |
| | `compact` | `1` | Omit per-key details |
|
|
| ### Response Status Codes |
|
|
| | HTTP Status | Overall Status | Meaning | |
| |-------------|---------------|---------| |
| | `200` | `HEALTHY` | All checks OK, no warnings | |
| | `200` | `WARNING` | Some keys stale or on-demand keys empty, but no critical failures | |
| | `200` | `DEGRADED` | Critical keys empty, but ≤3% of all probed keys | |
| | `200` | `UNHEALTHY` | Critical keys empty, >3% of all probed keys | |
| | `401` | - | Detailed health or history requested without a valid operator/enterprise API key | |
| | `503` | `REDIS_DOWN` | Could not connect to Redis — the **only** state that returns a non-200 code | |
|
|
| > The overall health verdict lives in the JSON `status` field, not the HTTP code. Every state except `REDIS_DOWN` returns `200` so warn-level seed jitter doesn't flap HTTP-status monitors (see PR #2699). `REDIS_DOWN` returns `503` because with Redis unreachable the endpoint can assess nothing, so a plain HTTP probe must see a failure. |
|
|
| ### Response Body |
|
|
| ```json |
| { |
| "status": "HEALTHY | WARNING | DEGRADED | UNHEALTHY | REDIS_DOWN", |
| "summary": { |
| "total": 194, |
| "ok": 180, |
| "warn": 5, |
| "onDemandWarn": 9, |
| "staleContent": 0, |
| "crit": 0 |
| }, |
| "checkedAt": "2026-03-11T14:00:00.000Z", |
| "checks": { |
| "earthquakes": { |
| "status": "OK", |
| "records": 142, |
| "seedAgeMin": 8, |
| "maxStaleMin": 30 |
| } |
| } |
| } |
| ``` |
|
|
| `summary` fields: `total` is one entry per probed key (currently ~194 and growing as panels are added). `warn` **excludes** on-demand-empty keys — those are surfaced separately as `onDemandWarn` so they don't drive the overall verdict to `WARNING`. `staleContent` is a **subset** of `warn` (fresh seeder but the upstream feed stopped advancing). Only `crit` (`EMPTY`/`EMPTY_DATA`) drives `DEGRADED`/`UNHEALTHY`. |
|
|
| With `?compact=1`, the `checks` object is replaced by `problems` containing only non-OK keys. |
|
|
| ### Key Classifications |
|
|
| Keys are grouped into three tiers that determine alert severity: |
|
|
| | Tier | Severity when empty | Description | |
| |------|-------------------|-------------| |
| | **Bootstrap** | CRIT | Seeded data required at startup. Empty means the dashboard is missing critical data | |
| | **Standalone** | CRIT (seeded) / WARN (on-demand) | Populated by seed loops or RPC handlers. On-demand keys are expected to be empty until first request | |
| | **On-demand** | WARN | Populated lazily by RPC calls. Empty is normal if nobody has requested the data yet | |
|
|
| ### Per-Key Statuses |
|
|
| | Status | Severity | Meaning | |
| |--------|----------|---------| |
| | `OK` | Green | Data present, seed fresh | |
| | `OK_CASCADE` | Green | Key empty but a sibling in the cascade group has data (e.g., theater posture fallback chain) | |
| | `NOT_CONFIGURED` | Green | An optional source adapter this deployment never supplied a credential for (producer wrote `sourceState: "unavailable"`, e.g. the Global Tenders SAM.gov adapter without `SAM_GOV_API_KEY`). Not a fault and not counted as a problem |
| | `SOURCE_BLOCKED` | Green | The Japan MOD adapter proved no transport path reaches the publisher while retaining fresh reviewed records — either an upstream HTTP 403 after a successful proxy CONNECT (`HTTP_403`), or a target-scoped proxy CONNECT refusal corroborated by a successful control tunnel (`PROXY_TARGET_FORBIDDEN`). Uncorroborated CONNECT refusals, stale, empty, or unreviewed states still fail closed | |
| | `STALE_SEED` | Warn | Data present but `seed-meta` age exceeds `maxStaleMin` | |
| | `STALE_CONTENT` | Warn | Seeder is fresh but the upstream content stopped advancing (frozen feed) |
| | `COVERAGE_PARTIAL` | Warn | Record count is below the key's declared `minRecordCount` (e.g. 10/13 chokepoints or 139/174 PortWatch port-activity countries) | |
| | `SEED_ERROR` | Warn | `seed-meta` reports `status: "error"` from the last seed run | |
| | `REDIS_PARTIAL` | Warn | A single per-command Redis error on this key's STRLEN/GET (not a full outage) | |
| | `EMPTY_ON_DEMAND` | Warn | On-demand key has no data yet (expected until first request) |
| | `EMPTY` | Crit | Bootstrap or seeded standalone key has no data | |
| | `EMPTY_DATA` | Crit | Key exists but contains zero records (and 0 is not a valid state for it) | |
|
|
| ### China Coverage Projection |
|
|
| `chinaCoverage` projects the hourly Railway summary at |
| `health:china-coverage:v1`. The evaluator checks each launched China contract |
| for both a fresh producer heartbeat and fresh, substantive China content |
| fresh seed cannot hide stale or missing source content. `CHINA_DEGRADED` is a |
| warning projection for partial or stale coverage, while `CHINA_UNAVAILABLE` is |
| critical when the summary is invalid or the launched content is unavailable. |
|
|
| The final public composition is monitored separately as |
| `chinaDecisionSignals`. Its canonical payload must contain all six stable |
| groups even when individual groups are explicitly unavailable. Health requires |
| six group records and a seed no older than 60 minutes. Per-source transport |
| details for policy, exchanges, and cross-Strait publishers remain visible only |
| in this authenticated operator view |
| country summary or Pro MCP result. |
| Blocked China contracts remain visible in the audit with their stable reason |
| code but are excluded from the strict launched-entry health count. |
|
|
| The launched cross-Strait activity contract audits the durable archive |
| `military:cross-strait-activity:v1` independently for producer transport and |
| latest Taiwan MND reporting-window freshness. A fresh seed with a stale |
| official report therefore remains degraded. Japan Joint Staff reviewed |
| observations are regional augmentation and do not satisfy the Taiwan MND |
| content requirement. `/api/health` separately monitors |
| `military:cross-strait-activity-bootstrap:v1`; fresh canonical data cannot hide |
| a missing compact UI projection. It also exposes dedicated MND and Japan Joint |
| Staff transport records. Japan Joint Staff alone reports `SOURCE_BLOCKED`, and |
| only when retained reviewed records exist and one of two evidenced conditions |
| holds. `HTTP_403` means the direct request and an upstream response received |
| after a successful proxy CONNECT both returned HTTP 403 — the publisher itself |
| refused both paths. `PROXY_TARGET_FORBIDDEN` means the direct request returned |
| HTTP 403 and the proxy refused CONNECT for the target *while a control CONNECT |
| to a different contracted host succeeded in the same run through the same |
| credentials* — the proxy provider forbids this destination specifically, so no |
| configured transport path exists. An uncorroborated CONNECT refusal, a control |
| tunnel that also fails, `PROXY_AUTH_FAILED`, stale metadata, a missing source |
| record, or any other source using the blocked state still fail closed through |
| the existing `STALE_SEED`, `EMPTY`, or `SEED_ERROR` statuses. The distinction |
| matters operationally: `PROXY_TARGET_FORBIDDEN` is durable and needs a |
| different egress to change, whereas a bare CONNECT refusal is a proxy fault to |
| remediate. Other current fetch failures |
| report `SEED_ERROR` while the last-good archive remains available. |
| The bundle freshness gate advances only after the archive, projection, and |
| both source-health records publish successfully. The public bootstrap retains |
| the bounded reason codes used for disclosure but omits proxy response |
| diagnostics |
| source record. |
|
|
| Operators can obtain the same sanitized, read-only audit with |
| `node scripts/audit-china-coverage.mjs --json`; add `--strict` to return a |
| nonzero exit code unless every launched entry is healthy. The audit reads only |
| the compact Redis contracts and emits status, age, and reason-code summaries— |
| never credentials or raw upstream payloads. |
|
|
| ### Cascade Groups |
|
|
| Some keys use fallback chains. If any sibling has data, empty siblings report `OK_CASCADE`: |
|
|
| - **Theater Posture:** `theaterPostureLive` -> `theaterPosture` (stale) -> `theaterPostureBackup` |
| - **Military Flights:** `militaryFlights` -> `militaryFlightsStale` |
| - **Displacement:** `displacement` (current UTC year) -> `displacementPrev` (prior year, covers the Jan-1 window before the new-year seed runs) |
|
|
| `riskScores` is intentionally stricter than a raw feed heartbeat. Its |
| `recordCount` is realtime signal-density coverage: the count of score-relevant |
| Tier-1 conflict, news, and cyber signal families present during the CII refresh. |
| The conflict family is satisfied by either the ACLED path or the UCDP event |
| feed, matching the CII v8 scorer. When those feeds are reachable but quiet, |
| `riskScores` can still report |
| `COVERAGE_PARTIAL`; underlying feed freshness is tracked by the source-specific |
| health entries where those feeds publish seed metadata. |
|
|
| `portwatchPortActivity` also uses `minRecordCount`. A fresh |
| `seed-meta:supply_chain:portwatch-ports` record below 174 countries reports |
| `COVERAGE_PARTIAL` instead of `OK`; partial runs may still refresh per-country |
| PortWatch cache entries, but the canonical country list and healthy seed-meta |
| signal do not advance until full 174-country coverage returns. |
|
|
| ### Staleness Thresholds (maxStaleMin) |
|
|
| Selected thresholds from `SEED_META`: |
|
|
| | Domain | Max Stale (min) | Notes | |
| |--------|----------------|-------| |
| | Market quotes, crypto, sectors, earthquakes, insights | 30 | High-frequency relay loops / critical event data | |
| | Military flights | 30 | Near-real-time tracking | |
| | Predictions, flight delays (FAA) | 90 | Polymarket / airport snapshots | |
| | Unrest | 120 | 45min cron, 2h grace | |
| | Cyber threats | 240 | APT data updated less frequently | |
| | Wildfires | 360 | FIRMS NRT accumulates over hours | |
| | Climate anomalies | 540 | 3h cron, 3× cadence | |
| | BIS extended, World Bank, IMF | 2160-100800 | Institutional data, weekly/monthly/annual | |
|
|
| > These are illustrative |
|
|
| ### Example Requests |
|
|
| ```bash |
| # Full health check (requires an operator API key) |
| curl -s https://api.worldmonitor.app/api/health \ |
| -H "X-WorldMonitor-Key: $WORLDMONITOR_API_KEY" | jq . |
|
|
| # Compact (problems only) |
| curl -s "https://api.worldmonitor.app/api/health?compact=1" | jq . |
|
|
| # UptimeRobot / monitoring: check HTTP status code |
| curl -o /dev/null -s -w "%{http_code}" "https://api.worldmonitor.app/api/health?compact=1" |
| # Returns 503 only when Redis is unreachable (REDIS_DOWN) |
| # state — the verdict (HEALTHY/WARNING/DEGRADED/UNHEALTHY) is in the body's `status`. |
| ``` |
|
|
| ## `/api/seed-health` |
|
|
| Focused endpoint for seed loop freshness. Checks only `seed-meta:*` keys without fetching actual data payloads. |
|
|
| **Authentication:** Requires valid API key or allowed origin. |
|
|
| **HTTP Method:** `GET` |
|
|
| ### Response Status Codes |
|
|
| | HTTP Status | Overall Status | Meaning | |
| |-------------|---------------|---------| |
| | `200` | `healthy` | All seed loops reporting on time | |
| | `200` | `warning` | Some seeds stale (age > 2x interval) or below a declared coverage floor | |
| | `200` | `degraded` | Some seeds missing entirely | |
| | `401` | - | Invalid or missing API key | |
| | `503` | - | Redis unavailable | |
|
|
| ### Response Body |
|
|
| ```json |
| { |
| "overall": "healthy | warning | degraded", |
| "checkedAt": 1710158400000, |
| "seeds": { |
| "seismology:earthquakes": { |
| "status": "ok", |
| "fetchedAt": 1710158100000, |
| "recordCount": 142, |
| "sourceVersion": null, |
| "ageMinutes": 5, |
| "stale": false |
| }, |
| "market:stocks": { |
| "status": "stale", |
| "fetchedAt": 1710150000000, |
| "recordCount": 85, |
| "sourceVersion": null, |
| "ageMinutes": 140, |
| "stale": true |
| }, |
| "supply_chain:portwatch-ports": { |
| "status": "coverage_partial", |
| "fetchedAt": 1710158100000, |
| "recordCount": 139, |
| "minRecordCount": 174, |
| "sourceVersion": null, |
| "ageMinutes": 5, |
| "stale": true |
| } |
| } |
| } |
| ``` |
|
|
| ### Staleness Logic |
|
|
| A seed is considered stale when its age exceeds **2x the configured interval**. This accounts for normal jitter in cron/relay timing. Seeds with an explicit `minRecordCount` also report `coverage_partial` and `stale: true` until their coverage floor is met. |
|
|
| | Domain | Interval (min) | Stale After (min) | |
| |--------|---------------|-------------------| |
| | Predictions, military flights | 8 | 16 | |
| | Market quotes, earthquakes, unrest | 15 | 30 | |
| | ETF flows, stablecoins, chokepoints | 30 | 60 | |
| | Service statuses, spending, wildfires | 60 | 120 | |
| | Shipping rates, satellites | 90-120 | 180-240 | |
| | GPS jamming, displacement | 360 | 720 | |
| | Iran events, UCDP | 210-5040 | 420-10080 | |
|
|
| ### History ingestion (`intel-history:*`) |
|
|
| Domains prefixed `intel-history:` do **not** describe a seeder's canonical publish. They track whether that collector's post-publish append to the historical intelligence store is still landing: |
|
|
| | Domain | Tracks | |
| |--------|--------| |
| | `intel-history:conflict:acled-intel` | history appends from `seed-conflict-intel` | |
| | `intel-history:military:cross-strait-activity` | history appends from `seed-cross-strait-activity` | |
| | `intel-history:energy:intelligence` | history appends from `seed-energy-intelligence` | |
|
|
| The append is fail-open by design — the canonical publish has already committed when it runs, so a failure must never fail the run. That means the collector's own entry (`conflict:acled-intel`, …) stays `ok` while history silently stops accumulating. These entries are the separate signal: |
|
|
| - `fetchedAt` is the last **healthy** append, never the last attempt. A run that reached the relay advances it |
| - `status: "error"` means the append failed on two consecutive runs — or, on the very first tick, that a relay credential present during an earlier successful append has since been removed. |
| - `lastErrorCode` names the cause when there is one: `http_401`, `budget_exhausted`, `all_chunks_failed`, `config_removed`, or a clamped error-class name. Absent when the ingest has never failed. |
| - `status: "not_configured"` means this deployment has **never** had relay credentials. It is visible but never an alarm — no operator action clears it except provisioning the relay. Losing credentials after a successful append is not this state |
| - `recordCount` is the volume the relay accepted on the last successful append. Zero is valid: a run whose records were all deduped still proves the pipeline works. |
|
|
| The richer per-run detail — `lastErrorReason`, `consecutiveFailures`, `missingConfig`, and the inserted/deduped/abandoned counts — is **not** returned by either endpoint. It lives in the Redis record `intel-history:ingest-health:<domain>:<resource>:v1`, which the endpoints project from. |
|
|
| A `stale` or `error` here alongside an `ok` collector means canonical data is fine and the history store is the thing to investigate. |
|
|
| ### Example Request |
|
|
| ```bash |
| curl -s https://api.worldmonitor.app/api/seed-health \ |
| -H "Origin: https://worldmonitor.app" | jq . |
| ``` |
|
|
| ## Integration with Monitoring Tools |
|
|
| ### UptimeRobot |
|
|
| Use `/api/health?compact=1` as the public monitor URL. The HTTP status code only distinguishes a total Redis outage from everything else: |
|
|
| - `503` = `REDIS_DOWN` (Redis unreachable — a true hard outage) |
| - `200` = every other state, including `DEGRADED` and `UNHEALTHY` |
|
|
| So an HTTP-status-only monitor catches a full backend outage but **not** degraded/unhealthy data. For those, add a keyword monitor. |
|
|
| Point the keyword monitor at `https://api.worldmonitor.app/api/health?compact=1` and alert when the compact token `"status":"HEALTHY"` (no space after the colon) is **absent** from the response body. Compact mode serializes with no indentation, so this exact token is stable regardless of formatting. |
| |
| > The bare `/api/health` URL is now an operator view and returns `401` without an API key. Public monitoring should always use `?compact=1`. |
| |
| ### Custom Alerting |
| |
| Parse the JSON response to build granular alerts: |
| |
| ```bash |
| # Alert on any critical keys |
| STATUS=$(curl -s "https://api.worldmonitor.app/api/health?compact=1") |
| CRIT=$(echo "$STATUS" | jq '.summary.crit') |
| if [ "$CRIT" -gt 0 ]; then |
| echo "CRITICAL: $CRIT data keys empty" |
| echo "$STATUS" | jq '.problems' |
| fi |
| ``` |
| |
| ## Differences Between Endpoints |
| |
| | Aspect | `/api/health` | `/api/seed-health` | |
| |--------|--------------|-------------------| |
| | **Scope** | Data keys + seed metadata | Seed metadata only | |
| | **Auth** | None (public) | API key or allowed origin | |
| | **Data fetched** | Full Redis values (to count records) | Only `seed-meta:*` keys | |
| | **HTTP 503** | Only `REDIS_DOWN` (DEGRADED/UNHEALTHY return 200) | No (always 200 unless Redis down) | |
| | **Best for** | Uptime monitoring, dashboard health | Debugging seed loop issues | |
| | **Response size** | Larger (~194 keys with record counts) | Smaller (seed-meta domains only) | |
| |