Skip to content

Admin Settings information architecture rework: 17 flat tabs to 14 groups - #1319

Merged
Paul Lizer (paullizer) merged 39 commits into
Developmentfrom
feature/admin-settings-ia
Aug 20, 2026
Merged

Admin Settings information architecture rework: 17 flat tabs to 14 groups#1319
Paul Lizer (paullizer) merged 39 commits into
Developmentfrom
feature/admin-settings-ia

Conversation

@paullizer

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

Copy link
Copy Markdown
Contributor

The Admin Settings information architecture rework, staged and verified across eight pull requests on feature/admin-settings-ia.

Why

Admin Settings had grown to 17 flat tabs in a single 14,344-line template. Related settings were scattered — voice and video sat under a chat heading, conversation archiving under Safety, health checks under General — and Latest Features opened on every visit, which was the original complaint.

What changed

Latest Features is now last and never the landing tab.

The 17 flat tabs became 14 groups / 44 tabs / 93 sections:

Group Tabs
Appearance Branding, Notices & Agreements, Pages & Links
Chat Chat Experience, Feedback & Alerts, Citations
AI Models Model Endpoints, Embeddings, Image Generation
Agents & Actions Agents, Actions, Inbound MCP
Workspaces Workspace Types, Files & Sharing, Global Identities
Workflow Workflow
Knowledge Web & Research, Search Index, Document Extraction, Audio & Video, File Sync
Security Access & Roles, Secrets, Content Safety, Session, Network
Governance Feature Governance, Policies, MCP Governance
Data Lifecycle Retention, Classification, Archiving
Backup & Recovery Backup, Migrate, Restore, Cosmos Editor, Jobs
Scale Redis & Caching, Cosmos
Operations Control Center, Logging & Health
Help Support Menu, Send Feedback, Latest Features

Grouping follows what each area actually does and how large it is, not what its title says. Voice and video are extraction pipelines, so they sit under Knowledge. Workflow and Governance were too big to nest inside anything else.

The template is now a shell plus 44 pane partials, and both navigations render from one source of truth (admin_settings_nav.py).

The guarantee: no setting changed

Admin Settings posts a single form and the backend reads by field name. That is the only thing that can break, so it was measured on every stage — and again after merging Development in:

Development : 452 distinct field names
staging     : 452 distinct field names
lost  : 0
added : 0

Not one field was renamed, dropped or duplicated. Every saved value survives; nothing needs re-entering.

Bugs found and fixed along the way

These were all latent — most only surfaced because something moved:

  • Group Workflow Assignment dialog would have been stranded in a tab that never opens it. An inactive pane is hidden, so it could never have appeared.
  • Model Setup Guide dialog is opened from three cards that became three separate tabs; it now lives outside the panes so all three reach it.
  • "Open Key Vault settings" in Data Management switched tabs by a hardcoded id and broke the moment that tab was renamed.
  • The landing pane was hardcoded in two places; splitting that pane left Admin Settings with no active tab at all.
  • Backup & Recovery save button would have been permanently hidden in the sidebar layout.
  • Two Citations links pointed at the wrong tab entirely — already broken before this work.
  • Classification banner preview sat outside its Jinja blocks and never rendered.
  • A missing nav entry for shared conversation file approvals.

Guardrails added

Eleven new test files, including:

  • test_admin_settings_field_contract.py — the field-name baseline. The one test that must never be updated casually.
  • test_admin_settings_modal_placement.py — every dialog must sit where it can be opened. Verified against a deliberately stranded modal.
  • test_admin_settings_template_composition.py — pane balance, and a check that no test reads the template uncomposed.
  • test_admin_settings_group_shared_regions.py — asserts all 1,608 element ids are unique.
  • test_admin_access_roles_roster.py — mirrors must carry no name.

Several existing tests hardcoded specific tab ids. They now assert durable properties — Latest Features is last, exactly one pane is active, every tab renders one pane — rather than names that legitimately change.

New capabilities

  • App Role Requirements roster — the ten require_member_of_* settings spread across seven tabs are now listed together in Security → Access & Roles, built from the page so it cannot go stale.
  • Dependency notices — settings that need another setting enabled say so, with an inline mirror and a link to the prerequisite.
  • Three-level search across group, tab and card.
  • Collapsible groups in the sidebar with remembered state, and group pills in the tab layout.

Verification

Check Result
Field names vs Development 452 → 452, zero lost, zero added
Regression set (75 test files) 32 pre-existing failures, identical set throughout
Jinja compile 46/46 admin templates
Element ids all 1,608 unique
Pane balance all 44 panes balanced
XSS sinks pass
Admin contract suite 43 tests pass

Housekeeping

Merged stages

#1297, #1304, #1306, #1307, #1309, #1310, #1311, #1313, #1314, #1315, #1316, #1318, #1320

Version: 0.260.018

Paul Lizer (paullizer) and others added 30 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>
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>
…edesign

Admin Settings: move Latest Features to the bottom and split the settings template
Cross-references between Admin Settings tabs named a tab button directly, for
example switchTab(event, 'workspaces-tab'). That couples every link to a tab
id, so a rename or regroup breaks it silently: the button no longer exists, no
pane is activated, and the only symptom is a console warning and a URL hash
pointing at nothing.

Two of the twelve links were already broken this way. Citations referenced
Enable Video File Support and Enable Audio File Support with #workspaces, and
the prose said to see the Workspaces tab, but both settings live under Search
and Extract. Those links sent admins to the wrong tab entirely.

Links now declare the card they want with data-admin-link, and the owning tab
is resolved from the DOM at click time via closest('.tab-pane'). Cards can move
between tabs, and tabs can be renamed or regrouped, without touching a link.
The destination card is briefly highlighted so the jump is obvious.

This matters now because the information architecture rework renames and
regroups most tabs, which would have broken all twelve links at once.

switchTab had no callers left and is removed, so the coupling cannot be
reintroduced by copy-paste. The new test fails if a link points at a card that
does not exist, if the href fallback stops matching, or if the old pattern
returns.

Also sanitizes the User Agreement preview inline at the sink. It used the same
guarded-reassignment shape already corrected for the Home Page Text preview,
which reads as unsanitized to static analysis; admin_settings.html now passes a
full-file XSS scan rather than only a changed-lines scan.

Field names and card ids unchanged at 462 and 110, and the 75 functional test
files covering admin_settings.html show the same 33 pre-existing failures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Several settings only take effect when a different setting is enabled, and the
two often live in different tabs. That was communicated four different ways,
none of them actionable: a static server-rendered alert, a sentence of prose, a
tooltip, or a flash message only after saving. An admin could switch something
on and have nothing happen with no visible reason, and no indication of where
the prerequisite lived.

A dependent card now declares what it needs with data-requires, and a shared
module renders an inline notice with two ways to act: a mirror of the
prerequisite switch, so it can be satisfied without leaving the tab, and a link
to its card using the stage A resolver.

The mirror deliberately carries no name attribute. Admin Settings posts one
form read by field name, so a second named control would submit the value
twice; the mirror drives the canonical input instead and exactly one named
control per setting is ever submitted.

File Sync warns rather than blocks, because the backend already accepts it as
requested and reconciles once Redis is ready. Removing that would take away a
deferred-intent path the backend supports on purpose. The Permissions
dependency is scoped to the FeedbackAdmin toggle so the unrelated
SafetyViolationAdmin toggle in the same card stays usable.

Three candidate dependencies are deliberately not wired: backup source blobs
are already gated in admin_data_management.js, multi-modal vision falls back to
the legacy GPT deployment rather than requiring multi-endpoint, and the Redis
Key Vault hint is conditional on a select value and already links correctly.

The File Sync server alert is narrowed to the case the client cannot detect,
Redis enabled but missing URL or credentials, so the two messages no longer
overlap.

Client-side gating is advisory. The backend still validates and flashes, and a
test asserts that enforcement is still in place.

