Skip to content

Bring V2 admin settings to parity with V1 for the Appearance group - #1396

Merged
Paul Lizer (paullizer) merged 2 commits into
paullizer-react-v2-uifrom
paullizer-v2-admin-appearance-parity
Sep 3, 2026
Merged

Bring V2 admin settings to parity with V1 for the Appearance group#1396
Paul Lizer (paullizer) merged 2 commits into
paullizer-react-v2-uifrom
paullizer-v2-admin-appearance-parity

Conversation

@paullizer

Copy link
Copy Markdown
Contributor

The problem

The V2 admin page could only render on/off switches. Not by design — it discovered settings by scanning the settings document for enable_* keys holding a boolean, so a switch was the only thing it could draw.

Everything else was invisible. For the Appearance group alone that meant the application title, three image uploads, a logo scale slider, two colour pickers, four selects, six text fields, three textareas, a four-way checkbox set, the external links list and the entire static page designer. The page even admitted it, with a footer telling admins to go back to /admin/settings.

The approach

Rather than hand-coding the missing controls, this adds a server-side declarative field schema and makes V2 a generic renderer for it.

  • admin_settings_fields.py declares, per section id already defined in admin_settings_nav.py, the concrete fields that section owns — type, label, help, default, bounds, options, visibility dependencies. It also normalizes and validates PATCH values, delegating to the existing terms-of-use and AI-notice normalizers so both admin surfaces agree on what a valid value is.
  • GET /api/v2/admin/settings returns the schema plus a description of the stored branding images (presence, version, URL — never the base64 blobs).
  • PATCH validates before writing and returns a field_errors map, applying nothing unless every key validates, so a save can't land half-applied.
  • POST /api/v2/admin/settings/branding-image handles logo, dark logo and favicon uploads.
  • functions_branding_images.py holds the image conversion that was previously inline in route_frontend_admin_settings.py.

Describing the remaining 13 groups is now a Python-only change. Sections with no declaration keep using the existing enable_* fallback scan, so nothing else in the admin page changes.

What Appearance looks like now

Tab Controls
Branding App title, show-logo / hide-title switches, 50–500% logo size slider, light + dark logo and favicon uploads with live previews
Home Page Text Alignment select, editor toggle, landing page text with a preview that follows the chosen alignment
Appearance Dark mode + left nav defaults
Notices & Agreements Classification banner (2 colour pickers + live preview strip), AI notice, full Terms of Use config, user agreement with 4 apply-to targets, word counter and Test preview
Pages & Links Custom pages with restart acknowledgement, the full static page designer, developer guide, and an add / remove / reorder external links editor

The static page designer writes to the same /api/admin/custom-pages CRUD as the classic one, so a page created in either interface is identical. Python-registered pages are listed but read-only, since they're defined in code.

Saving

Edits buffer into a draft and save together from a sticky bar, with Ctrl/Cmd+S and an unsaved-changes prompt on tab close.

This isn't just a UI preference. Terms of Use and the AI notice derive a content version from their text, and each new version re-prompts every user. Auto-saving would have minted a version per character typed. Branding uploads and custom page metadata still save immediately, because neither lives in the settings document.

Two bugs found along the way

External link URLs were unvalidated. They render into a navigation href on every page, so javascript:... was accepted and stored. Now restricted to local paths and http(s).

Two tests could no longer fail correctly. The Pillow security test pinned an exact dependency version (12.1.1 vs the current 12.3.0) and the custom logo test looked for help text in a file it had moved out of. Both were silently red before this change while no longer checking what they were written for. They now assert a minimum version and read the composed template.

Drift protection

