Skip to content

Admin Settings: move Latest Features to the bottom and split the settings template - #1297

Merged
Paul Lizer (paullizer) merged 8 commits into
microsoft:feature/admin-settings-iafrom
paullizer:paullizer-admin-settings-ia-redesign
Aug 19, 2026
Merged

Admin Settings: move Latest Features to the bottom and split the settings template#1297
Paul Lizer (paullizer) merged 8 commits into
microsoft:feature/admin-settings-iafrom
paullizer:paullizer-admin-settings-ia-redesign

Conversation

@paullizer

@paullizer Paul Lizer (paullizer) commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

First delivery stage of the Admin Settings information architecture rework.

Targets the feature/admin-settings-ia staging branch, not Development. Nothing here reaches Development until the full IA rework is finished and tested, at which point the staging branch is merged in one reviewed PR.

Why

Admin Settings had grown organically into 17 flat tabs in a single 13,526-line, 1 MB template. The most visible symptom was that Latest Features opened on every single visit — a curated release-notes tab behaving like the landing page.

What changed

Latest Features no longer hijacks the page

Three separate places forced this and all three are fixed:

Location Before
Top-tab strip First <li>, class="nav-link active"
Tab pane class="tab-pane fade show active"
admin_sidebar_nav.js Bootstrapped to showAdminTab('latest-features')

Latest Features now sits last in both navigations, after Send Feedback, and General is the landing tab. Its content, New badge, and hide/unhide option are untouched.

Template split into per-tab partials

Each tab pane moved verbatim into templates/admin/_panes/ and is pulled back in with {% include %}. The parent keeps the global <form>, the modals, and the script blocks, and drops from 13,526 lines to about 2,200.

Two features that were rendered as afterthoughts

  • Global Identities was a bare JavaScript mount with no heading or description. It now has both, and explains that secrets go to Key Vault when Key Vault storage is configured.
  • File Sync — ~400 lines, 32 inputs — was the only tab with no sidebar submenu, so its sub-areas couldn't be reached or searched. Its source-type and per-workspace-type areas are now sidebar destinations.

Bugs found along the way

  • Classification banner preview never worked. Its script sat between {% endblock %} and {% block scripts %}, and Jinja discards content outside blocks in a child template, so it was never rendered.
  • Home Page Text preview reinterpreted editor text as HTML. The fallback assigned a textarea's raw .value to innerHTML despite the comment saying "just show raw text" (CodeQL js/xss-through-dom, high). Now textContent, with the Markdown branch sanitized inline via DOMPurify.sanitize(...) at the sink.
  • Sidebar sectionMap had rotted. 72 entries, of which 66 mapped a key to itself (already handled by the || sectionId fallback), two were unreferenced, and control-center-admin-section pointed at an element that doesn't exist. Now 6 real aliases, with a test preventing regrowth.

CI coverage for the staging branch

Every gate in this repo is pinned to branches: [Development], so PRs into an integration branch would have run only enforce-branch-flow — no CodeQL, no XSS sink check, no access-control or syntax checking. The final commit adds feature/admin-settings-ia to the pull_request filters of six gates so staged work is checked exactly like a Development PR. codeql.yml's push trigger is deliberately unchanged, and release-notes-check is excluded because it already fails on fork PRs for token reasons and is non-blocking.

This is reverted in the final staging → Development PR, so Development's CI configuration ends up untouched.

Verification

The save payload cannot have changed. Every name= attribute and card id was fingerprinted before any edit and compared after each stage, and again after rebasing onto current Development:

OK   field_names: 462 identical
OK   card_ids: 110 identical
OK   field_name_counts: 452 unique names, counts unchanged

Zero test regressions. All 75 functional test files referencing admin_settings.html, run against the Development baseline and after every stage: 33 failures before, 33 after, identical sets. Those 33 are pre-existing and unrelated (workflow route registration, Cosmos document-access wiring, Send Feedback docs).

All 20 admin templates parse under Jinja. scripts/check_xss_sinks.py and functional_tests/test_xss_guardrails_checker.py both pass locally. All 11 CI checks pass.