Field names and card ids unchanged at 462 and 110, and the 75 functional test
files covering admin_settings.html show the same 33 pre-existing failures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Admin Settings IA stage A+B: card-targeted links and dependency announcements
Admin Settings submits a single form and the backend reads every value by
field name, so the set of name attributes is the real contract between the
template and route_frontend_admin_settings.py. Renaming or dropping one
silently stops that setting from saving: no error is raised, the control still
renders, and the value simply never arrives.

Every stage of the information architecture rework has been verified against a
field-name fingerprint, but that check lived in a scratch script and only ran
when someone remembered to run it. Stage D moves 96 cards between tabs, which
is exactly when an accidental rename is most likely and least visible.

The check is now a functional test with the baseline committed alongside it, so
it runs in CI on every commit and protects anyone editing these templates
later, not just this migration.

Adding settings is unaffected: new field names are reported, not failed.
Removing or renaming one requires regenerating the baseline in the same commit
via --update-baseline, which turns a silent loss into a reviewed decision.

The test also rejects duplicate field names outside the five known radio and
checkbox groups. That is the property that stops a mirrored control from
submitting its value twice, which the Access and Roles roster in stage E
depends on.

Both assertions were verified to fail correctly: renaming a field is reported
by name, and introducing a duplicate is reported with its count.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The rework spans several pull requests into a staging branch, and the context
needed to continue it safely has been living in conversation history and in
scratch scripts rather than in the repository.

Records what has shipped, how to run each guardrail, the regression baseline of
33 pre-existing failures, the three structural complications found while
splitting the template, the locked decisions, and the CI branch filters that
must be reverted before the staging branch merges into Development.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Admin Settings: enforce the form field contract before stage D moves cards
Admin Settings presented 18 tabs in a single flat list, and that list was
written out twice: once as the top tab strip and once as the sidebar. The two
copies had already drifted. Tab order differed between them, and Agents, Custom
Pages and Search and Extract each carried a different label depending on which
navigation you were looking at.

Both now render from admin_settings_nav.ADMIN_NAV, which describes the
navigation as groups containing tabs containing sections. A change is made once
and appears in both. The map was generated from the existing sidebar markup
rather than retyped, so it is faithful by construction, and the rendered
destinations were compared before and after: the same 18 tabs and the same 86
sections, with per-tab section order preserved.

That comparison also surfaced a gap. The Shared Conversation File Approvals
card, added upstream while this work was in progress, had no navigation entry
at all and could only be found by scrolling. It is now listed like every other
setting, which fixes a pre-existing parity failure.

The group level is added on top: collapsible headers with persisted state in
the sidebar, and a row of group pills that filters the tab strip in the tab
layout. Opening a tab reveals its group first in either layout, so a deep link
or a cross-reference can never activate a pane whose tab is out of view.
Sidebar search now spans groups as well as tabs and sections.

No cards move in this change. Field names and card ids are unchanged at 462 and
110, so saving is untouched, and the 75 functional test files covering
admin_settings.html go from 33 pre-existing failures to 32.

Tests that asserted against hand-written markup now assert against the map,
which is the contract both renderings depend on, and a new test rejects a
malformed map, a destination with nothing behind it, or a tab hardcoded back
into the strip.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Records the navigation map, the new regression baseline of 32 pre-existing
failures after stage C fixed a missing navigation entry, and how stage D
becomes a two-part edit now that navigation structure lives in one place.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Admin Settings IA stage C: group navigation rendered from one definition
First card move of the information architecture rework, and the pattern the
remaining groups follow: split the pane markup, update the navigation map, and
add a legacy redirect so existing links keep working.

Governance held five cards covering three distinct concerns. It is now three
tabs: which features are governed, the policies themselves, and MCP-specific
governance.

The split is done by a tool that works line by line and refuses to write unless
every source line is carried into exactly one output. That immediately caught a
real problem: governance-status sat after the last card, so copying the pane
trailer into each new pane would have produced three elements sharing one id.
It is now rendered once outside the panes, which is also better behaviour,
since a status message is no longer trapped on whichever governance tab
happened to be active when it was raised.

Adds LEGACY_TAB_REDIRECTS so #governance still resolves, now to Feature
Governance. Later moves extend the same map.

Field names and card ids are unchanged at 462 and 110, and the regression set
holds at 32 pre-existing failures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…rity

Scale mixed three concerns: cache configuration, Cosmos capacity, and Front
Door. It is now Redis & Caching and Cosmos, with Front Door moved out entirely.

Front Door configures authentication and redirect flows, not throughput, so it
sits under Security as a Network tab. That move needed no markup surgery: panes
are independent files and groups are entries in the navigation map, so moving a
whole tab between groups is a map change alone. Only moving a card between tabs
requires touching markup.

This pane exercised the interleaved-modal case. Five modals sit between cards
rather than collected at the end, and each has to travel with the tab owning its
trigger. The split tool assigns the gap after a card to that card, which placed
the Redis explorer with Redis, the three maintenance confirmations with Cosmos
Maintenance, and the two throughput dialogs with Cosmos Throughput. Verified by
listing the modals in each new pane.

Extends the legacy redirect map so #scale still resolves.

Field names and card ids are unchanged at 462 and 110, and the regression set
holds at 32 pre-existing failures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Records the split pattern that Governance and Scale established, the property
that moving a whole tab between groups needs only a navigation map change, and
the gap that has to close before the remaining groups can move: the split tool
handles one source pane at a time, but the rest pull cards from several.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Admin Settings IA stage D part 1: split Governance and Scale, move Front Door to Security
Move cards across panes so they sit with the settings they actually
work with, without touching a single field name.

Data Lifecycle (new group)
  Retention       retention-policy-section          from workspaces
  Classification  document-classification-section   from workspaces
  Archiving       conversation-archiving-section    from safety

  These three decide how long content lives and how it is labelled.
  Archiving was under Safety, which describes what it guards against
  rather than what it does.

Chat (new group)
  Chat Experience   processing-thoughts-section   from ai-models
                    chat-file-uploads-section     from workspaces
                      (carries nested conversation-contents-drawer-section)
                    workspace-scope-lock-section  from workspaces
  Feedback & Alerts user-feedback-section         from safety
                    desktop-notifications-section from safety

Moves use move_card.py, which refuses to write unless every source line
lands in exactly one output. Nested cards travel with their parent and
the gap after a card travels with it, so interleaved modals stay with
the tab owning their trigger.

Navigation is now 13 groups / 27 tabs / 88 sections.

Verified
  field contract   462 names / 110 card ids, unchanged
  regression set   32 failures, identical to baseline
  jinja compile    29/29 admin templates
  xss sinks        5 changed files pass

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Stage D part 2: Data Lifecycle and Chat groups
General had grown into a catch-all of eleven unrelated cards. Security
held one card while a separate Safety tab mixed content filtering with
role permissions. Both are broken up here.

Appearance
  Branding             branding, home page text, appearance
  Notices & Agreements classification banner, chat AI notice, terms of
                       use, user agreement (from workspaces)
  Pages & Links        static pages, external links (from general)

Security
  Access & Roles   permissions-section        who gets in, and as what
  Secrets          keyvault-section           was the "security" tab
  Content Safety   content-safety-section     what may be said once in
  Session          system-settings-section    interim home, see below
  Network          front-door-section

Operations   Logging becomes Logging & Health, gaining health check and
             API documentation from General.
Help         Gains Support Menu from General.

system-settings-section is a mixed card: four of its eight fields are
idle timeout, so Session is the plurality home. The remaining fields
are split out to their real homes in a later change.

Two hardcoded-id traps closed
  openKeyVaultSettings switched tabs by the literal id "security-tab",
  so the Data Management link to Key Vault broke the moment that tab
  was renamed. The function was dead weight: the link now carries
  data-admin-link and the tab is resolved from the DOM.

  The landing pane was hardcoded twice, as "show active" in one pane
  and showAdminTab('general') in the sidebar. Splitting that pane left
  Admin Settings with no active pane at all. Every pane now renders
  {% if admin_landing_tab == '<id>' %} show active{% endif %}, fed by
  get_landing_tab_id(), and the sidebar reads the first rendered tab.

Four test files hardcoded 'general' or 'security'. They now assert the
durable properties instead: Latest Features is last, exactly one pane
resolves active, every tab renders exactly one pane.

Navigation is now 13 groups / 31 tabs / 88 sections.

Verified
  field contract   462 names / 110 card ids, unchanged
  regression set   32 failures, identical to baseline
  jinja compile    33/33 admin templates
  xss sinks        41 changed files pass
  render check     exactly one pane resolves active, and it is the
                   first tab of the first group

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Search & Extract held eight cards spanning four unrelated jobs, from
Bing consent to voice transcription. Workspaces mixed workspace types
with file rules, workflow and extraction settings.

Knowledge
  Web & Research       web search, URL access, deep research
  Search Index         Azure AI Search
  Document Extraction  document intelligence, chunk sizes, metadata
                       extraction and multi-modal vision (from
                       workspaces)
  Audio & Video        video intelligence, voice conversations
  File Sync            unchanged

  Voice and video sit under Knowledge rather than Chat because they
  are extraction pipelines, not conversation features.

Workspaces
  Workspace Types      personal, group, public
  Files & Sharing      downloads, sharing, shared conversation file
                       approvals (from ai-models)
  Global Identities    unchanged

Workflow is now its own group. It drives approvals and assignment
across every workspace type and was too large to sit as one card.

Navigation is now 14 groups / 36 tabs / 88 sections.

Bug found and fixed by doing this
  The Group Workflow Assignment modal sat between two cards, so it
  followed file-download-settings-section into Files & Sharing while
  its trigger button went to Workflow. An inactive tab pane is hidden,
  so the dialog would never have opened.

  test_admin_settings_modal_placement.py now checks every modal lives
  in a pane that can open it, and that modal ids are unique. Verified
  against a deliberately stranded modal: the test fails with exactly
  that diagnosis.

Also corrected two section comments that had drifted onto the wrong
cards in ai-models, labelling embeddings as processing thoughts.

AI Models and Agents are deliberately not in this change:
gpt-configuration is not a top-level card, it lives inside
legacyModelSettingsModal, so splitting that pane means moving a modal
rather than a card and deserves its own review.

Verified
  field contract   462 names / 110 card ids, unchanged
  regression set   32 failures, identical to baseline
  jinja compile    38/38 admin templates
  xss sinks        pass
  modal placement  48 modals, all reachable, all ids unique

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Stage D part 3: dismantle General and Security
Stage D part 4: Knowledge, Workspaces and Workflow
The last two catch-all tabs. Both needed modal surgery rather than
plain card moves, which is why they were held back.

AI Models
  Model Endpoints    multi-endpoint-configuration, and the legacy
                     model settings modal that holds the Chat Model
                     card and is opened from it
  Embeddings         embeddings-configuration
  Image Generation   image-generation-configuration

Agents & Actions
  Agents        agents-configuration, agent-template-approvals-section
  Actions       document-action-capabilities-card, actions-configuration
  Inbound MCP   inbound-mcp-configuration and its three dialogs

Navigation is now 14 groups / 40 tabs / 88 sections.

Three splitting problems, and how each was solved

  gpt-configuration is not a top-level card. It lives inside
  legacyModelSettingsModal. The modal was relocated to sit directly
  after multi-endpoint-configuration, the card holding its trigger,
  and both were then assigned to the same tab so the modal's opening
  lines, the nested card and the closing lines landed together and it
  reassembled intact.

  legacyModelDiscoveryIdentityGuideModal is opened from endpoints,
  embeddings and image generation, which now sit in three different
  tabs, so it could only ever have opened from one of them. It moved
  to the shell, outside every pane. Checked first that it carries no
  name= attribute, since moving a field outside the form would change
  the save payload. It carries none.

  The inbound MCP block is a {% if mcp_ui_enabled %} conditional
  spanning a card and three modals. It was lifted out whole into its
  own pane rather than split.

