* feat(admin): explain env-var substitution in /settings, surface auth errors (#7819) Three small, env-var-only UX improvements driven by issue #7819, where a Docker operator saved an ep_oauth block in the admin /settings raw view and reported it "disappeared" — but the underlying confusion was that settings.json on disk is a *template*, not the effective config. None of these changes is visible to installs that don't use ${VAR} placeholders. * Banner above the editor explaining the template/env-substitution model, only rendered when the loaded file contains a ${VAR} placeholder. Tells the operator that the file is not env-substituted in place and that the Effective tab shows the live values. * Effective tab in the mode toggle, read-only, also gated on ${VAR}. The backend was already emitting redacted runtime settings as `resolved` alongside every `load`; the SPA now exposes them so an operator can verify what Etherpad is actually using. * admin_auth_error event from the /settings socket handler. The handler previously silently returned when the connecting session wasn't admin, which made misrouted Traefik+SSO auth look like "save did nothing" with no error path in the UI. Emit a dedicated event before dropping the socket so the SPA can show a clear toast. Tests: - src/tests/backend/specs/admin/adminSettingsAuthError.ts — new spec for the auth_error/disconnect contract. - src/tests/frontend-new/admin-spec/adminsettings.spec.ts — new Playwright test asserting the banner + Effective tab only appear after a ${VAR} is added to settings.json, and that the Effective view is read-only + shows [REDACTED] for secrets. No behaviour change for installs without ${VAR} placeholders — banner, Effective tab, and auth-error contract are all the same as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): drop fragile pre-condition + add reconnect-loop guard (#7819) CI's admin-UI workflow seeds settings.json by copying settings.json.template verbatim, which contains ~30 \${VAR} placeholders. The new Playwright test asserted "banner not present before adding placeholder" — true on a fresh dev machine, false in CI. Drop that assertion: the negative path is covered by the SettingsPage ENV_VAR_PATTERN regex itself; what matters at the UI level is the positive path (banner + Effective tab render correctly when placeholders are present), which this test still exercises. Also: the server's admin_auth_error path calls socket.disconnect(), which the SPA's existing disconnect handler interprets as "io server disconnect" and immediately reconnects — creating a reject/reconnect loop. Track an authErrored flag and suppress the reconnect once an auth_error has been received. Reset on successful connect, so a legitimate re-auth path still works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| public | ||
| scripts | ||
| src | ||
| .eslintrc.cjs | ||
| .gitignore | ||
| index.html | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
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.
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:
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.tssrc/node/handler/APIHandler.ts(changes tolatestApiVersion)- the resource definitions referenced by
openapi.ts
Using the client
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 > 0recent: edited within the last 7 daysempty:revisionNumber === 0stale: not edited in the last 365 daysall/ missing: no further filtering