Skip to content

feat(admin): explain env-var substitution in /settings, surface auth errors (#7819)#7826

Open
JohnMcLear wants to merge 2 commits into
developfrom
issue-7819-admin-settings-ux
Open

feat(admin): explain env-var substitution in /settings, surface auth errors (#7819)#7826
JohnMcLear wants to merge 2 commits into
developfrom
issue-7819-admin-settings-ux

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Three small admin /settings UX improvements driven by #7819, where a Docker operator saved a plugin block in the raw view and reported it "disappeared." After diagnosing the issue, root cause turned out to be a mix of environmental factors (no volume mount on settings.json, env-var substitution misunderstanding) — but the Etherpad UI was confusing enough that somebody will hit this again. These changes close the gaps without touching the UX of non-container installs.

  • Banner above the editor explaining the template / env-var substitution model. Only rendered when the loaded file contains a \${VAR} placeholder, so non-container installs see no change.
  • Effective tab in the mode toggle — read-only view of the in-memory settings the server is actually using (with secrets redacted). The backend was already emitting resolved alongside every load; the SPA now exposes it. Same gate: only appears when \${VAR} is in the file.
  • `admin_auth_error` event from the /settings socket. The connection handler previously silently returned when the session wasn't admin — making misrouted Traefik+SSO auth look like "save did nothing" with no error path. Now emits a dedicated event the SPA surfaces as a toast before dropping the socket.

No behaviour change for users who don't use env-var placeholders in their settings.json.

Screenshots / behaviour

With \${VAR} in the file (Docker default):

  • Top of /settings: blue note banner: "This file is a template, not the live config. Placeholders like ${VAR:default} are substituted into memory at startup; they are never written back to this file…"
  • Mode toggle: Form | Raw | Effective
  • Effective tab: read-only textarea showing the live in-memory config with passwords/secrets as `[REDACTED]`.

Without any `${VAR}` placeholders:

  • No banner, no Effective tab. Identical to today's UI.

Test plan

  • `pnpm run build` (admin SPA) passes
  • `pnpm run ts-check` (src) passes
  • `adminSettingsAuthError.ts` — new spec proves the auth_error/disconnect contract fires for non-admin sockets
  • `adminSettingsSave.ts` — existing save tests still pass (no save-path change)
  • `adminSettingsResolved.ts` — existing resolved tests still pass (no load-path change)
  • `adminsettings.spec.ts` — new Playwright test asserts banner + Effective tab only appear after `${VAR}` is added, and Effective view is readonly + shows [REDACTED]

Backend mocha output:
```
18 passing (2s)
```

🤖 Generated with Claude Code

…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>
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add env-var substitution UX improvements and auth error handling to admin /settings

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add explanatory banner and Effective tab for env-var substitution in /settings
• Emit admin_auth_error event when non-admin socket connects to /settings
• Gate Docker-aware UX features on presence of ${VAR} placeholders in settings.json
• Add comprehensive tests for auth error handling and env-var banner/Effective tab behavior
Diagram
flowchart LR
  A["Non-admin socket connects"] -->|Previously| B["Silent disconnect"]
  A -->|Now| C["Emit admin_auth_error event"]
  C --> D["SPA shows toast"]
  E["Settings file with \${VAR}"] -->|Detected| F["Show env-var banner"]
  E -->|Detected| G["Enable Effective tab"]
  G --> H["Display redacted runtime config"]
  I["Settings file without \${VAR}"] -->|No change| J["Original UI unchanged"]

Loading

File Changes

1. src/node/hooks/express/adminsettings.ts Error handling +11/-1

Emit auth error event for non-admin sockets

src/node/hooks/express/adminsettings.ts


2. src/tests/backend/specs/admin/adminSettingsAuthError.ts 🧪 Tests +56/-0

New test for admin auth error socket event

src/tests/backend/specs/admin/adminSettingsAuthError.ts


3. src/tests/backend/specs/admin/adminSettingsSave.ts 🧪 Tests +1/-0

Minor formatting adjustment to test file

src/tests/backend/specs/admin/adminSettingsSave.ts


View more (6)
4. src/tests/frontend-new/admin-spec/adminsettings.spec.ts 🧪 Tests +52/-0

New Playwright test for env-var banner and Effective tab

src/tests/frontend-new/admin-spec/adminsettings.spec.ts


5. admin/src/App.css ✨ Enhancement +31/-0

Add styling for env-var banner component

admin/src/App.css


6. admin/src/App.tsx ✨ Enhancement +13/-0

Add socket listener for admin_auth_error event

admin/src/App.tsx


7. admin/src/components/settings/ModeToggle.tsx ✨ Enhancement +19/-2

Add Effective tab to mode toggle with conditional rendering

admin/src/components/settings/ModeToggle.tsx


8. admin/src/pages/SettingsPage.tsx ✨ Enhancement +89/-38

Implement env-var detection, banner, and Effective tab view

admin/src/pages/SettingsPage.tsx


9. src/locales/en.json 📝 Documentation +5/-0

Add i18n strings for new banner and Effective tab UI

src/locales/en.json


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 20, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Effective tab lacks feature-flag 📘 Rule violation ⚙ Maintainability
Description
The new env-var banner and Effective settings view are enabled automatically based on ${VAR}
placeholders rather than a dedicated feature flag that is disabled by default. This violates the
requirement to gate new functionality behind an explicit enable/disable mechanism to preserve
default behavior control.
Code

admin/src/pages/SettingsPage.tsx[R12-42]

Evidence
PR Compliance ID 7 requires new features to be behind a feature flag and disabled by default. The PR
introduces a new banner and Effective tab, but enables them via the heuristic
ENV_VAR_PATTERN.test(settings) and resolved != null instead of an explicit flag that admins can
toggle (or that defaults off regardless of content).

admin/src/pages/SettingsPage.tsx[12-116]
admin/src/components/settings/ModeToggle.tsx[3-50]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New Admin Settings functionality (env-var banner + `Effective` tab/view) is gated by detecting `${VAR}` placeholders in `settings.json`, not by a dedicated feature flag that is disabled by default.
## Issue Context
Compliance requires new features to be behind a feature flag with an explicit enable/disable mechanism and default-off behavior.
## Fix Focus Areas
- admin/src/pages/SettingsPage.tsx[12-116]
- admin/src/components/settings/ModeToggle.tsx[3-50]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Auth error reconnect loop ✓ Resolved 🐞 Bug ☼ Reliability
Description
Non-admin clients are now forcibly disconnected from /settings, but the admin SPA reconnects on
server-initiated disconnects, which can create an infinite reconnect loop with repeated toasts and a
stuck loading overlay. This can spam the server with reconnect attempts and prevent the UI from
settling into a stable “not authorized” state.
Code

src/node/hooks/express/adminsettings.ts[R53-62]

Evidence
The backend now explicitly disconnects non-admin sockets, and the admin SPA reconnects on
server-initiated disconnects; combined, this creates a loop. The new admin_auth_error handler does
not disable reconnect, and the disconnect handler sets loading true then reconnects, which conflicts
with the intent to show an auth error and stop.

src/node/hooks/express/adminsettings.ts[49-63]
admin/src/App.tsx[47-50]
admin/src/App.tsx[80-91]
admin/src/utils/LoadingScreen.tsx[5-19]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The backend now emits `admin_auth_error` and then calls `socket.disconnect(true)` for non-admin sockets. In the admin SPA, the existing disconnect handler reconnects whenever the reason is `'io server disconnect'`, which is exactly what happens for server-initiated disconnects. This can cause an infinite reconnect loop for unauthorized sessions (toast spam + loading overlay toggling + unnecessary server load).
### Issue Context
- Server change disconnects non-admin sockets.
- Client reconnect logic is intended for transient server disconnects, but should not apply to an authorization failure.
### Fix Focus Areas
- admin/src/App.tsx[47-50]
- admin/src/App.tsx[80-91]
- (optional) src/node/hooks/express/adminsettings.ts[53-62]
### Suggested fix
1. In `admin/src/App.tsx`, set a flag when `admin_auth_error` is received (or immediately disable reconnection via `settingSocket.io.opts.reconnection = false`), and in the `disconnect` handler skip `settingSocket.connect()` when that flag is set.
2. Consider navigating the user to `/login` (or showing a stable logged-out state) on `admin_auth_error`.
3. Optionally, adjust the server to include a machine-readable reason (e.g. `{code: 'ADMIN_AUTH_REQUIRED'}`) so the client can reliably disable reconnect without string matching.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Auth toast not localized 🐞 Bug ⚙ Maintainability
Description
The backend always sends an English message with admin_auth_error, and the SPA prioritizes
payload.message over the i18n key, so localized UIs will still display English. This makes the new
admin_settings.toast.auth_error translation effectively unused.
Code

src/node/hooks/express/adminsettings.ts[R59-61]

Evidence
The server emits a hard-coded English message, and the SPA uses that message in preference to the
i18n translation. As a result, the new translation key is unreachable in normal operation.

src/node/hooks/express/adminsettings.ts[53-61]
admin/src/App.tsx[83-88]
src/locales/en.json[135-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`admin_auth_error` always includes an English `message`, and the SPA uses `payload.message || t('admin_settings.toast.auth_error')`, so the translated string will never be shown when the server emits the event.
### Issue Context
The admin SPA already has an i18n string for this toast, but it is bypassed by the server-provided English string.
### Fix Focus Areas
- src/node/hooks/express/adminsettings.ts[59-61]
- admin/src/App.tsx[83-90]
- src/locales/en.json[138-140]
### Suggested fix
- Prefer localization in the SPA by either:
1) Removing `message` from the server payload (emit `{}` or `{code: 'ADMIN_AUTH_REQUIRED'}`) and always use `t('admin_settings.toast.auth_error')`, or
2) Sending a stable `code`/`messageKey` from the server and mapping it to localized copy in the SPA.
- Keep `payload.message` only as a fallback for unexpected server payloads.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +12 to 42
// Heuristic: `${VAR}` or `${VAR:default}` in the file means the operator is
// running with env-var substitution (overwhelmingly Docker / Kubernetes).
// We use this to gate the Docker-aware UX (the explanatory banner and the
// Effective-config tab) so non-container installs see the existing UI
// unchanged. Conservative on purpose — false negatives just keep the old
// behaviour.
const ENV_VAR_PATTERN = /\$\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\}/;

export const SettingsPage = () => {
const { t } = useTranslation();
const settingsSocket = useStore(state => state.settingsSocket);
const settings = useStore(state => state.settings) ?? '';
const resolved = useStore(state => state.resolved);

const usesEnvVars = useMemo(() => ENV_VAR_PATTERN.test(settings), [settings]);

const [mode, setMode] = useState<Mode>('form');
const [exposeExperimental] = useState(false);

// The Effective tab is only meaningful when there is a `resolved`
// payload AND the file uses substitution. Falling back to Raw on
// either condition keeps the toggle honest if the user opens this
// page against an older server.
const canShowEffective = usesEnvVars && resolved != null;
useEffect(() => {
if (mode === 'effective' && !canShowEffective) setMode('raw');
}, [mode, canShowEffective]);

// Tab in textarea inserts two spaces instead of moving focus; rAF restores caret position after React re-renders.
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== 'Tab') return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. effective tab lacks feature-flag 📘 Rule violation ⚙ Maintainability

The new env-var banner and Effective settings view are enabled automatically based on ${VAR}
placeholders rather than a dedicated feature flag that is disabled by default. This violates the
requirement to gate new functionality behind an explicit enable/disable mechanism to preserve
default behavior control.
Agent Prompt
## Issue description
New Admin Settings functionality (env-var banner + `Effective` tab/view) is gated by detecting `${VAR}` placeholders in `settings.json`, not by a dedicated feature flag that is disabled by default.

