Bring V2 admin settings to parity with V1 for the Appearance group - #1396
Merged
Paul Lizer (paullizer) merged 2 commits intoSep 3, 2026
Merged
Conversation
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>
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.pydeclares, per section id already defined inadmin_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/settingsreturns the schema plus a description of the stored branding images (presence, version, URL — never the base64 blobs).PATCHvalidates before writing and returns afield_errorsmap, applying nothing unless every key validates, so a save can't land half-applied.POST /api/v2/admin/settings/branding-imagehandles logo, dark logo and favicon uploads.functions_branding_images.pyholds the image conversion that was previously inline inroute_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
The static page designer writes to the same
/api/admin/custom-pagesCRUD 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+Sand 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
hrefon every page, sojavascript:...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.1vs the current12.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.pyreads the three V1 panes, collects all 41 form field names, and fails unless each is claimed by the schema — directly or through the documentedLEGACY_FIELD_NAMESaliases where the shapes genuinely differ (V1's fouruser_agreement_apply_*checkboxes → theuser_agreement_apply_toarray;external_links_json→external_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.pyfails 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
npm run typecheckandnpm run buildcleanlogin_required+admin_requiredclassificationNotes for review
0.261.039, since Support shared conversations in the V2 interface #1394 landed and claimed0.261.038.test_custom_pages_wiring.py(an exact-version assertion and a missingdocs/how-to/custom_pages.md) are unrelated — confirmed failing on a clean tree and left alone.