etherpad-lite/admin/README.md
John McLear 10558ed115
fix(admin/pads): apply filter chip server-side, before pagination (#7798)
* fix(admin/pads): apply filter chip server-side, before pagination

Before: PadPage's filter chip (`active`/`recent`/`empty`/`stale`) ran
on the client AFTER the 12-row page slice was already on screen. On a
deployment with hundreds of pads it produced obviously wrong results
— click "empty pads" on page 1 with 100 empties and only the 0–12
empties within the current page passed the filter. thm reported this
on a 3.1.0 deployment.

Move the filter into `PadSearchQuery` so the `/settings` socket can
apply it before slicing:

  1. pattern filter on names (cheap)
  2. hydrate metadata for the matching pad universe iff a non-`all`
     filter is set or a non-`padName` sort is requested
  3. apply filter chip on the hydrated set
  4. sort + slice → `total` reflects the filtered universe so the
     pagination footer makes sense

The original handler also had a 4-way `if/else if` that duplicated the
hydrate-and-sort loop per `sortBy`. Folded those into one pipeline
with a single comparator switch.

Client side, `PadPage.tsx`:
- drop the client-side `filteredResults` filter (server already filters)
- chip click writes `filter` into searchParams (debounced refetch) and
  resets `currentPage` to 0
- older clients that don't send `filter` keep working — server defaults
  to `all`

Stats cards (totalUsers/activeCount/emptyCount) still count the visible
page only — that's a pre-existing UI limitation tracked separately.

Closes the regression thm reported.

Test plan
- `tsc --noEmit` clean (server + admin)
- New backend spec `padLoadFilter.ts` exercises filter:empty with
  small `limit` to lock in the bug-fix, plus all/active/omitted cases
- `5 passing` locally on Node 25

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* address Qodo review on #7798

1. Functional setState updaters for every searchParams mutation
   (Qodo bug 1). The debounced pattern handler captured a render-time
   snapshot of searchParams; a faster chip click or sort change in
   between would be silently reverted when the debounce fired. Now
   every mutation merges against the latest state.

2. Concurrency-limited hydration (Qodo bug 3). The earlier draft
   issued Promise.all over the full candidate set, fanning out to
   thousands of in-flight padManager.getPad() reads on busy
   deployments. New mapWithConcurrency() caps concurrent loads at 16
   — empirically enough to saturate a single ueberDB driver without
   pushing the event loop into back-pressure.

3. Test cleanup deletes the injected test-admin (Qodo bug 4). The
   original snapshot/restore pattern saved `settings.users` by
   reference; reassigning the same reference in after() left the
   inserted key in place and could leak into later backend specs.

4. Document the new `filter` field on the `padLoad` socket query in
   admin/README.md (Qodo rule violation 2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:43:36 +01:00

88 lines
4.4 KiB
Markdown

# 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