The schema is a second description of settings the server-rendered panes also describe, so it could drift. Two tests prevent that:

  • test_v2_admin_appearance_parity.py reads the three V1 panes, collects all 41 form field names, and fails unless each is claimed by the schema — directly or through the documented LEGACY_FIELD_NAMES aliases where the shapes genuinely differ (V1's four user_agreement_apply_* checkboxes → the user_agreement_apply_to array; external_links_jsonexternal_links; the three file inputs → their stored base64 keys). It checks the reverse direction too, so the schema can't invent a setting nothing reads, and compares select option values and range bounds.
  • test_v2_admin_field_renderer_coverage.py fails when the schema can declare a type or component the React renderer has no branch for — the one silent failure mode this indirection introduces.

Both were mutation-tested by deliberately breaking the schema, to confirm they aren't vacuous.

Verification

  • 29 new checks across 4 test files, all passing
  • 20 existing test files re-run on the merged tree with zero failures, including the incoming shared-conversations suite from Support shared conversations in the V2 interface #1394
  • npm run typecheck and npm run build clean
  • Route policy inventory picks up the new upload endpoint with the expected login_required + admin_required classification
  • No new browser assets — reuses the already-vendored markdown renderer, CSP untouched

Notes for review

  • This merges the updated base and renumbers to 0.261.039, since Support shared conversations in the V2 interface #1394 landed and claimed 0.261.038.
  • Two pre-existing failures in test_custom_pages_wiring.py (an exact-version assertion and a missing docs/how-to/custom_pages.md) are unrelated — confirmed failing on a clean tree and left alone.
  • Removing an uploaded logo doesn't exist in V1 either, so it's out of scope. Worth a follow-up.
  • Admin settings remain deliberately unsanitized, as before — sanitization strips the very fields an administrator is there to manage. The logo base64 blobs stay out of the payload; only URLs and version counters are returned.

The V2 React admin page could only render on/off switches, because it
discovered settings by scanning the settings document for enable_* keys
holding a boolean. Every other control was invisible, so the Appearance
group showed a handful of switches and a note pointing back to the
classic page.

Add a server-side declarative field schema and make V2 a generic
renderer for it:

- admin_settings_fields.py declares, per section id already defined in
  admin_settings_nav.py, the concrete fields that section owns. It also
  normalizes and validates PATCH values, delegating to the existing
  terms-of-use and AI-notice normalizers so both admin interfaces agree
  on what a valid value is.
- GET /api/v2/admin/settings returns the schema and a description of the
  stored branding images; PATCH validates before writing and returns a
  field_errors map, applying nothing unless every key validates.
- POST /api/v2/admin/settings/branding-image accepts logo, dark logo and
  favicon uploads.
- functions_branding_images.py holds the image conversion, previously
  inline in route_frontend_admin_settings.py, so both surfaces store an
  identical asset and the PNG/JPEG decode allowlist covers every path.

The V2 page renders declared fields and keeps the enable_* fallback scan
for sections not described yet, so the other 13 groups are unchanged.
Edits buffer into a draft saved by a sticky save bar: Terms of Use and
the AI notice derive a content version from their text, and a new
version re-prompts every user, so saving per keystroke would mint a
version per character.

Appearance now has the application title, logo size slider, three image
uploads, alignment and markdown editor with live preview, the
classification banner with colour pickers and preview, the AI notice,
the full Terms of Use configuration, the user agreement with apply-to
targets, word counter and test preview, the custom page designer and
developer guide, and an add/remove/reorder external links editor.

External link URLs are now restricted to local paths and http(s). They
render into a navigation anchor, so unrestricted text allowed a
javascript: URL into the nav bar on every page.

Drift protection: test_v2_admin_appearance_parity.py reads the three V1
panes and fails unless every form field they submit is claimed by the
schema, directly or through the documented alias map, and checks the
reverse direction plus select options and range bounds.
test_v2_admin_field_renderer_coverage.py fails when the schema can
declare a type or component the React renderer has no branch for.

Also repairs two tests that could no longer fail correctly: the Pillow
check pinned an exact dependency version and the logo check read the
parent template after the branding controls moved into a partial.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
PR #1394 landed on the base branch and claimed 0.261.038, so the
Appearance parity work moves to 0.261.039.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) merged commit 8f443be into paullizer-react-v2-ui Sep 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant