# Admin UI

Vite + React 19 single-page app served at `/admin`. Talks to the backend over
socket.io for the existing settings / plugins / pads pages, and (when
endpoints are added to the OpenAPI spec) over a typed REST client.

## Scripts

| Script               | What it does                                             |
| -------------------- | -------------------------------------------------------- |
| `pnpm dev`           | `gen:api` + Vite dev server (expects backend on :9001).  |
| `pnpm gen:api`       | Regenerates `src/api/{schema.d.ts,version.ts}` from the OpenAPI spec. |
| `pnpm build`         | `gen:api` + `tsc` + `vite build`.                        |
| `pnpm build-copy`    | Same, but writes into `../src/templates/admin`.          |
| `pnpm test`          | `gen:api` + smoke tests for the API client wiring.       |
| `pnpm lint`          | ESLint.                                                  |

## Typed API client

The admin uses [`openapi-typescript`] to generate types from
`src/node/hooks/express/openapi.ts`, [`openapi-fetch`] for typed requests, and
[`openapi-react-query`] for TanStack Query bindings.

[`openapi-typescript`]: https://github.com/openapi-ts/openapi-typescript
[`openapi-fetch`]: https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-fetch
[`openapi-react-query`]: https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-react-query

### Generated files

`admin/src/api/schema.d.ts` and `admin/src/api/version.ts` are generated by
`gen:api` and gitignored — never commit them. They are produced by:

```sh
pnpm --filter admin gen:api
```

`admin/scripts/gen-api.mjs` loads `src/node/hooks/express/openapi.ts`, calls
`generateDefinitionForVersion` for the latest API version, pipes the JSON
through `openapi-typescript` to produce `schema.d.ts`, and emits a runtime
constant `LATEST_API_VERSION` (read from `info.version` in the spec) to
`version.ts` so `client.ts` can build the right `/api/<version>/` baseUrl.

`gen:api` runs as the first step of `dev`, `build`, `build-copy`, and
`test`, so a fresh checkout produces the generated files automatically when
any of those scripts is invoked. After modifying any of the following, the
next `pnpm <dev|build|test>` will refresh the generated files; you can also
run `gen:api` directly:

- `src/node/hooks/express/openapi.ts`
- `src/node/handler/APIHandler.ts` (changes to `latestApiVersion`)
- the resource definitions referenced by `openapi.ts`

### Using the client

```tsx
import { $api } from './api/client';

const SettingsPanel = () => {
  const { data } = $api.useQuery('get', '/admin/settings'); // example
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
};
```

The admin endpoints are not yet present in the OpenAPI spec — this client is
in place to support upcoming work (see issue #7638 follow-up). For now, it is
exercised only by the smoke test.

## Socket.io: `padLoad` query shape

The admin `/settings` namespace's `padLoad` event accepts a `PadSearchQuery`
defined in `src/node/types/PadSearchQuery.ts`:

| field        | type                                                              | required | notes |
| ------------ | ----------------------------------------------------------------- | -------- | ----- |
| `pattern`    | `string`                                                          | yes      | Substring match on pad name. |
| `offset`     | `number`                                                          | yes      | Pagination start, in items. Clamped server-side. |
| `limit`      | `number`                                                          | yes      | Page size. Capped at 12. |
| `ascending`  | `boolean`                                                         | yes      | Sort direction. |
| `sortBy`     | `"padName" \| "lastEdited" \| "userCount" \| "revisionNumber"`    | yes      | Column to sort by. |
| `filter`     | `"all" \| "active" \| "recent" \| "empty" \| "stale"` *(opt.)*    | no       | Filter chip; defaults to `"all"`. Applied **before** pagination so `total` and the page slice both reflect the filtered universe. Older clients that omit the field get the unchanged `"all"` behaviour. |

Filter semantics — applied after pattern matching, before sort + slice:

- `active`: `userCount > 0`
- `recent`: edited within the last 7 days
- `empty`: `revisionNumber === 0`
- `stale`: not edited in the last 365 days
- `all` / missing: no further filtering