Tab-level conditions

  The nav map now supports condition on a tab, not just a section, so
  a tab whose entire pane is behind a feature flag disappears instead
  of rendering empty. Both renderers honour it.
  Verified: 40 tabs with mcp_ui_enabled, 39 without.

Also removed seven dangling section comments left at the tail of panes,
each labelling a card that had moved to another tab.

Two location tests asserted document order that only meant something
while everything shared one tab. They now assert the real invariant:
the embeddings and image cards are not inside the legacy modal, and
the document action card leads the tab it actually lives in.

Verified
  field contract   462 names / 110 card ids, unchanged
  regression set   32 failures, identical to baseline
  jinja compile    42/42 admin templates
  xss sinks        pass
  modal placement  all modals reachable, all ids unique

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Stage D part 5: AI Models and Agents & Actions
Paul Lizer (paullizer) and others added 7 commits August 19, 2026 17:09
One tab carried the entire backup, migration, restore, Cosmos editing
and job history surface: 1,622 lines in a single scroll.

  Backup          readiness, backup, schedule, storage, encryption
  Migrate         migration workflow
  Restore         backup inventory and restore
  Cosmos Editor   direct database editor
  Jobs            job history

Navigation is now 14 groups / 44 tabs / 88 sections. Stage D is done,
from an original 17 flat tabs.

Three problems the existing tools could not handle

  The migration card is a <section>, not a <div>, so every div
  balancing helper walked straight past it and the tools reported five
  top level cards where there were six. Its boundaries were found by
  balancing <section> instead, and the split was driven from explicit
  ranges with a check that every one of the 1,183 lines lands in
  exactly one output.

  Eleven dialogs, six of them opened from JavaScript rather than a
  button, serving what became five different tabs. A dialog inside a
  tab pane cannot appear while another tab is showing, so all eleven
  moved outside the panes. Checked first that none carried a name=
  attribute: the pane's twelve form fields are all radio groups inside
  the migration card, which stayed put.

  One save button, one status line and one operational warning serve
  all five tabs.

Group-shared regions

  Shared controls cannot be copied into each pane, because that repeats
  element ids and the JavaScript module would bind to the wrong one,
  and cannot live in one pane, because an inactive pane is hidden and
  the other four tabs would lose the save button.

  They now sit outside the panes marked
  data-admin-group-shared="backup-recovery" and are revealed only while
  that group is active, synced from every path that activates a tab
  including Bootstrap's own shown.bs.tab event.

  syncAdminGroupSharedRegions resolves the owning group from either
  navigation. Reading only the top tab strip was a real bug: that strip
  is not rendered at all in the sidebar layout, so the save button
  would have been hidden permanently. Verified against a simulated DOM
  in both layouts.

Verified
  field contract     462 names / 110 card ids, unchanged
  regression set     32 failures, identical to baseline
  jinja compile      46/46 admin templates
  xss sinks          pass
  element ids        all 1,608 ids in the composed template are unique
  modal placement    every dialog reachable from its trigger

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
system-settings-section mixed five unrelated concerns under one
heading. Each moved to the tab that owns it, with the field markup
carried across byte for byte.

  max_file_size_mb            -> Workspaces / Files & Sharing
  conversation_history_limit  -> Chat / Chat Experience
  default_system_prompt       -> Chat / Chat Experience
  access_denied_message       -> Security / Access & Roles
  idle timeout (4 fields)     stays in Security / Session, and the card
                              is renamed idle-timeout-section for what
                              it now holds

Splitting a card changes card ids but not field names, which is why
the field contract passed unchanged. Card ids are structural; field
names are the breaking surface, and the contract test tracks only
those, deliberately.