## Issue Context
Compliance requires new features to be behind a feature flag with an explicit enable/disable mechanism and default-off behavior.

## Fix Focus Areas
- admin/src/pages/SettingsPage.tsx[12-116]
- admin/src/components/settings/ModeToggle.tsx[3-50]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/node/hooks/express/adminsettings.ts
)

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>
@JohnMcLear
Copy link
Copy Markdown
Member Author

Thanks Qodo — first one (#1) is intentional, second (#2) is already addressed; quick notes on each:

#1 — Effective tab / banner without dedicated feature flag

These two additions are read-only informational UX that don't change Etherpad's runtime behaviour at all — no save semantics changed, no load semantics changed, no API surface added that wasn't already there. The backend already emitted resolved alongside every load (since #7670); this PR just exposes that existing data in a non-editable view.

The activation heuristic — render only when ${VAR} placeholders are present in settings.json — is itself a form of soft gating: operators who don't use env-var substitution (the overwhelming majority of non-container installs) never see the banner or the Effective tab, so for them the UX is byte-identical to today.

That said, happy to add a settings.adminUI.envVarHints flag if @JohnMcLear prefers an explicit toggle — defaulting true (so the docker audience this PR exists for actually sees the UX) with the option to disable. Pinging you to weigh in.

#2 — Auth error reconnect loop

Already fixed in 5c6b164 (commit pushed before this review landed) — the SPA now tracks an authErrored flag that is reset on each successful connect and suppresses the auto-reconnect once an admin_auth_error has fired. Qodo correctly auto-marked this resolved in the same review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant