# Status API contract for the reference dashboard

Status: draft, schema version 1. The runtime may change this shape. When it does,
change `assets/js/status-adapter.js` (`normalizeStatus`) and this document together.
No other file reads the response.

## Endpoint

`GET /api/status` on the same origin that serves the dashboard. It is read-only. The
dashboard sends no other requests and has no way to change runtime or network state.

Request: `cache: no-store`, `credentials: same-origin`, `redirect: error`,
`Accept: application/json`. The adapter refuses absolute or protocol-relative URLs.

## Body

```json
{
  "schema_version": 1,
  "mode": "replay",
  "authenticated": false,
  "hardware_qualified": false,
  "supported_profiles": ["state-replay"],
  "decks": [
    {
      "endpoint": 1,
      "generation": 2,
      "tempo_micro_bpm": "126000000",
      "beat_q32": "1378684502016",
      "bar_phase_nano": 250000000,
      "transport": "playing",
      "master": true,
      "on_air": true,
      "fresh": true,
      "provenance": "replay"
    }
  ],
  "qualification": { "native_wifi_four_deck": false },
  "jobs": [],
  "devices": []
}
```

| Field | Type | Adapter rule |
|---|---|---|
| `schema_version` | number | Must be `1`. Anything else shows "Incompatible status" and no deck data. |
| `mode` | `"replay"` or `"idle"` | Anything else shows Unknown and logs a warning. |
| `authenticated` | boolean | Non-boolean shows Unknown. |
| `hardware_qualified` | boolean | `true` is shown as "Reported qualified (not verified here)". |
| `supported_profiles` | string[] | Up to 32 unique strings. |
| `qualification` | object of booleans | `native_wifi_four_deck` is always shown; other keys are listed. Non-boolean shows Unknown. |
| `decks` | array | Up to 64. Sorted by endpoint. The count is not assumed to be four. |
| `jobs`, `devices` | arrays of objects | Shape not fixed yet. The dashboard shows `name`, `label`, `title`, `id` or `kind` as a title and up to six primitive values. |

Deck fields:

| Field | Type | Adapter rule |
|---|---|---|
| `endpoint` | integer 1..2^32-1 | Invalid: the deck is dropped. Duplicates: the later deck is dropped. |
| `generation` | integer 0..2^32-1 | Invalid: shown as unknown. |
| `tempo_micro_bpm` | decimal string, positive i64, or null | Numbers are rejected (not coerced) because they lose precision. Shown as unknown. |
| `beat_q32` | decimal string, signed i64 Q32.32, or null | As above. Negative values (preroll) are allowed. |
| `bar_phase_nano` | integer 0..999 999 999, or null | Out of range: unknown. |
| `transport` | `playing`, `paused`, `stopped`, `unknown` | Anything else: unknown. |
| `master`, `on_air` | boolean or null | Non-boolean: unknown. |
| `fresh` | boolean | Only `true` counts. Never inferred from tempo, transport or any other field. |
| `provenance` | string | Shown verbatim, up to 64 characters. Missing: "unspecified". |

Absent or null means unknown. It is never shown as zero, false or stopped. Values the
adapter could not use are listed under Connection, "Values the dashboard could not use".

## Freshness, staleness and failure

The dashboard measures the age of data from when its request was sent, using a
monotonic clock (`performance.now`).

| Condition | Dashboard state | Decks |
|---|---|---|
| No answer yet | Looking for a runtime | Hidden |
| Answer younger than the stale budget | Connected | Shown; fresh only if the server said `fresh: true` |
| No good answer for longer than the stale budget | Stale | Shown dimmed; none count as fresh |
| No good answer for longer than the disconnect budget, or never answered | Disconnected | Hidden |
| HTTP error, network error, timeout, non-JSON | Recorded as the last problem | Ages toward Stale and Disconnected |
| Valid JSON with the wrong schema | Incompatible status | Hidden |

Budgets: stale after `max(2 s, 2.5 x refresh)`, disconnected after
`max(10 s, 5 x refresh)`. Request timeout is `min(4 s, max(1 s, 2 x refresh))`. Refresh
is 250 ms to 2 s (setting); the host-screen study polls every 2 s at most.

Requests are sequential. If two overlap, a response is applied only when its request
is newer than the last applied one; older answers are counted as "late answers
ignored". Polling stops while the tab is hidden.

## Things the dashboard never does

- Interpolate or extrapolate beat, phase or position between answers.
- Treat a nonzero tempo as fresh or playing.
- Start the simulated demo by itself, or remember it across reloads.
- Send settings to the runtime. Settings live in this browser's `localStorage` under
  `traralink.dashboard.v1`.
- Contact AZ Prep. The optional shortcut is a plain `http`/`https` link.

## Simulated demo

`assets/js/demo-source.js` generates bodies in this schema and hands them to the same
adapter through an injected `fetch`. Every deck, device and job has provenance
`simulated-demo`, and the page shows a striped banner and a blue frame while it runs.
The demo assumes four-beat bars to compute its own bar phase; real data does not.