App Role Requirements roster

  Ten settings across seven tabs can each require an Entra app role,
  so the overall access policy could not be read without hunting
  through the whole of Admin Settings. Access & Roles now lists every
  one of them.

  The roster is built at runtime from
  input[name^="require_member_of_"] rather than from a list, so a new
  role requirement added anywhere appears automatically and this can
  never fall out of step.

  Each row is a mirror carrying no name attribute, following the
  existing proxy convention, so the setting is still posted exactly
  once. Sync is two way: the mirror drives the canonical input and
  dispatches a change event, and the canonical input updates the
  mirror when changed on its own tab. Verified against a simulated
  DOM.

A structural guard worth keeping

  test_every_pane_partial_is_balanced checks <div> and <section>
  balance in every pane. An unbalanced pane does not fail to render,
  it silently nests the panes that follow it, so the failure surfaces
  somewhere unrelated. Found the hard way here: a stray </div> in
  access-roles.html made a Send Feedback test fail. The new test names
  the offending file directly, and is verified against a deliberately
  broken pane.

The parity test also caught the new roster being listed in the nav map
in a different order from the markup. Nav order must match pane order.

Navigation is now 14 groups / 44 tabs / 93 sections.

Verified
  field contract   462 names, unchanged
  regression set   32 failures, identical to baseline
  jinja compile    46/46 admin templates
  pane balance     all panes balanced
  xss sinks        pass

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Stage D part 6: Backup & Recovery, completing Stage D
Stage E: split the mixed System Settings card, add the app role roster
Six workflows were gated on branches: [Development] only, so pull
requests into the Admin Settings staging branch ran nothing but
enforce-branch-flow. The staging branch was added to each filter so
every stage was properly checked.

That work is finished, so the filters go back to what they were. The
workflow files are now byte identical to Development.

The final staging to Development pull request runs all gates because
it targets Development.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Revert the temporary CI branch filters
Development moved on while the Admin Settings rework was staged, so
the final pull request conflicted. Two files needed resolving.

  config.py       Both branches independently reached 0.260.011.
                  Bumped to 0.260.018 so the merged result is clearly
                  ahead of both.

  release_notes   Both branches wrote a v0.260.011 section from
                  parallel work. Their entries are folded into the one
                  section rather than leaving two headings for the
                  same version.

Re-verified after the merge

  field names      452 on Development, 452 here, none lost, none added
  regression set   32 failures, identical to baseline
  jinja compile    46/46 admin templates
  contract suite   43 tests pass

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) and others added 2 commits August 19, 2026 18:04
Two places still navigated by naming a tab id, and the information
architecture rework broke both.

The setup walkthrough

  Each of twelve steps mapped to a tab id. Eleven named tabs that no
  longer exist, so those steps would have moved nowhere and left the
  admin looking at whatever was already on screen.

  Each step already knew which card it was about, so the tab id was a
  second copy of the same knowledge, free to go stale. Steps now name
  the card and openAdminCard finds the owning tab from the page.
  scrollToRelevantSection became redundant and is deleted, along with
  the old map.

  Steps 2 and 3 previously pointed at gpt-configuration, which lives
  inside the legacy model settings dialog and so cannot be scrolled to
  while that dialog is closed. They now point at
  multi-endpoint-configuration, the visible card in the same tab and
  the modern place to configure models.

Cosmos throughput validation

  Clicked scale-tab to reveal an invalid field. scale is a group now,
  not a tab, so the field needing attention could be left on a hidden
  tab with no indication of where to look. It now walks up from the
  invalid field to its own card, so it goes wherever that field
  actually lives.

test_admin_settings_walkthrough_targets.py asserts every step points
at an element that exists, that no tab id is hardcoded, and that step
numbering has no gaps.

A scan confirms no stale '-tab' literal remains in executable admin
JavaScript.

The rule this rework converged on: never name a tab, name the setting
and resolve the tab from the page.

Verified
  regression set   32 failures, identical to baseline
  admin suite      46 tests pass
  jinja compile    46/46 admin templates

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Merge Development into staging, and stop navigating by tab id
@paullizer
Paul Lizer (paullizer) merged commit eaf4cdb into Development Aug 20, 2026
11 of 12 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