# Dashboard and admin API

Source: https://apigenie.pl/docs/reference/server/dashboard
Part of the reference of [apigenie-server](https://apigenie.pl/docs/reference/server.md).
Its other pages: [Responses](https://apigenie.pl/docs/reference/server/responses.md), [Scenarios and extensions](https://apigenie.pl/docs/reference/server/scenarios.md), [Scripts](https://apigenie.pl/docs/reference/server/scripts.md), [Document store](https://apigenie.pl/docs/reference/server/store.md), [Several services](https://apigenie.pl/docs/reference/server/services.md), [Validation and security](https://apigenie.pl/docs/reference/server/validation.md), [Contract testing (validating proxy)](https://apigenie.pl/docs/reference/server/contract-testing.md), [Session recording and replay](https://apigenie.pl/docs/reference/server/recording.md), [AsyncAPI](https://apigenie.pl/docs/reference/server/asyncapi.md), [Protobuf / gRPC](https://apigenie.pl/docs/reference/server/grpc.md), [GraphQL](https://apigenie.pl/docs/reference/server/graphql.md), [Configuration](https://apigenie.pl/docs/reference/server/configuration.md), [TLS](https://apigenie.pl/docs/reference/server/tls.md).

The dashboard shows the server's log and state in a browser; the admin API behind it is what tests and scripts use to switch scenarios, add stubs and read the log.

## Dashboard

`http://127.0.0.1:4010/__apigenie/ui` is a page served by the server itself, for a browser on the
same machine: it follows the [admin API](#admin-api)'s rules (local clients only unless
`admin.allow_remote`, on `admin.port` when the admin API has a port of its own, off with
`--admin off`). The header on every screen names the documents and how they answer (mock, proxy,
generated), where they listen, the seed, the uptime, the requests and violations so far, and has
Reload and Stop; with [several services](/docs/reference/server/services) a switcher narrows every screen to
one.

| Screen | What it shows and does |
| --- | --- |
| Traffic | Every exchange as it happens (the [event stream](#the-event-stream)), with filters; an exchange's request and answer (headers and bodies, captured while the page is open) and its validation problems with where they point in the document. Copy as curl, Replay, Save as stub (`POST /stubs`), Add to `.http` (a file built up in the browser, to copy or download), Copy as YAML (the answer as an `operations` entry of the configuration). Record session and Save session: a [session recording](/docs/reference/server/recording#session-recording) of what happens from here, written where the server runs. |
| Operations | The operations by tag, with their hits, their last status and what answered them last (an example, a generated body, a scenario, a proxy). For one operation: a fixed answer (a declared status, one of its examples, a generated body, a dropped connection), a latency and whether its requests are validated, saved as an always-on [override](/docs/reference/server/scenarios) (`PUT /overrides`) until the server stops, with the `Prefer` header (and `__` parameters) that do the same for one request. |
| Scenarios and network | The scenarios of the configuration, the document and the admin API, switched on and off, with their rules and hits; a builder for a new one (by operation, tag, method and path, header or query parameter; status, example, latency, probability); the network simulation of every response (fixed, uniform, normal or log-normal latency, jitter, dropped connections, bandwidth, the seed) with the response times of the last requests. |
| Contract | Whether the server would pass `--fail-on` and `--min-coverage` if it stopped now, the coverage, the violations by check (and the exchanges they were seen in), the operations never called, the report as JSON or JUnit to download, and Start over (`DELETE /contract`). |
| Data | The [document store](/docs/reference/server/store)'s collections with their documents, Reset to seed, snapshots to save and restore; and `ctx.state`, the values the scripts keep, to edit or reset. |
| Channels | For an [AsyncAPI](/docs/reference/server/asyncapi) document only (the screen is listed when a served document has channels): the channels by WebSocket, MQTT and HTTP, with their operations, the clients connected and the messages counted (`GET /asyncapi/channels`); for a channel, its messages in and out as the event stream brings them (with an exchange's payload and validation problems, and Open in Traffic), its clients (`GET /asyncapi/clients`, followed through the `ws-open`, `ws-close`, `mqtt-subscribe` and `mqtt-unsubscribe` events), and Publish: a message sent as the application would (`POST /asyncapi/publish`), to every client of the channel or to one, with a payload started from the document's examples or left to the server; a payload the message's schema refuses shows the violations and goes nowhere, until Send anyway; Copy as curl. |

Every panel copies its settings as the configuration's YAML, or as `x-apigenie-mock` for the
document, so what was tried in the dashboard is kept in a file (Channels changes no setting: it
copies the publish as curl). The page follows the system's light or dark theme, or the one chosen
in the header.

The page is built from the web components package (`src/app_web`, the `apg-server-*`
elements) and embedded in the binary; nothing is fetched from the network. The design is ADR-167.

![The Traffic screen of the dashboard](/server-dashboard-light.png)

## Admin API

On by default for local clients only (`admin.allow_remote` opens it, `admin.port` moves it to a
port of its own). The `/db` endpoints work on the namespace of an `X-Apigenie-Test` header (or a
`?test=` parameter), the default one without it:

| Endpoint | Description |
| --- | --- |
| `GET /__apigenie` | Status: version, `url` (where clients reach the server), document (with its proxies and stubs; an AsyncAPI document's `mqttProxy` and `mqttClients`), seed, uptime (`uptimeSeconds`, `startedAt`), requests (the clients', not the admin API's), active scenarios, simulation, `logSize`. |
| `GET /__apigenie/ui/` | The [dashboard](#dashboard): the page and its files (`GET /__apigenie/ui` redirects to it). |
| `GET /__apigenie/routes` | The operations served, with their `hits`, `lastStatus`, `lastSource` (what answered last) and `lastAt`; an OpenAPI operation's `tags`, `deprecated` and `responses` (each status with its content types and the names of its examples). |
| `DELETE /__apigenie/hits` | Start the hits of routes and scenarios over (a reload keeps them). |
| `GET /__apigenie/services` | The services: name, mount, URL, document (see [several services](/docs/reference/server/services)). |
| `GET /__apigenie/scenarios`, `PUT` / `DELETE /__apigenie/scenarios/{name}` | List (with `hits`, `lastStatus`, `lastSource` and `lastAt`, and the rules as `ruleList` next to their count `rules`), add or remove scenarios. |
| `POST /__apigenie/scenarios/{name}/activate`, `/deactivate` | Switch a scenario on or off. |
| `GET`, `PUT /__apigenie/overrides` | The always-on rules, whole: what `GET` answers, `PUT` takes back. |
| `GET`, `PUT /__apigenie/simulation` | The network simulation of every response. |
| `POST /__apigenie/seed` | Reseed: `{"seed": 42}`. |
| `GET`, `DELETE /__apigenie/log?limit=&after=&level=` | The recent events, with headers and bodies when they were captured (see [log entries](#log-entries)). |
| `GET /__apigenie/events?type=&level=&service=&after=` | The events as they happen, as Server-Sent Events (see [the event stream](#the-event-stream)). |
| `POST /__apigenie/stubs` | A stub file of a logged exchange: `{"id": 42, "dir": "stubs", "name": "rex"}` (see [stubs from the log](#stubs-from-the-log)). |
| `GET /__apigenie/record` | The [session recording](/docs/reference/server/recording#session-recording): `recording`, and the session's `dir`, `name`, `started`, `exchanges` while one is on; `test`, the mark. |
| `POST /__apigenie/record/start`, `/stop` | Record a session in `{"dir": "sessions/run-7", "name": "run 7"}` (`dir` defaults to `record.dir`), or stop it and write `session.yaml`. |
| `POST /__apigenie/record/mark` | `{"test": "checkout-happy"}`: the test of the exchanges to come that carry no `X-Apigenie-Test` of their own; `{}` clears it. |
| `GET /__apigenie/replay` | The [session replay](/docs/reference/server/recording#session-replay)'s report so far: `exchanges`, `replayed`, `neverReplayed`, `unmatched` (with their tests and closest recordings), `passed`, and `tests`, the exchanges each test used since its reset. |
| `POST /__apigenie/replay/reset` | Restart a test's counters: `{"test": "checkout"}`, or the `X-Apigenie-Test` header, or `?test=`; without one, every test's. 204. |
| `POST /__apigenie/reload`, `POST /__apigenie/shutdown` | Reload, or stop gracefully. |
| `GET /__apigenie/spec` | The served document, bundled (converted to OpenAPI 3.2 for a Swagger 2.0 document; a `.proto` file with its imports, or a GraphQL schema, as text). |
| `GET`, `PUT`, `DELETE /__apigenie/state/{key}` | Values kept between requests. |
| `GET /__apigenie/db` | The [document store](/docs/reference/server/store): its file, namespaces, snapshots and collections (documents, seeds, id, and the schema and operations of a resource's collection). |
| `GET /__apigenie/db/{name}?limit=&offset=` | A collection's documents (100 by default) and `total`. |
| `PUT`, `DELETE /__apigenie/db/{name}` | Replace every document of a collection (a JSON array; all or nothing: 422 when the schema refuses one, 409 for an id used twice), or remove them. |
| `POST /__apigenie/db/reset` | Back to the seeds: with `X-Apigenie-Test`, that namespace; without it, the default one, and the tests' namespaces go. `{"collections": ["notes"]}` resets some. |
| `GET /__apigenie/db/snapshots`, `PUT`, `DELETE /__apigenie/db/snapshots/{name}` | List, take (or replace) or remove a snapshot of the documents. |
| `POST /__apigenie/db/snapshots/{name}/restore` | Put a snapshot's documents back. |
| `POST /__apigenie/db/save` | Write the store to `--db-file` now (501 without one). |
| `DELETE /__apigenie/db/namespaces/{test}` | Forget a test's namespace. |
| `GET`, `DELETE /__apigenie/contract` | The contract report (coverage and violations), or start it over. |
| `GET /__apigenie/contract.json`, `GET /__apigenie/contract.xml` | The contract report as `--report` (JSON) and `--report-junit` (JUnit XML) would write it now, to save. |
| `GET /__apigenie/contract/violations?operation=&check=&limit=` | The violations seen, by operation (id or `GET /pets/{id}`; GraphQL: the operation's name or a root field such as `Query.book`; AsyncAPI: the operation's key; gRPC: `package.Service/Method` or its path) and check. |
| `POST /__apigenie/contract/schema-check` | GraphQL proxy: introspect the server and compare its schema now (202; the result is logged and reported). |
| `GET /__apigenie/asyncapi/channels` | AsyncAPI: the channels, their operations and messages (with the document's examples), the clients connected, and the messages counted (see [AsyncAPI channels](#asyncapi-channels-from-the-admin-api)). |
| `GET /__apigenie/asyncapi/clients` | AsyncAPI: the WebSocket clients and MQTT subscriptions of the channels. |
| `POST /__apigenie/asyncapi/publish` | AsyncAPI: send a message to a channel's clients, or to one of them: `{"channel": "rooms/42", "payload": {"text": "hi"}}`, `{"channel": "room", "client": 7, ...}`. |
| `GET /__apigenie/grpc/services` | gRPC: the services and their methods. |
| `GET`, `POST`, `DELETE /__apigenie/grpc/stubs` | gRPC: list, add (one stub or a list) or remove the stubs added at run time (`?scope=all`: every stub). |
| `GET`, `PUT`, `DELETE /__apigenie/grpc/stubs/{name}` | gRPC: one stub. |
| `POST /__apigenie/grpc/stubs/reset`, `GET /__apigenie/grpc/stubs/schema` | gRPC: back to the stub files, and the stubs' JSON Schema. |
| `GET`, `DELETE /__apigenie/grpc/history?method=&limit=&after=&q=` | gRPC: the calls received, newest last. |
| `POST /__apigenie/grpc/verify` | gRPC: count the calls that match `{method, request, metadata, stub, code}`. |

A server of [several services](/docs/reference/server/services) names the service of each item, and takes
`?service=NAME` on every endpoint.

### The event stream

`GET /__apigenie/events` keeps the connection open and writes each event of the log as it happens,
as [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html): `id:` is
the log entry's id, `event:` its kind, and `data:` the same JSON object as a `/log` entry. A
dashboard reads it with `EventSource`; a script with `curl -N`:

```bash
curl -N 'http://127.0.0.1:4010/__apigenie/events?type=request,violation'
```

```text
: connected

id: 12
event: request
data: {"event":"request","id":12,"time":"2026-10-12T09:00:00.000Z","level":"info","method":"POST","path":"/pets","operationId":"addPet","status":201,...}

id: 13
event: request
data: {"event":"request","id":13,...,"status":422,"violations":[...]}

id: 13
event: violation
data: {"event":"request","id":13,...,"status":422,"violations":[...]}
```

| Event | When |
| --- | --- |
| `request` | An exchange, request and answer together (there is no separate response event). |
| `violation` | After an event with violations: the same id and data. |
| `ws-open`, `ws-in`, `ws-out`, `ws-close`, `mqtt-*`, `grpc`, `grpc-in`, `grpc-out` | WebSocket, MQTT and gRPC traffic, as in the log. |
| `scenario` | A scenario was added, replaced, removed, activated or deactivated through the admin API (`message` says which). |
| `reload` | The documents were reloaded (`POST /reload` or `--watch`). |
| `contract-updated` | The contract report's coverage changed (at most once a second), or it started over (`message: "reset"`, after `DELETE /contract` or a reload). |
| `db` | A write to the [document store](/docs/reference/server/store): the collection, what changed, who wrote. |
| `admin`, `script`, `server` | The admin API's own exchanges, the scripts' logs, the server's errors. |

`?type=` keeps the named events, `?level=` those at a level and above (default `debug`),
`?service=` one service's. `?after=ID`, or the `Last-Event-ID` header a reconnecting `EventSource`
sends, first replays the log's later entries, so a client that reconnects misses nothing the log
still holds. A comment every 15 seconds keeps the connection alive. A client that falls more than
1 MiB behind is disconnected. The stream follows the admin API's rules: local clients only unless
`admin.allow_remote`, on `admin.port` when the admin API has a port of its own.

### Log entries

While at least one event stream is open, the server captures the headers and bodies of every
exchange, as `log.headers: true` and `log.bodies: true` do. `/log` entries and stream events
then carry `headers` and `responseHeaders` (arrays of `[name, value]`), `requestBody` and
`responseBody`, and `captured: {"headers": true, "bodies": true}`. A body that isn't UTF-8 is in
Base64 with `requestBodyEncoding` (or `responseBodyEncoding`) `"base64"`. A body is kept up to
`log.body_limit` bytes (4096 by default); a longer one is cut and marked `requestBodyTruncated` or
`responseBodyTruncated`, and `requestSize` and `responseSize` keep the whole sizes.

### Stubs from the log

`POST /__apigenie/stubs` makes a [stub file](/docs/reference/server/contract-testing) of a logged exchange of an OpenAPI document,
as `--record-dir` writes them: the client's headers without the connection's, the hidden headers
of `proxy.redact` as `[redacted]`, and the query without the mock's own `__` parameters.

```bash
curl -X POST http://127.0.0.1:4010/__apigenie/stubs -d '{"id": 12, "dir": "stubs", "name": "new-pet"}'
```

With `dir`, or else the service's `proxy.record_dir`, the stub is written to a new file there,
`<operationId>-N.yaml`, and the answer (201) has the `file` and the `stub`. Without either the
answer (200) is the file's content, `{"stubs": [...]}`. The entry must still be in the log, and
must have been captured whole (an open event stream, or `log.headers` and `log.bodies`, and a
`log.body_limit` above its bodies' sizes): else 404 or 409. The running mock answers with the new
stub after `POST /__apigenie/reload` when `dir` is one of its `stubs` directories.

### AsyncAPI channels from the admin API

`GET /__apigenie/asyncapi/channels` lists the channels: `key`, `address`, `protocols`, `paths`,
`messages` (their names) and `messageList` (each message's `name`, `contentType`, `summary` and
`examples`, the document's, each with its `payload` and `headers`: a starting point for a payload
to publish), `operations` (with `qos` and `retain` for MQTT), the `clients` connected now and the
contract report's `connections`, `messagesIn`, `messagesOut` and `unmatched` counts.
`GET /__apigenie/asyncapi/clients` lists the WebSocket clients and MQTT subscriptions: `id`,
`protocol`, `remote`, `channel`, `address`, `route`, `path` (the WebSocket path, or the MQTT
filter), `clientId` (MQTT), `connectedAt` and `messagesOut`.

`POST /__apigenie/asyncapi/publish` sends a message to the clients of a channel, as the schedule
sends one: scripts see it, and each delivery is logged as `ws-out` or `mqtt-out` with
`source: "admin"`.

```bash
curl -X POST http://127.0.0.1:4010/__apigenie/asyncapi/publish \
  -d '{"channel": "rooms/42", "message": "notice", "payload": {"text": "maintenance at noon"}}'
```

```json
{"channel": "room", "address": "rooms/42", "operation": "sendNotices", "message": "notice", "source": "admin",
 "contentType": "application/json", "payload": "{\"text\":\"maintenance at noon\"}", "delivered": 1,
 "webSocket": 1, "mqtt": 0, "clients": [7]}
```

With a [Kafka tap](/docs/reference/server/asyncapi#contract-testing-of-kafka-channels), a Kafka channel's message is produced to
the cluster instead. The record's `key`, `headers` (an object of strings) and `partition` may be
given; without them the key and headers are made from the message's Kafka binding and headers, and
the partition is the key's. The record is checked as the tap checks records (422 with the
`violations`, nothing produced, unless `"validate": false`), produced, and answered with where it
went; a cluster that can't be reached or refuses it answers 502. The tap then reads it like any
other record.

```bash
curl -X POST http://127.0.0.1:4010/__apigenie/asyncapi/publish \
  -d '{"channel": "orders", "payload": {"sku": "A-1"}, "key": "cust-1", "headers": {"correlationId": "c-1"}}'
```

```json
{"channel": "orders", "operation": "placeOrder", "message": "order", "source": "admin",
 "contentType": "application/json", "payload": "{\"sku\":\"A-1\"}",
 "kafka": {"upstream": "kafka://localhost:9092", "topic": "shop.orders", "partition": 1, "offset": 42,
 "key": "cust-1", "headers": {"correlationId": "c-1"}}}
```

With an [AMQP tap](/docs/reference/server/asyncapi#contract-testing-of-amqp-channels), an AMQP channel's message is published to
the broker: a queue channel's through the default exchange, any other to its exchange with
`routingKey` (else the address given, else the address with its parameters filled). Its
`properties` (`contentType`, `contentEncoding`, `deliveryMode`, `priority`, `correlationId`,
`replyTo`, `expiration`, `messageId`, `timestamp`, `type`, `userId`, `appId`) and `headers` may be
given; without them they come from the message's and the operation's AMQP bindings and the message's
headers. The message is checked (422), published mandatory with publisher confirms, and answered
with `{"amqp": {"upstream", "exchange", "routingKey", "headers"}}`; a message no queue takes answers
409 `asyncapi-unroutable`.

`channel` is a channel's key (every client of the channel gets the message) or an address with its
parameters (that address's clients only); `client`, an `id` of `/asyncapi/clients`, narrows it to
that one client. `message` names one of the channel's messages; without it, the first message of
its first `send` operation is sent. `payload` is the message as JSON, `raw` as text, and without
either one is made as for the schedule. A payload that doesn't match the message's schema answers
422 with the `violations` and is sent to nobody, unless `"validate": false`. Unknown channels and
messages answer 404, as does a `client` that isn't connected to the channel
(`asyncapi-unknown-client`). An admin publish isn't retained by the MQTT
broker. Only clients the mock matched to a channel get it. Clients of a validating proxy are counted
in the status, not listed. A server of several services lists every AsyncAPI service's channels and
clients, and needs `?service=` to publish when several services serve AsyncAPI.
