# 14 · Dashboard observability

The reactive surface. An invocation submitted from the CLI or SDK should
appear in the dashboard's Invocations view, transition through states,
and report its outcome — with no page refresh.

## Exercises

- The dashboard's long-poll loop against `GET /v1/admin/invocations`.
- Cursor-based change detection: the response carries a `cursor` and the
  next request holds until the server's cursor moves past it.
- CORS for cross-origin browser fetches (Netlify origin → Heroku control
  plane).

## Requires

- The control plane up, and the dashboard either running locally or the
  hosted one at `demo.lakeshore.dreamlake.ai`.
- At least one daemon registered and draining the queue you submit to,
  or the row will sit in `queued` forever.
- Admin auth — `/v1/admin/*` is gated on `LAKESHORE_ADMIN_TOKEN`, or
  passes freely in dev open mode when that variable is unset.

> **Warning:** `GET /v1/admin/invocations` resolves the **default** namespace and reads
> its invocations. There is no namespace query parameter. Submitting into
> some other namespace produces a row the dashboard's Invocations view will
> not show.

## Verifies

Submit → live state transitions visible in the browser without a manual
refresh.

## Run

In one terminal, submit:

```bash
export LAKESHORE_WORKSPACE=~/lakeshore-workspace
export LAKESHORE_URL=http://localhost:8080

uv run --project $LAKESHORE_WORKSPACE/lakeshore-py \
  $LAKESHORE_WORKSPACE/lakeshore-examples/09-hello-queue-udf/hello_udf.py
```

In the browser, open the dashboard and go to **Invocations**.

## Expected output

A new row appears, moves `queued → running → succeeded`, and settles
with a result preview. The delay before it appears is bounded by the
long-poll window, not by a fixed refresh interval.

## The long-poll contract

| Query param | Default | Cap |
| ----------- | ------- | --- |
| `cursor` | `0` | — |
| `timeout_s` | 30 | 60 |
| `limit` | 100 | 500 |

The request blocks until the server's `adminCursor` passes the supplied
`cursor`, or the timeout expires. The response is
`{ invocations, cursor }`; feed that `cursor` back into the next
request. Rows are ordered newest-submitted first, and each carries `id`,
`queueName`, `functionQualname`, `state`, `workerId`, the three
timestamps, and `progress`.

`queueName` is read off `runConfig.queue` — Phase A.5 removed
`Invocation.queueId`, so there is no join and no foreign key. Rows with
nothing there render as `default`.

## If it fails

| Symptom | Likely cause |
| ------- | ------------ |
| The row never appears | Submitted into a non-default namespace — see the callout above — or into a namespace the dashboard account is not pointed at. |
| The row appears but stays `queued` | Nothing is draining the queue. Check `lakeshore daemon list` and the daemon's `queues` column. |
| 401 on `/v1/admin/invocations` | The control plane has `LAKESHORE_ADMIN_TOKEN` set and the dashboard is not sending it. |
| CORS error in the browser console | The control plane registers `@fastify/cors` with `origin: true` (reflect any origin), so a CORS failure usually means the request never reached it — check the URL and TLS first. |
| The row appears but never updates | The long-poll hung. Look for the `/v1/admin/invocations?cursor=…` request in the Network panel; a request that returns without the cursor advancing is the tell. |

## Status

Manual.

## Next

→ [15 · Token lifecycle](/cli/happy-paths/15-token-lifecycle.md)
