> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirador.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Dashboards

> Read dashboards, render their widgets, and manage them as code with conditional writes

Dashboards are readable and **writable** through the REST API. Reads are always safe; writes are full-document replacements guarded by ETags, so concurrent writers can't silently clobber each other — and so a dashboard can live in version control and be `PUT` on deploy.

## Reading dashboards

### `GET /v1/dashboards`

Returns dashboard summaries (without widget bodies) plus a cache-only collection `ETag`. Re-send it as `If-None-Match` to receive `304 Not Modified` when nothing changed. This collection ETag **cannot authorize a write**.

### `GET /v1/dashboards/{slug}`

Returns the complete dashboard as a bare object; its opaque revision **ETag arrives as an HTTP header** — keep it for conditional writes.

```bash theme={null}
# Include headers so the ETag is visible.
curl -sS --fail-with-body -i \
  -H "Authorization: Bearer $MIRADOR_SERVER_KEY" \
  https://api.mirador.org/v1/dashboards/example-slug
```

<Note>
  GET-by-slug does not support conditional reads — sending its ETag as
  `If-None-Match` still returns `200` with the full body. Only the collection
  returns `304`.
</Note>

### Rendering a stored widget yourself

Run `timeseries` widgets through [`/v1/metrics/query_range`](/api/metrics); run `stat`, `pie`, and `table` through [`/v1/metrics/query`](/api/metrics) evaluated at the selected range end (put whole-window aggregation such as `sum(increase(m[1h]))` inside the PromQL). `markdown` widgets need no query.

## Writing dashboards

<Warning>
  A server key can create, replace, and delete **every dashboard in its
  project**, and writes are **not attributed** — the stored dashboard records no
  actor, and an overwritten document is unrecoverable. `GET` the current body
  and keep it before any replace or delete. If you're wiring up an automation or
  agent, don't let it mutate dashboards unless that's explicitly its job.
</Warning>

### The document

The **slug** is the immutable identity in the URL — 1–63 lowercase letters/digits with internal hyphens — and is never part of the body. The exact body contract is `DashboardInput` in the [OpenAPI spec](https://api.mirador.org/openapi.yaml); the essentials:

* `title` — required, 1–200 characters. `description` — optional, up to 2,000.
* `default_time_window` — required; one of `1m, 5m, 15m, 1h, 4h, 12h, 24h, 7d, 30d`.
* `widgets` — required (may be `[]`), up to 24. IDs are unique, `^[A-Za-z0-9_-]{1,64}$`.
* Widget `type` — `timeseries`, `stat`, `pie`, `table`, or `markdown`.
* `grid` — `{x, y, w, h}` on 12 columns; `x + w <= 12`.
* Queries are `{ "promql": "..." }` — timeseries/table take 1–4, stat/pie exactly 1, markdown none. Every query must evaluate to an **instant vector or scalar** — a bare range selector is rejected at write time, so wrap it as `rate(...)` or `sum(increase(...))`.
* Only timeseries supports `style` (`line, area, bar, stacked_bar`) and `time_interval`. **Leave `time_interval` unset** — the bucket then auto-sizes to whatever window the viewer selects. A fixed interval must satisfy `window / time_interval <= 600` at *every* selectable window, or the widget renders as unavailable at wider windows.
* A markdown widget provides non-empty `markdown` (up to 16,384 UTF-8 bytes). Any widget may override `time_window`.
* Grouping and aggregation belong **in the PromQL** — there is no `group_by`, `unit`, or `color` field. Unknown JSON fields are **rejected** with `400`, not ignored.
* The body is capped at 1 MiB and requires `Content-Type: application/json`.

<Warning>
  `PUT` is **full replacement, not merge or patch**. Optional fields omitted
  from an update are cleared. Always start from the current document.
</Warning>

### The conditional-write contract

Every mutation requires **exactly one** conditional header:

| Intent  | Header               | Success                           | Failure                                   |
| ------- | -------------------- | --------------------------------- | ----------------------------------------- |
| Create  | `If-None-Match: *`   | `201` if the slug is free         | `412` if it already exists                |
| Replace | `If-Match: "<etag>"` | `200` if that revision is current | `412` if stale — re-`GET`, reapply, retry |
| Delete  | `If-Match: "<etag>"` | `204`, no body                    | `412` if stale                            |

Supplying both headers is `400`; supplying neither is `428`. A malformed header **value** (unquoted ETag, `W/"..."`, `If-Match: *`, a list of ETags) is `400`, not `412` — a client bug to fix, not a race to retry. Replay the ETag **verbatim, including quotes**; never synthesize one or borrow it from another slug or the collection list. Conditional writes never return `404` — a well-formed `If-Match` on a vanished slug is `412`.

Reapplying an identical body is a `200` no-op with an unchanged ETag.

### Create

```bash theme={null}
# dashboard.json holds the complete desired state (see "The document" above)
curl -sS --fail-with-body -D dashboard.headers -X PUT \
  -H "Authorization: Bearer $MIRADOR_SERVER_KEY" \
  -H 'Content-Type: application/json' \
  -H 'If-None-Match: *' \
  --data-binary @dashboard.json \
  https://api.mirador.org/v1/dashboards/agent-api-health

ETAG=$(grep -i '^etag:' dashboard.headers | tail -1 | tr -d '\r' | cut -d' ' -f2)
```

### Replace or delete

`GET` the latest document and ETag first, apply your changes to the **complete** document, then `PUT`/`DELETE` with `If-Match`:

```bash theme={null}
curl -sS --fail-with-body -D dashboard.headers -o dashboard.current.json \
  -H "Authorization: Bearer $MIRADOR_SERVER_KEY" \
  https://api.mirador.org/v1/dashboards/agent-api-health
ETAG=$(grep -i '^etag:' dashboard.headers | tail -1 | tr -d '\r' | cut -d' ' -f2)

# Replace:
curl -sS --fail-with-body -X PUT \
  -H "Authorization: Bearer $MIRADOR_SERVER_KEY" \
  -H 'Content-Type: application/json' \
  -H "If-Match: ${ETAG}" \
  --data-binary @dashboard.json \
  https://api.mirador.org/v1/dashboards/agent-api-health

# ...or delete:
curl -sS --fail-with-body -X DELETE \
  -H "Authorization: Bearer $MIRADOR_SERVER_KEY" \
  -H "If-Match: ${ETAG}" \
  https://api.mirador.org/v1/dashboards/agent-api-health
```

On `412`, another writer won: `GET` again, reapply your intent to the fresh document, and retry — never blindly resend a stale body. On a cleanup `412` whose follow-up `GET` is `404`, the dashboard is already gone — stop; don't re-create it.

## Next Steps

<CardGroup cols={2}>
  <Card title="Metrics" icon="chart-line" href="/api/metrics">
    The PromQL that widget queries evaluate
  </Card>

  <Card title="Metric alerts" icon="bell" href="/api/alerts">
    The same slug + ETag contract, for alerting
  </Card>
</CardGroup>