Rebase note

Development moved 14 commits during this work, including a new
shared-conversation-file-approvals-section card added to the region this PR split into partials. That card was ported into admin/_panes/ai-models.html in the position upstream placed it, which is why the fingerprint is 462/110 rather than the 461/109 quoted earlier. Version renumbered to 0.260.007 to clear upstream's 0.260.0040.260.006.

Note for reviewers

Tests that read templates/admin_settings.html directly now see only the parent shell. Use test_support.templates.read_admin_settings_template(), or compose_if_admin_settings() inside a shared file-reading helper.

The new test_admin_settings_template_composition.py enforces this — it fails if a test references a partial-backed card or form field while reading the template uncomposed. It caught 10 such tests, including 3 that were passing only because the assertion happened to be satisfied by the parent shell.

Still to come, into the same staging branch

  • A: nav map as a single source of truth for the IA; convert the 12 hardcoded switchTab cross-tab links to card-targeted links that survive renames
  • B: declarative dependency gating — inline mirror plus a resolved link when a setting needs another one enabled; unify the 10 hand-written Latest Features mirrors
  • C: group navigation level, three-level search, legacy hash redirect map
  • D: re-home cards into the 14 groups, one group per commit
  • E: split system-settings-section; consolidated Access and Roles roster

Fix documentation in docs/explanation/fixes/ADMIN_SETTINGS_NAVIGATION_AND_TEMPLATE_STRUCTURE_FIX.md.

Comment thread functional_tests/test_admin_settings_template_composition.py Fixed
@paullizer

Copy link
Copy Markdown
Collaborator Author

Copilot resolve the merge conflicts in this pull request

Paul Lizer (paullizer) and others added 7 commits August 19, 2026 13:23
Admin Settings had grown to 13,526 lines in a single 1 MB template, which
made the upcoming information architecture rework impractical to do safely.

Each tab pane now lives in application/single_app/templates/admin/_panes/
and is pulled back in with {% include %}. The parent template keeps the
global form, the modals, and the script blocks, so the submitted payload is
unchanged: every field name, and all 109 configuration card ids, are
byte-identical to before the split.

Functional tests that read admin_settings.html straight from disk would now
only see the parent shell, so they route through a new shared helper,
test_support.templates, which inlines the Admin Settings partials and leaves
every other repository file untouched.

Adds test_admin_settings_template_composition.py to pin the contract: the
parent delegates to partials, no partial is orphaned, composition restores
the full card inventory, and no functional test asserts on a partial-backed
card without composing the template first.

Verified against the 75 functional test files that touch admin_settings.html:
32 pre-existing failures before and after, identical sets, zero regressions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Latest Features was pinned first in both the top-tab strip and the admin
sidebar, and its pane was hard-coded as the default active tab, so it opened
on every single visit to Admin Settings. That made a curated release-notes
tab behave like the landing page.

It now sits last in both navigations, after Send Feedback, and General is the
default landing tab instead. The Latest Features markup itself is unchanged,
including its New badge and the hide/unhide dropdown.

Three places drove the old behaviour and all three are updated: the top-tab
button ordering and active class, the tab-pane show active class, and the
sidebar bootstrap default in admin_sidebar_nav.js.

Test assertions that pinned the old ordering are inverted to pin the new
intent, so a future change cannot silently restore the landing behaviour.

Field names and card ids remain byte-identical, and the 75 functional test
files covering admin_settings.html show the same 32 pre-existing failures as
before the change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two Admin Settings features were rendered as afterthoughts compared to every
other tab.

Global Identities was a bare JavaScript mount point: no heading, no
description, and no intro copy, so the tab rendered as an unlabelled widget.
It now has a heading and explains that identities are deployment-wide and
that secrets are held in Key Vault when Key Vault storage is configured,
which matches the behaviour in functions_workspace_identities.py.

File Sync is one of the larger settings surfaces at roughly 400 lines and 32
inputs, but it was the only tab with no sidebar submenu, so none of its
sub-areas could be reached or searched from the navigation. Its source-type
and per-workspace-type areas now carry ids and are exposed as sidebar
destinations. The existing data-testid hooks are left in place.

