+ {% for admin_section in admin_tab.sections %}
+ {% if not admin_section.condition
+ or (admin_section.condition == 'enable_agent_template_gallery' and app_settings.enable_agent_template_gallery)
+ or (admin_section.condition == 'mcp_ui_enabled' and mcp_ui_enabled) %}
+
{% set nav_layout = user_settings.get('settings', {}).get('navLayout') %}
{% if not (nav_layout == 'sidebar' or (not nav_layout and app_settings.enable_left_nav_default)) %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {% for admin_group in admin_nav %}
-
-
-
-
- Agents
-
-
-
-
-
- Scale
-
-
-
-
-
- Control Center
-
-
-
-
-
- Backup, Migrate & Restore
-
-
-
-
-
- Governance
-
-
-
-
-
- Logging
-
-
-
-
- Send Feedback
-
-
-
-
-
- Latest Features New
+ {% endfor %}
+
+
+ {% for admin_group in admin_nav %}
+ {% set group_is_first = loop.first %}
+ {% for admin_tab in admin_group.tabs %}
+ {% set is_default_tab = group_is_first and loop.first %}
+
+
+ {{ admin_tab.label }}
+ {% if admin_tab.render == 'latest_features' %}
+ New
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
{% endif %}
diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md
index a1d4772bc..3811e8f15 100644
--- a/docs/explanation/release_notes.md
+++ b/docs/explanation/release_notes.md
@@ -2,6 +2,29 @@
For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/).
+### **(v0.260.010)**
+
+#### New Features
+
+* **Admin Settings Navigation Is Now Grouped**
+ * Admin Settings presented 18 tabs in one flat list. Related tabs are now collected under 12 groups such as Appearance, Knowledge, Security and Operations, so the list is scannable and has room to grow.
+ * In the sidebar, groups are collapsible and remember whether you left them open. In the tab layout, a row of group pills filters the tab strip to one group at a time.
+ * Opening a tab always reveals its group first, so a deep link or a cross-reference can never land you on a pane whose tab is hidden.
+ * Sidebar search now matches group names as well as tab and setting names, and expands whatever it needs to show a result.
+ * No settings moved in this release. Every tab keeps its contents; only the navigation around them changed.
+ * (Ref: `admin_settings_nav.py`, `_sidebar_nav.html`, `admin_settings.html`, `admin_sidebar_nav.js`)
+
+#### Bug Fixes
+
+* **Shared Conversation File Approvals Is Reachable From The Sidebar**
+ * The Shared Conversation File Approvals card had no navigation entry, so it could only be found by scrolling the AI Models tab. It is now listed like every other setting.
+ * (Ref: `shared-conversation-file-approvals-section`, navigation map)
+
+* **Navigation Labels And Order Can No Longer Drift**
+ * The tab strip and the sidebar each maintained the same structure by hand and had diverged: tab order differed between them, and Agents, Custom Pages and Search and Extract each showed a different name depending on which navigation you used.
+ * Both now render from one definition, so a change is made once and appears in both.
+ * (Ref: `admin_settings_nav.py`, `test_admin_settings_nav_map.py`)
+
### **(v0.260.009)**
#### New Features
diff --git a/functional_tests/test_admin_latest_features_tab.py b/functional_tests/test_admin_latest_features_tab.py
index 6f947d4ac..322bfea1a 100644
--- a/functional_tests/test_admin_latest_features_tab.py
+++ b/functional_tests/test_admin_latest_features_tab.py
@@ -10,6 +10,7 @@
focused on features users can see and control.
"""
+import re
import importlib.util
import os
import sys
@@ -17,6 +18,7 @@
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from test_support.templates import resolve_template_includes
+from test_support.nav import get_group_for_tab, get_tab_ids
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -248,8 +250,6 @@ def test_latest_features_template_structure():
template_content = read_text(ADMIN_TEMPLATE)
required_markers = [
- 'id="latest-features-tab"',
- 'data-bs-target="#latest-features"',
'id="latest-features"',
'admin_latest_feature_release_groups',
'{% for release_group in admin_latest_feature_release_groups %}',
@@ -336,52 +336,71 @@ def test_latest_features_sidebar_navigation():
raise AssertionError(f'Missing Latest Features sidebar markers: {missing_markers}')
latest_features_index = sidebar_content.index('data-tab="latest-features"')
- general_index = sidebar_content.index('data-tab="general"')
- send_feedback_index = sidebar_content.index('data-tab="send-feedback"')
- assert latest_features_index > general_index, 'Latest Features should appear after General in the admin sidebar'
- assert latest_features_index > send_feedback_index, 'Latest Features should be the last admin sidebar destination, after Send Feedback'
assert 'New' in sidebar_content, 'Sidebar Latest Features item should include a New badge'
+ # Tab order is defined once in the nav map, which the sidebar renders from,
+ # so ordering is asserted there rather than against the rendered markup.
+ tab_ids = get_tab_ids()
+ assert tab_ids.index('latest-features') > tab_ids.index('general'), 'Latest Features should appear after General'
+ assert tab_ids.index('latest-features') > tab_ids.index('send-feedback'), 'Latest Features should be the last destination, after Send Feedback'
+
print('Latest Features sidebar navigation is present')
return True
def test_latest_features_top_nav_priority():
- """Latest Features should be the last top-nav tab and never default active."""
- print('Testing Latest Features top-nav placement...')
+ """Latest Features should be the last tab and never default active."""
+ print('Testing Latest Features navigation placement...')
template_content = read_text(ADMIN_TEMPLATE)
-
- latest_features_tab_index = template_content.index('id="latest-features-tab"')
- general_tab_index = template_content.index('id="general-tab"')
- send_feedback_tab_index = template_content.index('id="send-feedback-tab"')
- assert latest_features_tab_index > general_tab_index, 'Latest Features tab should appear after General in top nav'
- assert latest_features_tab_index > send_feedback_tab_index, 'Latest Features tab should be the last top-nav tab, after Send Feedback'
-
- assert 'id="latest-features-tab" data-bs-toggle="tab" data-bs-target="#latest-features"' in template_content, 'Latest Features top-nav tab missing'
- assert 'Latest Features New' in template_content, 'Latest Features top-nav tab should include a New badge'
+ tab_ids = get_tab_ids()
+
+ # Navigation order now comes from the nav map, which both the top tab strip
+ # and the sidebar render from, so order is asserted there rather than
+ # against either rendering.
+ assert tab_ids[-1] == 'latest-features', (
+ 'Latest Features should be the last tab in the navigation map, '
+ f'got {tab_ids[-1]}'
+ )
+ assert tab_ids.index('latest-features') > tab_ids.index('send-feedback'), (
+ 'Latest Features should come after Send Feedback'
+ )
+
+ group = get_group_for_tab('latest-features')
+ assert group is not None and group['id'] == 'help', (
+ f"Latest Features should sit in the Help group, got {group}"
+ )
# Latest Features opened on every visit to Admin Settings, which is why it
- # now sits last and General is the landing tab instead.
+ # is pinned last and General is the landing tab instead.
assert 'class="tab-pane fade" id="latest-features" role="tabpanel" aria-labelledby="latest-features-tab"' in template_content, 'Latest Features pane should not be the default active tab'
assert 'class="tab-pane fade show active" id="general" role="tabpanel" aria-labelledby="general-tab"' in template_content, 'General pane should be the default active tab'
+ assert tab_ids[0] == 'general', 'General should be the first tab in the navigation map'
- print('Latest Features is last in top navigation and not default active')
+ print('Latest Features is last in navigation and not default active')
return True
def test_admin_settings_tab_uniqueness():
- """Admin settings template should not contain duplicate Security tab controls or extra active panes."""
+ """Every tab must appear exactly once, with exactly one default pane."""
print('Testing admin settings tab uniqueness...')
template_content = read_text(ADMIN_TEMPLATE)
normalized_template = ''.join(template_content.split())
+ tab_ids = get_tab_ids()
+
+ duplicates = sorted({t for t in tab_ids if tab_ids.count(t) > 1})
+ assert not duplicates, f'Tabs listed more than once in the nav map: {duplicates}'
- assert template_content.count('id="security-tab"') == 1, 'Security tab button should appear exactly once'
assert template_content.count('id="security" role="tabpanel"') == 1, 'Security tab pane should appear exactly once'
- assert template_content.count('tab-pane fade show active') == 1, 'Only one tab pane should be marked show active in top-nav markup'
+ assert template_content.count('tab-pane fade show active') == 1, 'Only one tab pane should be marked show active'
assert 'Managesecuritysettingsforkeyvaultandothersecurityconfigurations.
' in normalized_template, 'Security intro paragraph should be properly closed'
+ # Every tab in the map must have a pane to activate.
+ panes = set(re.findall(r'
1})}"
+ )
+
+ for group in ADMIN_NAV:
+ for key in ("id", "label", "icon", "tabs"):
+ assert key in group, f"Group {group.get('id')} is missing '{key}'"
+ assert group["tabs"], f"Group {group['id']} has no tabs"
+
+ tab_ids = get_tab_ids()
+ duplicate_tabs = sorted({t for t in tab_ids if tab_ids.count(t) > 1})
+ assert not duplicate_tabs, f"Tabs listed in more than one group: {duplicate_tabs}"
+
+ section_ids = get_section_ids()
+ duplicate_sections = sorted({s for s in section_ids if section_ids.count(s) > 1})
+ assert not duplicate_sections, (
+ f"Sections listed under more than one tab: {duplicate_sections}"
+ )
+
+ for _, tab in iter_tabs():
+ for key in ("id", "label", "icon", "sections"):
+ assert key in tab, f"Tab {tab.get('id')} is missing '{key}'"
+
+ print(
+ f"Map is well formed: {len(ADMIN_NAV)} groups, "
+ f"{len(tab_ids)} tabs, {len(section_ids)} sections."
+ )
+
+
+def test_every_nav_destination_exists():
+ """A destination with nothing behind it is a dead end for the admin."""
+ print("Testing Admin Settings nav destinations...")
+
+ composed = read_admin_settings_template()
+ panes = set(re.findall(r'
Target Cosmos' not in sidebar
- assert 'Migration' in sidebar
- assert 'Backup, Migrate & Restore' in sidebar
+ # Sidebar labels now come from the navigation map, which both the sidebar
+ # and the top tab strip render from.
+ data_management_sections = {
+ section["label"]
+ for _, tab in iter_tabs()
+ if tab["id"] == "data-management"
+ for section in tab["sections"]
+ }
+ assert "Target Cosmos" not in data_management_sections
+ assert "Migration" in data_management_sections
+ assert any(
+ tab["id"] == "data-management" and tab["label"] == "Backup, Migrate & Restore"
+ for _, tab in iter_tabs()
+ ), "Data Management tab label missing from the navigation map"
assert 'cdn.jsdelivr.net' not in read_text(ADMIN_JS)
- assert 'data-tab="data-management"' in sidebar
- assert 'data-section="data-management-readiness-section"' in sidebar
- assert 'data-section="data-management-backup-section"' in sidebar
- assert 'data-section="data-management-cosmos-editor-section"' in sidebar
- assert 'data-section="data-management-backup-inventory-section"' in sidebar
- assert 'data-section="data-management-migration-section"' in sidebar
+ assert any(tab["id"] == "data-management" for _, tab in iter_tabs()), (
+ "Data Management tab missing from the navigation map"
+ )
+ data_management_section_ids = {
+ section["id"]
+ for _, tab in iter_tabs()
+ if tab["id"] == "data-management"
+ for section in tab["sections"]
+ }
+ for expected_section in (
+ "data-management-readiness-section",
+ "data-management-backup-section",
+ "data-management-cosmos-editor-section",
+ "data-management-backup-inventory-section",
+ "data-management-migration-section",
+ ):
+ assert expected_section in data_management_section_ids, (
+ f"Navigation map is missing {expected_section}"
+ )
if __name__ == "__main__":
diff --git a/functional_tests/test_inbound_mcp_admin_ui.py b/functional_tests/test_inbound_mcp_admin_ui.py
index 2717e5751..4dafbf5f7 100644
--- a/functional_tests/test_inbound_mcp_admin_ui.py
+++ b/functional_tests/test_inbound_mcp_admin_ui.py
@@ -39,7 +39,9 @@
import sys
from pathlib import Path
from test_support.versioning import assert_app_version_at_least
+from test_support.nav import iter_tabs
from test_support.templates import compose_if_admin_settings
+from test_support.nav import iter_tabs
ROOT_DIR = Path(__file__).resolve().parents[1]
@@ -177,9 +179,26 @@ def test_admin_settings_mcp_ui_is_gated_and_minimal():
assert f"'{operational_setting}'," in admin_route_source
assert f"INBOUND_MCP_SETTINGS_DEFAULTS['{operational_setting}']" in admin_route_source
- assert "{% if mcp_ui_enabled %}" in sidebar_template
- assert 'data-section="inbound-mcp-configuration"' in sidebar_template
- assert "Inbound MCP" in sidebar_template
+ # The Inbound MCP navigation entry is gated by the same flag as its card,
+ # declared in the navigation map rather than inline in the sidebar.
+ inbound_section = next(
+ (
+ section
+ for _, tab in iter_tabs()
+ for section in tab["sections"]
+ if section["id"] == "inbound-mcp-configuration"
+ ),
+ None,
+ )
+ assert inbound_section is not None, (
+ "Inbound MCP navigation entry missing from the navigation map"
+ )
+ assert inbound_section.get("condition") == "mcp_ui_enabled", (
+ "Inbound MCP navigation entry must be gated by mcp_ui_enabled"
+ )
+ assert inbound_section["label"] == "Inbound MCP", (
+ f"Unexpected Inbound MCP navigation label: {inbound_section['label']}"
+ )
def test_inbound_mcp_auth_uses_settings_runtime_config():
diff --git a/functional_tests/test_support/nav.py b/functional_tests/test_support/nav.py
new file mode 100644
index 000000000..ed847b2ef
--- /dev/null
+++ b/functional_tests/test_support/nav.py
@@ -0,0 +1,34 @@
+# nav.py
+"""Access to the Admin Settings navigation map from functional tests.
+
+``admin_settings_nav`` lives in ``application/single_app``, which is not on the
+test path by default. Centralising the import here keeps individual tests from
+each manipulating ``sys.path``, and gives them one place to read navigation
+structure from.
+"""
+
+import sys
+from pathlib import Path
+
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+APP_ROOT = REPO_ROOT / "application" / "single_app"
+
+if str(APP_ROOT) not in sys.path:
+ sys.path.insert(0, str(APP_ROOT))
+
+from admin_settings_nav import ( # noqa: E402
+ ADMIN_NAV,
+ get_group_for_tab,
+ get_section_ids,
+ get_tab_ids,
+ iter_tabs,
+)
+
+__all__ = [
+ "ADMIN_NAV",
+ "get_group_for_tab",
+ "get_section_ids",
+ "get_tab_ids",
+ "iter_tabs",
+]
diff --git a/functional_tests/test_support_menu_user_feature.py b/functional_tests/test_support_menu_user_feature.py
index 236a26d0e..1d653c895 100644
--- a/functional_tests/test_support_menu_user_feature.py
+++ b/functional_tests/test_support_menu_user_feature.py
@@ -16,6 +16,7 @@
import sys
import importlib.util
from test_support.templates import compose_if_admin_settings
+from test_support.nav import iter_tabs
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -261,7 +262,6 @@ def test_support_menu_navigation_and_routes():
support_route_content = read_text(SUPPORT_ROUTE)
sidebar_markers = [
- 'data-section="support-menu-section"',
'id="support-menu-toggle"',
"url_for('frontend_support.support_latest_features')",
"url_for('frontend_support.support_send_feedback')",
@@ -270,6 +270,13 @@ def test_support_menu_navigation_and_routes():
missing_sidebar = [marker for marker in sidebar_markers if marker not in sidebar_content]
assert not missing_sidebar, f'Missing support menu sidebar markers: {missing_sidebar}'
+ # The admin destination for the Support card comes from the navigation map.
+ assert any(
+ section["id"] == "support-menu-section"
+ for _, tab in iter_tabs()
+ for section in tab["sections"]
+ ), 'Support menu admin destination missing from the navigation map'
+
assert sidebar_content.index('id="support-menu-toggle"') < sidebar_content.index('id="external-links-toggle"'), 'Support menu should render before external links in the sidebar'
short_sidebar_markers = [
diff --git a/functional_tests/test_workflow_access_controls.py b/functional_tests/test_workflow_access_controls.py
index 544e858da..3c455da17 100644
--- a/functional_tests/test_workflow_access_controls.py
+++ b/functional_tests/test_workflow_access_controls.py
@@ -12,7 +12,9 @@
import json
from pathlib import Path
from test_support.versioning import assert_app_version_at_least
+from test_support.nav import iter_tabs
from test_support.templates import compose_if_admin_settings
+from test_support.nav import iter_tabs
ROOT = Path(__file__).resolve().parents[1]
@@ -118,9 +120,11 @@ def test_workflow_access_control_wiring():
assert 'id="require_member_of_workflow_user"' in admin_template_content, (
"Expected admin WorkflowUser requirement toggle markup."
)
- assert 'data-section="workflow-settings-section"' in sidebar_template_content, (
- "Expected Admin Settings sidebar navigation to link to Workflow settings."
- )
+ assert any(
+ section["id"] == "workflow-settings-section"
+ for _, tab in iter_tabs()
+ for section in tab["sections"]
+ ), "Expected Admin Settings navigation to link to Workflow settings."
assert '{% if settings.allow_user_workflows %}' in workspace_template_content, (
"Expected workspace workflow tab and modals to be gated by effective workflow access."
From a809a71e34c437b3da84449481cd20616f49f432 Mon Sep 17 00:00:00 2001
From: Paul Lizer
Date: Wed, 19 Aug 2026 14:55:21 -0400
Subject: [PATCH 2/2] Update IA rework status for stage C
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_REWORK_STATUS.md | 35 ++++++++-----------
1 file changed, 15 insertions(+), 20 deletions(-)
diff --git a/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md b/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md
index 8addedc3e..7069ef7d2 100644
--- a/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md
+++ b/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md
@@ -15,13 +15,14 @@ and merged in one final reviewed PR.
```
Development ─── feature/admin-settings-ia ──┬── #1297 stage 1 MERGED
├── #1304 stage A+B MERGED
- ├── stage C next
+ ├── #1306 guardrail MERGED
+ ├── stage C in review
├── stage D
└── stage E
└──► final PR ──► Development
```
-Current version: **0.260.009**. Fingerprint: **462 field names / 110 card ids**.
+Current version: **0.260.010**. Fingerprint: **462 field names / 110 card ids**.
### Shipped
@@ -31,6 +32,7 @@ Current version: **0.260.009**. Fingerprint: **462 field names / 110 card ids**.
| A | 12 cross-tab links converted from tab-coupled `switchTab` to `data-admin-link` card targets; two were already broken; `switchTab` removed |
| B | `data-requires` dependency announcements with inline mirror and link; File Sync and Permissions wired |
| — | Form field contract enforced in CI |
+| C | Navigation moved into `admin_settings_nav.py` as groups → tabs → sections; both navigations render from it; group level with persisted collapse state and group pills; three-level search |
---
@@ -44,6 +46,7 @@ silently stops a setting from saving with no error anywhere.
|---|---|
| **Field contract** | `python -m pytest functional_tests/test_admin_settings_field_contract.py` |
| Regenerate baseline *(only for a deliberate removal)* | `python functional_tests/test_admin_settings_field_contract.py --update-baseline` |
+| **Navigation map** | `python -m pytest functional_tests/test_admin_settings_nav_map.py` |
| Composition contract | `python -m pytest functional_tests/test_admin_settings_template_composition.py` |
| Card links | `python -m pytest functional_tests/test_admin_card_links.py` |
| Dependencies | `python -m pytest functional_tests/test_admin_settings_dependencies.py` |
@@ -51,8 +54,8 @@ silently stops a setting from saving with no error anywhere.
| XSS | `python scripts/check_xss_sinks.py --full-file ` |
**Regression baseline:** the 75 functional test files that reference
-`admin_settings.html` sit at **33 pre-existing failures**. That number must not
-change. To reproduce the list:
+`admin_settings.html` now sit at **32 pre-existing failures**, down from 33
+after stage C fixed a missing navigation entry. That number must not increase. To reproduce the list:
```
cd functional_tests
@@ -76,25 +79,17 @@ template uncomposed while referencing a partial-backed card or field.
---
-## Next: Stage C
+## Next: Stage D — the risky one
-Add the group navigation level **against the current 18 tabs**, so grouping is
-proven before any card moves.
+Re-home cards into the 14 target groups, **one group per commit**, running the
+field contract test on each. The nav map now makes this a two-part edit: move
+the card markup between partials, and move its entry between tabs in
+`admin_settings_nav.py`. The parity test fails if the two disagree.
-1. Group row in the top-tab strip; collapsible group headers in
- `_sidebar_nav.html` with persisted state.
-2. Three-level sidebar search (group → tab → card).
-3. Legacy hash redirect map inside `showAdminTab`.
+The interim grouping in the map has 12 groups, because Workflow and Data
+Lifecycle have no tabs until their cards move out of Workspaces and Safety.
-A server-side nav map is worth introducing here, since both navs need to render
-the same group structure. Note it is **not** needed for link resolution —
-`admin_card_links.js` resolves card → tab from the DOM via
-`closest('.tab-pane')`, which is why links already survive any IA change.
-
-## Then Stage D — the risky one
-
-Re-home cards into the 14 groups, **one group per commit**, running the field
-contract test on each. Three complications found while splitting:
+Three complications found while splitting:
- **I1** Modals are interleaved *between* cards, not collected at the end. Each
modal moves with the tab that owns its trigger.