The composition guard added with the template split is extended to cover form
field names as well as card ids. That immediately surfaced seven more tests
that read admin_settings.html without composing its partials, including ones
asserting on fields rather than cards, and those are now routed through the
shared helper.

Field names and card ids remain byte-identical, and the 75 functional test
files covering admin_settings.html show the same 32 pre-existing failures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The classification banner live preview never worked. Its script sat between
{% endblock %} and {% block scripts %} in a child template, and Jinja drops
content outside blocks in a child template, so the code was never rendered.
Moving it inside the scripts block makes the preview respond to banner text,
background colour, and text colour changes.

Bumps VERSION to 0.260.004 and adds the fix write-up plus release notes for
this delivery stage: the template split, the Latest Features reposition, the
Global Identities and File Sync surfaces, and this preview fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
scrollToSection resolves a sidebar target with sectionMap[sectionId] ||
sectionId, so an entry that maps a key to itself does nothing. The map had
grown to 72 entries: 66 were self-referencing no-ops, two were referenced by
no sidebar link, and control-center-admin-section pointed at an element that
does not exist anywhere in the template.

Every new admin section had been registered twice, once as a sidebar link and
once in a map that did not need it, so the map drifted out of sync with the
markup without anything failing.

Reduced to the six genuine aliases. A new test in the sidebar parity suite
fails if a no-op, dangling, or unreferenced entry is added back, so the map
cannot rot again.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two alerts were reported against this branch.

js/xss-through-dom (high) in admin_settings.html: showPreview fell back to
assigning the raw editor contents to innerHTML when the Markdown editor had
not initialized, so text typed into the editor was reinterpreted as HTML. The
comment already stated the intent was to just show raw text, so the fallback
now uses textContent, which removes the sink and matches that intent.

The Markdown branch in the same function also wrote rendered HTML straight to
innerHTML. It is now passed through DOMPurify, following the pattern the User
Agreement preview in this same template already uses. DOMPurify comes from the
locally served static bundle, so no external asset is introduced.

py/unused-import in test_admin_settings_template_composition.py:
resolve_template_includes was imported but only appeared as a string literal
inside COMPOSITION_HELPERS, so the import was genuinely unused.

Bumps VERSION to 0.260.005 with release notes and fix documentation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The previous fix assigned DOMPurify output through an intermediate variable,
which cleared CodeQL but tripped the repository's own xss-sink-check. That
checker only treats an identifier as safe when it is declared directly from a
sanitizer call, so reassigning inside an availability guard left the innerHTML
sink looking unsanitized on the changed line.

DOMPurify is loaded globally from the local vendored bundle in base.html,
before the page script block, so the guard was not buying anything. Sanitizing
inline at the sink is both simpler and visible to the checker.

Verified locally with scripts/check_xss_sinks.py and
functional_tests/test_xss_guardrails_checker.py, both passing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) force-pushed the paullizer-admin-settings-ia-redesign branch from 2f94c2c to 5884e76 Compare August 19, 2026 17:28
@paullizer
Paul Lizer (paullizer) changed the base branch from Development to feature/admin-settings-ia August 19, 2026 17:28
Every gate in this repository is pinned to branches: [Development], so a pull
request into the feature/admin-settings-ia integration branch would run only
enforce-branch-flow. That would leave the Admin Settings information
architecture work with no CodeQL, XSS sink, access-control, malicious-PR, or
syntax checking for its whole staging period, which is the opposite of what an
integration branch is for.

These gates have already earned their keep on this work: CodeQL caught the
js/xss-through-dom sink in the Home Page Text preview, and xss-sink-check
caught the first attempted fix for it.

Adds the staging branch to the pull_request filters only. The push trigger on
codeql.yml is deliberately unchanged. release-notes-check is excluded because
it already fails on fork pull requests for token reasons and is non-blocking.

This is reverted in the final integration -> Development pull request, so
Development's own CI configuration is untouched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) merged commit 08c61b6 into microsoft:feature/admin-settings-ia Aug 19, 2026
11 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.

2 participants