diff --git a/application/single_app/admin_settings_nav.py b/application/single_app/admin_settings_nav.py
index 48026e3ea..84229254c 100644
--- a/application/single_app/admin_settings_nav.py
+++ b/application/single_app/admin_settings_nav.py
@@ -66,6 +66,8 @@
{"id": "chat-file-uploads-section", "label": "Chat File Uploads", "icon": "bi-paperclip"},
{"id": "conversation-contents-drawer-section", "label": "Conversation Contents Drawer", "icon": "bi-list-nested"},
{"id": "workspace-scope-lock-section", "label": "Workspace Scope Lock", "icon": "bi-lock"},
+ {"id": "conversation-history-section", "label": "Conversation History", "icon": "bi-clock-history"},
+ {"id": "default-system-prompt-section", "label": "Default System Prompt", "icon": "bi-chat-square-quote"},
],
},
{
@@ -184,6 +186,7 @@
{"id": "file-download-settings-section", "label": "File Downloads", "icon": "bi-download"},
{"id": "file-sharing-section", "label": "File Sharing", "icon": "bi-share"},
{"id": "shared-conversation-file-approvals-section", "label": "Shared Conversation File Approvals", "icon": "bi-check2-square"},
+ {"id": "file-size-limit-section", "label": "Maximum File Size", "icon": "bi-file-earmark-arrow-up"},
],
},
{
@@ -284,6 +287,8 @@
"icon": "bi-person-check",
"sections": [
{"id": "permissions-section", "label": "Permissions", "icon": "bi-person-check"},
+ {"id": "app-role-requirements-section", "label": "App Role Requirements", "icon": "bi-person-badge"},
+ {"id": "access-denied-message-section", "label": "Access Denied Message", "icon": "bi-shield-x"},
],
},
{
@@ -303,14 +308,14 @@
],
},
{
- # Interim home for the mixed System Settings card. Four of its
- # eight fields are idle-timeout, which is the plurality; the
- # rest are split out to their real homes in a later change.
+ # Idle timeout is the only thing left here. The rest of the old
+ # System Settings card was split out to the tabs that own each
+ # setting, without renaming a single field.
"id": "session",
"label": "Session",
"icon": "bi-hourglass-split",
"sections": [
- {"id": "system-settings-section", "label": "System Settings", "icon": "bi-sliders"},
+ {"id": "idle-timeout-section", "label": "Idle Session Timeout", "icon": "bi-hourglass-split"},
],
},
{
@@ -395,18 +400,50 @@
"icon": "bi-database",
"tabs": [
{
- "id": "data-management",
- "label": "Backup, Migrate & Restore",
- "icon": "bi-database-check",
+ # Schedule, storage and encryption are cards nested inside the
+ # backup card, so they stay with it.
+ "id": "backup",
+ "label": "Backup",
+ "icon": "bi-archive",
"sections": [
{"id": "data-management-readiness-section", "label": "Start Here", "icon": "bi-compass"},
{"id": "data-management-backup-section", "label": "Backup", "icon": "bi-archive"},
{"id": "data-management-schedule-section", "label": "Schedule", "icon": "bi-calendar-event"},
{"id": "data-management-storage-section", "label": "Storage", "icon": "bi-hdd"},
{"id": "data-management-encryption-section", "label": "Encryption", "icon": "bi-key"},
+ ],
+ },
+ {
+ "id": "migrate",
+ "label": "Migrate",
+ "icon": "bi-arrow-left-right",
+ "sections": [
{"id": "data-management-migration-section", "label": "Migration", "icon": "bi-arrow-left-right"},
- {"id": "data-management-cosmos-editor-section", "label": "Cosmos Editor", "icon": "bi-database-exclamation"},
+ ],
+ },
+ {
+ "id": "restore",
+ "label": "Restore",
+ "icon": "bi-box-seam",
+ "sections": [
{"id": "data-management-backup-inventory-section", "label": "Backup Inventory & Restore", "icon": "bi-box-seam"},
+ ],
+ },
+ {
+ # A direct database editor. It is a repair tool that belongs
+ # with the backup and restore tooling it shares a module with.
+ "id": "cosmos-editor",
+ "label": "Cosmos Editor",
+ "icon": "bi-database-exclamation",
+ "sections": [
+ {"id": "data-management-cosmos-editor-section", "label": "Cosmos Editor", "icon": "bi-database-exclamation"},
+ ],
+ },
+ {
+ "id": "jobs",
+ "label": "Jobs",
+ "icon": "bi-clock-history",
+ "sections": [
{"id": "data-management-jobs-section", "label": "Jobs", "icon": "bi-clock-history"},
],
},
diff --git a/application/single_app/config.py b/application/single_app/config.py
index 27e50b825..f2d9bfa54 100644
--- a/application/single_app/config.py
+++ b/application/single_app/config.py
@@ -96,7 +96,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
-VERSION = "0.260.015"
+VERSION = "0.260.017"
IS_DEVELOPMENT = is_development_env_enabled()
SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
diff --git a/application/single_app/static/js/admin/admin_access_roles_roster.js b/application/single_app/static/js/admin/admin_access_roles_roster.js
new file mode 100644
index 000000000..3b7993b16
--- /dev/null
+++ b/application/single_app/static/js/admin/admin_access_roles_roster.js
@@ -0,0 +1,148 @@
+// admin_access_roles_roster.js
+//
+// Access & Roles shows every "require an Entra app role" switch in one place.
+//
+// The switches themselves stay on the tabs that own them, because that is where
+// they make sense in context. Duplicating the real inputs here would submit
+// each setting twice, so this builds a roster of mirrors instead: each row
+// carries no name attribute and simply drives the canonical input.
+//
+// The roster is built from the page rather than from a hand-written list, so a
+// new role requirement anywhere in Admin Settings appears here on its own and
+// this list cannot fall out of step with reality.
+//
+// Roster links carry data-admin-link, which admin_card_links.js already handles
+// through a delegated listener, so no wiring is needed here.
+
+const ROLE_INPUT_SELECTOR = 'input[type="checkbox"][name^="require_member_of_"]';
+const LIST_ID = 'app-role-requirements-list';
+const EMPTY_ID = 'app-role-requirements-empty';
+const ROSTER_CARD_ID = 'app-role-requirements-section';
+
+/**
+ * Read the visible label for a control, falling back to its field name.
+ * @param {HTMLInputElement} input Canonical role checkbox.
+ * @returns {string} Human readable label.
+ */
+function labelFor(input) {
+ const explicit = input.id ? document.querySelector(`label[for="${input.id}"]`) : null;
+ if (explicit && explicit.textContent.trim()) {
+ return explicit.textContent.trim();
+ }
+
+ const wrapping = input.closest('label');
+ if (wrapping && wrapping.textContent.trim()) {
+ return wrapping.textContent.trim();
+ }
+
+ return input.name;
+}
+
+/**
+ * Find the card a control belongs to, so the roster can link back to it.
+ * @param {HTMLInputElement} input Canonical role checkbox.
+ * @returns {HTMLElement|null} The owning card, when it has an id.
+ */
+function owningCard(input) {
+ let card = input.closest('.card[id]');
+ while (card && card.id === ROSTER_CARD_ID) {
+ card = card.parentElement ? card.parentElement.closest('.card[id]') : null;
+ }
+ return card;
+}
+
+/**
+ * Read the heading of a card, used as the "where does this live" hint.
+ * @param {HTMLElement} card Owning card.
+ * @returns {string} Card title, or an empty string when it has none.
+ */
+function cardTitle(card) {
+ const heading = card ? card.querySelector('h5, h4, h6, .card-title') : null;
+ return heading ? heading.textContent.trim() : '';
+}
+
+/**
+ * Build one roster row: a mirror switch, its label, and a link to the setting.
+ * @param {HTMLInputElement} input Canonical role checkbox.
+ * @returns {HTMLElement} The row element.
+ */
+function buildRow(input) {
+ const row = document.createElement('div');
+ row.className = 'd-flex flex-wrap align-items-center gap-2';
+ row.setAttribute('data-role-requirement-row', input.name);
+
+ const wrapper = document.createElement('div');
+ wrapper.className = 'form-check form-switch mb-0 flex-grow-1';
+
+ // No name attribute: only the canonical input is submitted with the form.
+ const mirror = document.createElement('input');
+ mirror.type = 'checkbox';
+ mirror.className = 'form-check-input';
+ mirror.id = `${input.name}-roster-mirror`;
+ mirror.checked = input.checked;
+ mirror.disabled = input.disabled;
+ mirror.setAttribute('data-role-mirror-for', input.id || input.name);
+ mirror.setAttribute('data-ignore-settings-change', 'true');
+
+ const label = document.createElement('label');
+ label.className = 'form-check-label ms-2';
+ label.setAttribute('for', mirror.id);
+ label.textContent = labelFor(input);
+
+ wrapper.append(mirror, label);
+ row.appendChild(wrapper);
+
+ const card = owningCard(input);
+ if (card) {
+ const title = cardTitle(card);
+ const link = document.createElement('a');
+ link.href = `#${card.id}`;
+ link.className = 'small text-nowrap';
+ link.setAttribute('data-admin-link', card.id);
+ link.textContent = title ? `In ${title}` : 'Go to setting';
+ row.appendChild(link);
+ }
+
+ // Two-way: the mirror drives the real input, and the real input keeps the
+ // mirror honest when it is changed on its own tab.
+ mirror.addEventListener('change', () => {
+ if (input.checked === mirror.checked) {
+ return;
+ }
+ input.checked = mirror.checked;
+ input.dispatchEvent(new Event('change', { bubbles: true }));
+ });
+
+ input.addEventListener('change', () => {
+ mirror.checked = input.checked;
+ mirror.disabled = input.disabled;
+ });
+
+ return row;
+}
+
+/**
+ * Populate the Access & Roles roster from the role switches on the page.
+ */
+export function initAdminAccessRolesRoster() {
+ const list = document.getElementById(LIST_ID);
+ if (!list) {
+ return;
+ }
+
+ const empty = document.getElementById(EMPTY_ID);
+ const inputs = Array.from(document.querySelectorAll(ROLE_INPUT_SELECTOR))
+ .filter(input => !list.contains(input));
+
+ list.replaceChildren();
+ inputs
+ .map(input => ({ input, label: labelFor(input) }))
+ .sort((a, b) => a.label.localeCompare(b.label))
+ .forEach(({ input }) => list.appendChild(buildRow(input)));
+
+ if (empty) {
+ empty.classList.toggle('d-none', inputs.length > 0);
+ }
+}
+
+document.addEventListener('DOMContentLoaded', initAdminAccessRolesRoster);
diff --git a/application/single_app/static/js/admin/admin_sidebar_nav.js b/application/single_app/static/js/admin/admin_sidebar_nav.js
index 1bf0e5e53..b452447c9 100644
--- a/application/single_app/static/js/admin/admin_sidebar_nav.js
+++ b/application/single_app/static/js/admin/admin_sidebar_nav.js
@@ -157,7 +157,19 @@ function initAdminSidebarNav() {
}
} else {
console.log('initAdminSidebarNav - Found existing active tab, preserving current state:', activeTab.getAttribute('data-tab'));
+ syncAdminGroupSharedRegions(activeTab.getAttribute('data-tab'));
}
+
+ // Clicking a tab button directly does not go through showAdminTab, so the
+ // shared regions are synced from Bootstrap's own event as well.
+ document.querySelectorAll('button.nav-link[data-bs-target^="#"]').forEach(button => {
+ button.addEventListener('shown.bs.tab', event => {
+ const target = event.target.getAttribute('data-bs-target');
+ if (target) {
+ syncAdminGroupSharedRegions(target.slice(1));
+ }
+ });
+ });
}
function setupAdminGroupToggles() {
@@ -297,12 +309,43 @@ const LEGACY_TAB_REDIRECTS = {
'workspaces': 'workspace-types',
'search-extract': 'web-research',
'ai-models': 'model-endpoints',
+ 'data-management': 'backup',
};
function resolveAdminTabId(tabId) {
return LEGACY_TAB_REDIRECTS[tabId] || tabId;
}
+/**
+ * Some groups share one set of controls across all of their tabs, such as the
+ * single save button that serves every Backup & Recovery tab. Those controls
+ * cannot be duplicated into each pane without repeating element ids, and they
+ * cannot sit in one pane because the other tabs would lose them, so they live
+ * outside the panes and are revealed only while their group is active.
+ */
+function syncAdminGroupSharedRegions(tabId) {
+ const regions = document.querySelectorAll('[data-admin-group-shared]');
+ if (!regions.length) {
+ return;
+ }
+
+ // Only one of the two navigations is rendered at a time, so resolve the
+ // owning group from whichever is present. Looking only at the top tab strip
+ // would leave the region hidden for good in the sidebar layout.
+ const tabButton = document.querySelector(`.admin-tab-item[data-admin-group] button[data-bs-target="#${tabId}"]`);
+ let owner = tabButton ? tabButton.closest('[data-admin-group]') : null;
+ if (!owner) {
+ const sidebarLink = document.querySelector(`.admin-nav-tab[data-tab="${tabId}"]`);
+ owner = sidebarLink ? sidebarLink.closest('[data-admin-group]') : null;
+ }
+ const activeGroup = owner ? owner.getAttribute('data-admin-group') : null;
+
+ regions.forEach(region => {
+ const ownerGroup = region.getAttribute('data-admin-group-shared');
+ region.hidden = ownerGroup !== activeGroup;
+ });
+}
+
function showAdminTab(requestedTabId) {
const tabId = resolveAdminTabId(requestedTabId);
@@ -333,6 +376,7 @@ function showAdminTab(requestedTabId) {
// Update the hash in URL for deep linking
window.location.hash = tabId;
+ syncAdminGroupSharedRegions(tabId);
if (typeof window.updateAdminSettingsSaveButtonState === 'function') {
window.updateAdminSettingsSaveButtonState();
}
diff --git a/application/single_app/templates/admin/_panes/access-roles.html b/application/single_app/templates/admin/_panes/access-roles.html
index 3e93d9ff7..e87ef059d 100644
--- a/application/single_app/templates/admin/_panes/access-roles.html
+++ b/application/single_app/templates/admin/_panes/access-roles.html
@@ -43,7 +43,37 @@
+
+
+ App Role Requirements
+
+
+ Every setting that can require an Entra app role, gathered here so the
+ full access policy can be read in one place. Each switch is a mirror of
+ the setting on its own tab, so changing it here changes it there.
+
+ {# Rows are built from the page itself by
+ admin_access_roles_roster.js, so a new role requirement appears here
+ automatically and this list can never fall out of step. #}
+
+
+ No app role requirements are available on this page.
+
+
-
+
+
+ Access Denied Message
+
+
+ Shown to a user whose account does not carry a required role.
+
+
+
+ Shown to signed-in users who lack the required roles. Use Enter for line breaks.
+
+
+
diff --git a/application/single_app/templates/admin/_panes/backup.html b/application/single_app/templates/admin/_panes/backup.html
new file mode 100644
index 000000000..a00542658
--- /dev/null
+++ b/application/single_app/templates/admin/_panes/backup.html
@@ -0,0 +1,323 @@
+
+
+
+
+
Start Here
+
Use these checkpoints before running backup, migration, restore, or advanced repair actions.
+
+
+
+
+
+
+
Back up
+
Configure dedicated storage, encryption, schedule, and backup scope before queueing jobs.
+
+
+
+
+
+
Migrate
+
Connect a destination, choose who moves, run preflight, then execute a recoverable transfer.
+
+
+
+
+
+
Restore
+
Review backup readiness and stage restore decisions from Backup Inventory.
+
+
+
+
+
+
RU Boost
+
Temporarily raise eligible Cosmos capacity during approved backup or migration windows.
+
+
+
+
+
+
+
+
+
+
+
+
+
Backup
+
Configure when backups run, where artifacts are stored, and how backup files are encrypted.
+
+
+
+
+
+
+
Schedule
+
Full backups run on the selected cadence; partial backups run daily only.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Default is 03:00 UTC.
+
+
+
+
+
+
+
+
+
+
Automatic cleanup keeps the newest successful full backup as a safety baseline.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ These backup scope controls are advanced. Modify them at your own risk because excluding a surface can create backups that are incomplete for restore or migration scenarios.
+
+
+
+
+
+
+
Core application records required for meaningful restore and migration.
+
+
+
+
+
+
+
Search index schemas and retrievable indexed documents.
+
+
+
+
+
+
+
Original source files used by Enhanced Citations.
+
+
+
+
+
+
+
+
+
+
+
+
Storage
+
Store backup artifacts in Azure Blob Storage.
+
+
+
+
+
+ Use a dedicated backup storage account. Data Management will reject storage that matches the Enhanced Citations connection string or Blob endpoint.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No connection string saved yet.
+
+
+
+
+
+
+
+
+
+
+
+
Encryption
+
Generate a 256-bit backup encryption key.
+
+
+
+
+
+
+
+
+
Key storage
+
Not configured
+
Key reference
+
Not configured
+
+
+
+
+
Key Vault is strongly recommended
+
Generated backup encryption keys are stored in the Data Management settings document when Key Vault is not enabled.
Backups stream deterministic checkpoint batches and commit only verified work. Higher concurrency can increase source Cosmos cost and pressure.
+
+
+
+
+
+
+
Source Blob Backup Performance
+
Source files stream through bounded chunks and durable per-file checkpoints. Peak transfer buffering is bounded by concurrent transfers multiplied by chunk size.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Defaults bound application transfer buffering to approximately 32 MiB, excluding Azure SDK overhead. Throttling temporarily reduces active transfers.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The backup records the current source capacity, raises only eligible targets up to 10,000 RU/s, and restores the original setting after completion, cancellation, failure, or recovery. This can increase Cosmos charges and requires source ARM throughput permission.
+
+
+
+
+
+
+
+
Backup Operations
+
Queue immediate full or partial backup jobs using the settings above.
+
+
+
+
+
+
+
Jobs use Cosmos-backed leases so scaled-out App Service workers do not run the same backup twice.
+ How many previous messages are carried into each new request.
+
+
+
+
+
+
+
+
+
+ Default System Prompt
+
+
+ The system prompt applied to conversations that do not set their own.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/single_app/templates/admin/_panes/cosmos-editor.html b/application/single_app/templates/admin/_panes/cosmos-editor.html
new file mode 100644
index 000000000..3639ba706
--- /dev/null
+++ b/application/single_app/templates/admin/_panes/cosmos-editor.html
@@ -0,0 +1,49 @@
+
+
+
+
+
Cosmos DB JSON Editor
+
Query SimpleChat Cosmos DB containers, inspect one document, and save JSON changes with ETag protection.
+
+
+
+
+ This tool can modify production Cosmos DB documents directly. Use it only for targeted admin repair or investigation after you understand the impact.
+
+
+ The Cosmos DB JSON editor is locked. Acknowledge the danger prompt before querying or editing data.
+
+
+
+
+
+
+
Choose a known SimpleChat Cosmos DB container.
+
+
+
+
+
Max 100 per request.
+
+
+
+
+
Empty query returns only the first 100 documents. Custom SELECT queries can page beyond 100 with Next Page.
+
+
+
+
+ No query has run yet.
+
+
+ Query results and the JSON editor open in a modal so the Data Management page stays compact.
+
Protect SimpleChat data, move selected workspaces to another environment, and stage restore decisions with guided checks.
-
-
-
-
-
- We suggest not running backups, restores, or migrations during your operational business hours. These jobs run inside the App Service environment and can affect application performance.
-
-
-
-
-
-
-
-
Start Here
-
Use these checkpoints before running backup, migration, restore, or advanced repair actions.
-
-
-
-
-
-
-
Back up
-
Configure dedicated storage, encryption, schedule, and backup scope before queueing jobs.
-
-
-
-
-
-
Migrate
-
Connect a destination, choose who moves, run preflight, then execute a recoverable transfer.
-
-
-
-
-
-
Restore
-
Review backup readiness and stage restore decisions from Backup Inventory.
-
-
-
-
-
-
RU Boost
-
Temporarily raise eligible Cosmos capacity during approved backup or migration windows.
-
-
-
-
-
-
-
-
-
-
-
-
-
Backup
-
Configure when backups run, where artifacts are stored, and how backup files are encrypted.
-
-
-
-
-
-
-
Schedule
-
Full backups run on the selected cadence; partial backups run daily only.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Default is 03:00 UTC.
-
-
-
-
-
-
-
-
-
-
Automatic cleanup keeps the newest successful full backup as a safety baseline.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- These backup scope controls are advanced. Modify them at your own risk because excluding a surface can create backups that are incomplete for restore or migration scenarios.
-
-
-
-
-
-
-
Core application records required for meaningful restore and migration.
-
-
-
-
-
-
-
Search index schemas and retrievable indexed documents.
-
-
-
-
-
-
-
Original source files used by Enhanced Citations.
-
-
-
-
-
-
-
-
-
-
-
-
Storage
-
Store backup artifacts in Azure Blob Storage.
-
-
-
-
-
- Use a dedicated backup storage account. Data Management will reject storage that matches the Enhanced Citations connection string or Blob endpoint.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
No connection string saved yet.
-
-
-
-
-
-
-
-
-
-
-
-
Encryption
-
Generate a 256-bit backup encryption key.
-
-
-
-
-
-
-
-
-
Key storage
-
Not configured
-
Key reference
-
Not configured
-
-
-
-
-
Key Vault is strongly recommended
-
Generated backup encryption keys are stored in the Data Management settings document when Key Vault is not enabled.
Backups stream deterministic checkpoint batches and commit only verified work. Higher concurrency can increase source Cosmos cost and pressure.
-
-
-
-
-
-
-
Source Blob Backup Performance
-
Source files stream through bounded chunks and durable per-file checkpoints. Peak transfer buffering is bounded by concurrent transfers multiplied by chunk size.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Defaults bound application transfer buffering to approximately 32 MiB, excluding Azure SDK overhead. Throttling temporarily reduces active transfers.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
The backup records the current source capacity, raises only eligible targets up to 10,000 RU/s, and restores the original setting after completion, cancellation, failure, or recovery. This can increase Cosmos charges and requires source ARM throughput permission.
-
-
-
-
-
-
-
-
Backup Operations
-
Queue immediate full or partial backup jobs using the settings above.
-
-
-
-
-
-
-
Jobs use Cosmos-backed leases so scaled-out App Service workers do not run the same backup twice.
-
-
-
-
-
-
-
-
- Migration
-
-
Move SimpleChat data through a reviewed, recoverable environment transfer.
-
-
-
- Not reviewed
-
-
-
-
-
-
-
-
-
-
-
-
-
Connect the destination
-
Configure the services this migration will write to. Stored credentials remain redacted.
-
- Destination database: SimpleChat
-
-
-
-
-
-
-
Target Cosmos Database
-
Required for every migration.
-
-
-
-
- Managed identity requires Cosmos DB Data Contributor and target network access.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Fixed app contract.
-
-
-
-
-
-
-
-
-
-
-
-
Target Search
-
Required when AI Search documents are included.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Target Enhanced Citation Storage
-
Required only when source document blobs are included.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Choose who and what moves
-
Selections persist while you search and page. “All” always uses the exhaustive server count.
-
-
0 principal scopes selected
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Available users
-
Search the server catalog.
-
-
-
-
-
-
-
-
-
-
- Page 1
-
-
-
-
-
-
-
-
-
- Loading exhaustive count…
- Every current user record will be resolved by the server when the job starts.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Available groups
-
Search the server catalog.
-
-
-
-
-
-
-
-
-
-
- Page 1
-
-
-
-
-
-
-
-
- Loading exhaustive count…
- Every current group record will be resolved by the server when the job starts.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Available public workspaces
-
Search the server catalog.
-
-
-
-
-
-
-
-
-
-
- Page 1
-
-
-
-
-
-
-
-
- Loading exhaustive count…
- Every current public workspace record will be resolved by the server when the job starts.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Choose what happens at the destination
-
Choose whether to copy only missing items, catch up changes, or make migrated destination data match the source.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Prove the plan is ready
-
Preflight runs server-owned access probes and inventory. Any earlier change makes this review stale.
-
-
-
-
-
-
-
-
-
- Review has not run.
- Run preflight to verify target access, counts, collisions, locks, and capacity policy.
-
-
-
-
-
-
-
-
-
-
-
-
Confirm execution
-
Review the final server-normalized plan. Submission is guarded against duplicate requests.
-
-
-
-
Complete preflight review before confirmation.
-
-
- Destructive destination cleanup. Only destination items previously created by SimpleChat migration may be deleted. Unowned destination data is retained and reported as a conflict.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Operate the migration
-
Progress comes from the durable job record. Cancel, Retry, and Resume retain verified checkpoints.
-
-
-
-
-
-
-
-
-
- No migration is attached to this workflow yet.
- After execution, this stage follows the queued job and exposes its recovery actions.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Cosmos DB JSON Editor
-
Query SimpleChat Cosmos DB containers, inspect one document, and save JSON changes with ETag protection.
-
-
-
-
- This tool can modify production Cosmos DB documents directly. Use it only for targeted admin repair or investigation after you understand the impact.
-
-
- The Cosmos DB JSON editor is locked. Acknowledge the danger prompt before querying or editing data.
-
-
-
-
-
-
-
Choose a known SimpleChat Cosmos DB container.
-
-
-
-
-
Max 100 per request.
-
-
-
-
-
Empty query returns only the first 100 documents. Custom SELECT queries can page beyond 100 with Next Page.
-
-
-
-
- No query has run yet.
-
-
- Query results and the JSON editor open in a modal so the Data Management page stays compact.
-
-
-
-
-
-
-
-
Backup Inventory
-
Track completed full and partial backups created by Data Management jobs.
-
-
-
-
-
-
-
-
-
-
What does Run Retention Cleanup do?
-
- It permanently deletes backups whose age exceeds the retention period configured in Data Management settings,
- and removes their stored artifacts from the backup container. Backups newer than the retention cutoff are left alone.
-
-
-
Only backups in a finished state are eligible; running or queued jobs are skipped.
-
When Keep latest full backup is enabled, the most recent successful full backup is protected even if it is past the cutoff.
-
Each run deletes at most 25 backups, so very large cleanups may need several runs.
-
Cleanup also runs automatically on the configured schedule; this button just runs it now.
-
-
- Seeing “found no expired backups to delete” means every backup is still inside the retention window. That is expected, not an error.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Backup
-
Completed
-
Contents
-
Storage
-
Protection
-
Warnings
-
Actions
-
-
-
-
-
Backup inventory has not loaded yet.
-
-
-
-
-
-
-
-
-
-
-
-
-
Restore Backup
-
Review the target, policy, and backup manifest before queueing a restore job.
-
-
-
-
-
- Restore writes data into the configured target Cosmos DB, AI Search, and Enhanced Citation Storage services. Create-only mode blocks existing destination collisions; overwrite mode requires a separate phrase.
-
-
-
Selected backup
-
-
Choose Restore from a backup row.
-
-
-
-
Restore policy and surfaces
-
-
-
-
-
Create-only is non-destructive and recommended for first restore attempts.
-
-
-
-
-
-
-
-
-
- Required phrase: RESTORE WITH OVERWRITE
-
-
-
-
-
-
-
Preflight review
-
Run review to validate the manifest, target access, and restore policy.
-
-
-
-
-
No restore review has run yet.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Job History
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Created
-
Operation
-
Status
-
Progress
-
Message
-
Actions
-
-
-
-
-
Job history has not loaded yet.
-
-
-
-
-
-
-
-
-
-
-
-
-
Cosmos DB JSON Editor Warning
-
This interface can change live application data.
-
-
-
-
-
- Incorrect edits can break authentication, workspaces, chat history, documents, automations, or activity log integrity. Use this editor only when safer admin tools cannot solve the problem.
-
-
-
Run targeted SELECT queries and page results instead of loading large containers at once.
-
Do not change id or the container partition key value.
-
Review JSON carefully before saving. Saves are audited in Activity Logs.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Cosmos DB Query Results
-
Run a query to load document summaries.
-
No query has run yet.
-
-
-
-
-
-
-
-
-
Results
-
-
-
Run a query to list documents.
-
-
-
-
-
-
-
-
-
Document JSON
-
Select a result to load JSON.
-
-
-
-
-
-
-
-
-
-
The editor blocks id and partition key changes. Saves use the ETag from the loaded document.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Confirm Cosmos DB Document Save
-
Review the change summary before saving.
-
-
-
-
-
- Saving replaces the selected Cosmos DB document with the JSON currently in the editor. This action is audited and cannot be undone from this screen.
-
-
- No changes summarized yet.
-
-
-
-
- Required phrase: I understand this can damage system data
-
-
-
-
-
-
-
-
-
-
-
-
-
Job Details
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Timeline
-
-
-
-
No timeline events loaded.
-
-
-
-
-
-
-
-
Backup Contents
-
-
-
-
No backup artifacts recorded for this job.
-
-
-
-
-
-
-
-
Storage and Manifest
-
-
-
-
-
-
-
-
Warnings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Request Data Management Job Cancellation
-
-
-
-
-
The worker stops at its next durable checkpoint. Completed items remain available for Retry or Resume.
-
-
-
-
-
-
-
-
-
-
Delete Data Management Backup
-
-
-
-
-
- Backup deletion removes stored artifacts, job timeline records, and differential sidecar state for this job. Future partial backups will re-export affected unchanged items.
-
-
-
-
-
-
-
-
-
-
-
-
-
Backup, Migrate & Restore Configuration Guide
-
-
-
-
Configure backup, migration, and restore readiness before an incident or cutover window. The safest path is to first prove backups, then rehearse migration or restore decisions from completed backup records.
-
-
Use dedicated backup storage that is separate from Enhanced Citation source files.
-
Generate and protect the backup encryption key, preferably in Key Vault.
-
Run a manual full backup and inspect the Backup Inventory entry.
-
For migration, connect destination Cosmos, Search, and storage services before selecting scope.
-
For restore, open Backup Inventory and stage the selected backup through the Restore workflow.
-
-
-
-
-
-
-
-
-
-
-
Backup Setup Guide
-
-
-
-
Backups create durable artifacts and job records that later migration or restore workflows can inspect. Full backups capture a complete selected snapshot. Partial backups capture daily changes between full backups.
-
-
Storage: Use a dedicated backup account/container so restore and migration artifacts never overwrite source documents.
-
Encryption: Keep backup encryption enabled and store generated keys in Key Vault for production.
-
Scope: Leave Cosmos DB and AI Search enabled unless you are intentionally creating a limited backup.
-
Timing: Queue large backup jobs outside business hours because App Service workers perform the work.
-
-
-
-
-
-
-
-
-
-
-
Migration Guide
-
-
-
-
Migration moves selected SimpleChat users, groups, public workspaces, and their optional documents to another SimpleChat environment.
-
-
Copy missing items only: safest first run; existing destination records are not changed.
-
Catch up changed items: use after a previous completed migration to copy new items and update migration-owned changes.
-
Make destination match source: use only during cutover when migrated destination-only items should be removed.
-
AI Search freeze: freeze external destination writers before moving Search documents because SimpleChat cannot coordinate external writers.
-
-
-
-
-
-
-
-
-
-
-
RU Boost Permission Guide
-
-
-
-
RU Boost temporarily raises eligible Cosmos DB throughput up to 10,000 RU/s and then restores the original setting. It can increase Azure charges and requires Azure management-plane permissions.
-
-
Data copy permissions prove the identity can create, read, and delete probe records in destination Cosmos containers.
-
RU Boost permissions prove the identity can read and write Cosmos throughput settings through Azure Resource Manager.
-
Destination RU Boost also needs the destination subscription ID and resource group because those values are not part of the data-plane endpoint.
-
Use Test RU Boost before a cutover window; it validates capacity permissions separately from migration data access.
-
-
-
-
-
-
-
-
-
-
-
Restore Guide
-
-
-
-
Restore starts from Backup Inventory. The workflow helps admins confirm what is in the backup, what surfaces are safe to restore, and whether backend restore execution is available for the selected backup.
-
-
Prefer full backups for restore decisions because partial backups depend on earlier backup history.
-
Confirm the encryption key is still available before relying on encrypted artifacts.
-
Review warnings and missing surfaces before restoring AI Search or source document blobs.
-
Run restore only in a maintenance window because restored records can affect live users and search results.
+ The largest file a user may upload into a workspace.
+
+
+
+
+
+
+
diff --git a/application/single_app/templates/admin/_panes/jobs.html b/application/single_app/templates/admin/_panes/jobs.html
new file mode 100644
index 000000000..98c939eed
--- /dev/null
+++ b/application/single_app/templates/admin/_panes/jobs.html
@@ -0,0 +1,89 @@
+
+
+
+
Job History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Created
+
Operation
+
Status
+
Progress
+
Message
+
Actions
+
+
+
+
+
Job history has not loaded yet.
+
+
+
+
+
+
+
diff --git a/application/single_app/templates/admin/_panes/migrate.html b/application/single_app/templates/admin/_panes/migrate.html
new file mode 100644
index 000000000..ad6290a15
--- /dev/null
+++ b/application/single_app/templates/admin/_panes/migrate.html
@@ -0,0 +1,578 @@
+
+
+
+
+
+ Migration
+
+
Move SimpleChat data through a reviewed, recoverable environment transfer.
+
+
+
+ Not reviewed
+
+
+
+
+
+
+
+
+
+
+
+
+
Connect the destination
+
Configure the services this migration will write to. Stored credentials remain redacted.
+
+ Destination database: SimpleChat
+
+
+
+
+
+
+
Target Cosmos Database
+
Required for every migration.
+
+
+
+
+ Managed identity requires Cosmos DB Data Contributor and target network access.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Fixed app contract.
+
+
+
+
+
+
+
+
+
+
+
+
Target Search
+
Required when AI Search documents are included.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Target Enhanced Citation Storage
+
Required only when source document blobs are included.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose who and what moves
+
Selections persist while you search and page. “All” always uses the exhaustive server count.
+
+
0 principal scopes selected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Available users
+
Search the server catalog.
+
+
+
+
+
+
+
+
+
+
+ Page 1
+
+
+
+
+
+
+
+
+
+ Loading exhaustive count…
+ Every current user record will be resolved by the server when the job starts.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Available groups
+
Search the server catalog.
+
+
+
+
+
+
+
+
+
+
+ Page 1
+
+
+
+
+
+
+
+
+ Loading exhaustive count…
+ Every current group record will be resolved by the server when the job starts.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Available public workspaces
+
Search the server catalog.
+
+
+
+
+
+
+
+
+
+
+ Page 1
+
+
+
+
+
+
+
+
+ Loading exhaustive count…
+ Every current public workspace record will be resolved by the server when the job starts.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose what happens at the destination
+
Choose whether to copy only missing items, catch up changes, or make migrated destination data match the source.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Prove the plan is ready
+
Preflight runs server-owned access probes and inventory. Any earlier change makes this review stale.
+
+
+
+
+
+
+
+
+
+ Review has not run.
+ Run preflight to verify target access, counts, collisions, locks, and capacity policy.
+
+
+
+
+
+
+
+
+
+
+
+
Confirm execution
+
Review the final server-normalized plan. Submission is guarded against duplicate requests.
+
+
+
+
Complete preflight review before confirmation.
+
+
+ Destructive destination cleanup. Only destination items previously created by SimpleChat migration may be deleted. Unowned destination data is retained and reported as a conflict.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Operate the migration
+
Progress comes from the durable job record. Cancel, Retry, and Resume retain verified checkpoints.
+
+
+
+
+
+
+
+
+
+ No migration is attached to this workflow yet.
+ After execution, this stage follows the queued job and exposes its recovery actions.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/single_app/templates/admin/_panes/restore.html b/application/single_app/templates/admin/_panes/restore.html
new file mode 100644
index 000000000..6b8e1ff5a
--- /dev/null
+++ b/application/single_app/templates/admin/_panes/restore.html
@@ -0,0 +1,147 @@
+
+
+
+
+
Backup Inventory
+
Track completed full and partial backups created by Data Management jobs.
+
+
+
+
+
+
+
+
+
+
What does Run Retention Cleanup do?
+
+ It permanently deletes backups whose age exceeds the retention period configured in Data Management settings,
+ and removes their stored artifacts from the backup container. Backups newer than the retention cutoff are left alone.
+
+
+
Only backups in a finished state are eligible; running or queued jobs are skipped.
+
When Keep latest full backup is enabled, the most recent successful full backup is protected even if it is past the cutoff.
+
Each run deletes at most 25 backups, so very large cleanups may need several runs.
+
Cleanup also runs automatically on the configured schedule; this button just runs it now.
+
+
+ Seeing “found no expired backups to delete” means every backup is still inside the retention window. That is expected, not an error.
+
- System-level settings that control application behavior, including file size limits, conversation history,
- and default prompts.
+ Warn inactive users and sign them out after a period of inactivity.
-
-
-
-
-
-
-
-
Custom text shown at the top of the idle warning dialog.
-
-
-
-
-
-
- Shown to signed-in users who lack the required roles. Use Enter for line breaks.
-
-
-
\ No newline at end of file
+
diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html
index a34e450cf..f2daba2bc 100644
--- a/application/single_app/templates/admin_settings.html
+++ b/application/single_app/templates/admin_settings.html
@@ -983,6 +983,27 @@
12. Enhanced Citations and Image Generation
{# Governance status is rendered outside the panes so a message stays
visible whichever governance tab is active. #}
+ {# Shared by every Backup & Recovery tab: one save button, one status
+ line and one operational warning serve all five tabs, so they sit
+ outside the panes and are shown only while that group is active. #}
+
+
+
+
Backup, Migrate & Restore
+
Protect SimpleChat data, move selected workspaces to another environment, and stage restore decisions with guided checks.
+
+
+
+
+
+ We suggest not running backups, restores, or migrations during your operational business hours. These jobs run inside the App Service environment and can affect application performance.
+
+
+
+
+
{% include "admin/_panes/secrets.html" %}
{% include "admin/_panes/access-roles.html" %}
@@ -1016,7 +1037,11 @@
12. Enhanced Citations and Image Generation
{% include "admin/_panes/control-center-config.html" %}
- {% include "admin/_panes/data-management.html" %}
+ {% include "admin/_panes/backup.html" %}
+ {% include "admin/_panes/migrate.html" %}
+ {% include "admin/_panes/restore.html" %}
+ {% include "admin/_panes/cosmos-editor.html" %}
+ {% include "admin/_panes/jobs.html" %}
{% include "admin/_panes/redis-caching.html" %}
{% include "admin/_panes/cosmos.html" %}
@@ -1173,6 +1198,433 @@
Recommended setup
+
+
+
+
+
+
+
Restore Backup
+
Review the target, policy, and backup manifest before queueing a restore job.
+
+
+
+
+
+ Restore writes data into the configured target Cosmos DB, AI Search, and Enhanced Citation Storage services. Create-only mode blocks existing destination collisions; overwrite mode requires a separate phrase.
+
+
+
Selected backup
+
+
Choose Restore from a backup row.
+
+
+
+
Restore policy and surfaces
+
+
+
+
+
Create-only is non-destructive and recommended for first restore attempts.
+
+
+
+
+
+
+
+
+
+ Required phrase: RESTORE WITH OVERWRITE
+
+
+
+
+
+
+
Preflight review
+
Run review to validate the manifest, target access, and restore policy.
+
+
+
+
+
No restore review has run yet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Cosmos DB JSON Editor Warning
+
This interface can change live application data.
+
+
+
+
+
+ Incorrect edits can break authentication, workspaces, chat history, documents, automations, or activity log integrity. Use this editor only when safer admin tools cannot solve the problem.
+
+
+
Run targeted SELECT queries and page results instead of loading large containers at once.
+
Do not change id or the container partition key value.
+
Review JSON carefully before saving. Saves are audited in Activity Logs.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Cosmos DB Query Results
+
Run a query to load document summaries.
+
No query has run yet.
+
+
+
+
+
+
+
+
+
Results
+
+
+
Run a query to list documents.
+
+
+
+
+
+
+
+
+
Document JSON
+
Select a result to load JSON.
+
+
+
+
+
+
+
+
+
+
The editor blocks id and partition key changes. Saves use the ETag from the loaded document.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Confirm Cosmos DB Document Save
+
Review the change summary before saving.
+
+
+
+
+
+ Saving replaces the selected Cosmos DB document with the JSON currently in the editor. This action is audited and cannot be undone from this screen.
+
+
+ No changes summarized yet.
+
+
+
+
+ Required phrase: I understand this can damage system data
+
+
+
+
+
+
+
+
+
+
+
+
+
Job Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Timeline
+
+
+
+
No timeline events loaded.
+
+
+
+
+
+
+
+
Backup Contents
+
+
+
+
No backup artifacts recorded for this job.
+
+
+
+
+
+
+
+
Storage and Manifest
+
+
+
+
+
+
+
+
Warnings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Request Data Management Job Cancellation
+
+
+
+
+
The worker stops at its next durable checkpoint. Completed items remain available for Retry or Resume.
+
+
+
+
+
+
+
+
+
+
Delete Data Management Backup
+
+
+
+
+
+ Backup deletion removes stored artifacts, job timeline records, and differential sidecar state for this job. Future partial backups will re-export affected unchanged items.
+
+
+
+
+
+
+
+
+
+
+
+
+
Backup, Migrate & Restore Configuration Guide
+
+
+
+
Configure backup, migration, and restore readiness before an incident or cutover window. The safest path is to first prove backups, then rehearse migration or restore decisions from completed backup records.
+
+
Use dedicated backup storage that is separate from Enhanced Citation source files.
+
Generate and protect the backup encryption key, preferably in Key Vault.
+
Run a manual full backup and inspect the Backup Inventory entry.
+
For migration, connect destination Cosmos, Search, and storage services before selecting scope.
+
For restore, open Backup Inventory and stage the selected backup through the Restore workflow.
+
+
+
+
+
+
+
+
+
+
+
Backup Setup Guide
+
+
+
+
Backups create durable artifacts and job records that later migration or restore workflows can inspect. Full backups capture a complete selected snapshot. Partial backups capture daily changes between full backups.
+
+
Storage: Use a dedicated backup account/container so restore and migration artifacts never overwrite source documents.
+
Encryption: Keep backup encryption enabled and store generated keys in Key Vault for production.
+
Scope: Leave Cosmos DB and AI Search enabled unless you are intentionally creating a limited backup.
+
Timing: Queue large backup jobs outside business hours because App Service workers perform the work.
+
+
+
+
+
+
+
+
+
+
+
Migration Guide
+
+
+
+
Migration moves selected SimpleChat users, groups, public workspaces, and their optional documents to another SimpleChat environment.
+
+
Copy missing items only: safest first run; existing destination records are not changed.
+
Catch up changed items: use after a previous completed migration to copy new items and update migration-owned changes.
+
Make destination match source: use only during cutover when migrated destination-only items should be removed.
+
AI Search freeze: freeze external destination writers before moving Search documents because SimpleChat cannot coordinate external writers.
+
+
+
+
+
+
+
+
+
+
+
RU Boost Permission Guide
+
+
+
+
RU Boost temporarily raises eligible Cosmos DB throughput up to 10,000 RU/s and then restores the original setting. It can increase Azure charges and requires Azure management-plane permissions.
+
+
Data copy permissions prove the identity can create, read, and delete probe records in destination Cosmos containers.
+
RU Boost permissions prove the identity can read and write Cosmos throughput settings through Azure Resource Manager.
+
Destination RU Boost also needs the destination subscription ID and resource group because those values are not part of the data-plane endpoint.
+
Use Test RU Boost before a cutover window; it validates capacity permissions separately from migration data access.
+
+
+
+
+
+
+
+
+
+
+
Restore Guide
+
+
+
+
Restore starts from Backup Inventory. The workflow helps admins confirm what is in the backup, what surfaces are safe to restore, and whether backend restore execution is available for the selected backup.
+
+
Prefer full backups for restore decisions because partial backups depend on earlier backup history.
+
Confirm the encryption key is still available before relying on encrypted artifacts.
+
Review warnings and missing surfaces before restoring AI Search or source document blobs.
+
Run restore only in a maintenance window because restored records can affect live users and search results.
+
+
+
+
+
+
@@ -1883,6 +2335,7 @@
+
{% if settings.enable_semantic_kernel %}
diff --git a/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md b/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md
index 1426a3870..8b6054ec4 100644
--- a/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md
+++ b/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md
@@ -22,8 +22,8 @@ Development ─── feature/admin-settings-ia ──┬── #1297 stage 1
└──► final PR ──► Development
```
-Current version: **0.260.015**. Fingerprint: **462 field names / 110 card ids**.
-Navigation: **14 groups / 40 tabs / 88 sections**.
+Current version: **0.260.017**. Fingerprint: **462 field names / 116 card ids**.
+Navigation: **14 groups / 44 tabs / 93 sections**. Stages D and E are complete.
### Shipped
@@ -81,10 +81,10 @@ template uncomposed while referencing a partial-backed card or field.
---
-## Next: Stage D — the risky one, in progress
+## Stage D — re-homing the cards — complete
-Re-home cards into the target groups, **one group per commit**, running the
-field contract test on each.
+Cards were re-homed into the target groups, one group per pull request, running
+the field contract test and the full regression set on each.
### Proven pattern
@@ -118,10 +118,83 @@ moved from Scale to Security that way.
| Workflow | **New group**, split out of Workspaces |
| AI Models | → Model Endpoints (carries the legacy modal and its nested Chat Model card), Embeddings, Image Generation |
| Agents & Actions | → Agents, Actions, Inbound MCP (whole tab behind `mcp_ui_enabled`) |
+| Backup & Recovery | → Backup, Migrate, Restore, Cosmos Editor, Jobs |
+
+**Stage D is complete.** 14 groups, 44 tabs, from 17 flat tabs.
+
+### Backup & Recovery: complication I4, resolved
+
+The pane was 1,622 lines and needed three things the tools could not do:
+
+- **The migration card is a ``, not a `
`**, so every div-balancing
+ helper walked straight past it. Its boundaries had to be found by balancing
+ `` instead. This is why the tools reported 5 top-level cards when
+ there were 6.
+- **Eleven dialogs**, six of them opened from JavaScript rather than a button,
+ serving what became five different tabs. All were lifted to the shell.
+ 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.
+- **Shared controls.** One save button, one status line and one operational
+ warning serve all five tabs.
+
+### Stage E: the mixed card and the role roster
+
+**`system-settings-section` is gone.** It mixed five unrelated concerns under
+one heading. Each moved to the tab that owns it, wrapped in a new card, with the
+field markup carried across byte-for-byte:
+
+| Field | New home |
+|---|---|
+| `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, card renamed `idle-timeout-section` |
+
+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.
+
+**The Access & Roles roster** gathers all ten `require_member_of_*` switches,
+which are spread over seven tabs. It is built at runtime by
+`admin_access_roles_roster.js` from `input[name^="require_member_of_"]`, not
+from a list, so a new role requirement appears automatically.
+
+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.
+
+### A structural guard worth keeping
+
+`test_every_pane_partial_is_balanced` checks `
` and `` 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 and confusing.
+This was found the hard way: a stray `
` in `access-roles.html` made a
+Send Feedback test fail. Verified against a deliberately broken pane.
+
+### Nav order must match markup order
+
+`test_admin_settings_sidebar_card_parity` requires the nav map to list sections
+in the same order the cards appear in the pane. Adding a card to a pane means
+inserting it at the matching position in the map, not appending.
+
+
+
+### Group-shared regions
+
+Shared controls cannot be copied into each pane (duplicate element ids, and the
+JavaScript module would bind to the wrong one) and cannot live in one pane (an
+inactive pane is hidden, so the other four tabs lose the save button). They now
+sit outside the panes in a region marked `data-admin-group-shared=""`,
+revealed only while that group is active.
+
+`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 Backup & Recovery save button
+would have been hidden permanently. Verified against a simulated DOM in both
+layouts.
-**Only Backup & Recovery is left**, and it is the hardest: complication I4, a
-~985-line migration workflow that must split across Backup / Migrate / Restore
-/ Jobs as one unit.
### Card container ids are not the nav section ids
@@ -203,28 +276,24 @@ every-line-accounted-for guarantee as the split tool. Two behaviours matter:
- **The gap after a card travels with it**, which is what correctly routes an
interleaved modal to the tab that owns its trigger.
-### Remaining
-
-Three complications found while splitting:
+### Complications, all now resolved
- **I1** Modals are interleaved *between* cards, not collected at the end. Each
- modal moves with the tab that owns its trigger.
-- **I2** Three cards are nested inside other cards and must move with their
- parents: `conversation-contents-drawer-section` (inside chat file uploads),
+ modal moves with the tab that owns its trigger. Where a modal is shared by
+ cards that end up in different tabs, it goes to the shell instead.
+- **I2** Some cards are nested inside other cards and move with their parents:
+ `conversation-contents-drawer-section` (inside chat file uploads),
`content-understanding-section` and `office-embedded-image-section` (inside
- document intelligence). All three land in the same target tab as their parent.
-- **I4** The Data Management migration workflow is ~985 lines of non-card markup
- forming one unit. Split it across Backup/Migrate/Restore/Jobs **last and on
- its own**.
-
-## Then Stage E
-
-- Split `system-settings-section`: `max_file_size_mb` → Workspaces;
- `conversation_history_limit` and `default_system_prompt` → Chat; idle timeout
- fields → Security; `access_denied_message` → Security. **No renames.**
-- Access and Roles roster mirroring the 10 `require_member_of_*` toggles, built
- on the Stage B proxy handling. Mirrors carry **no `name` attribute** — the
- field contract test enforces this.
+ document intelligence). The card scanner skips card bodies, so this is
+ automatic.
+- **I4** The Data Management migration workflow is one `` of non-card
+ markup. It was split last and on its own, driven from explicit line ranges.
+
+## Stage E — complete
+
+- `system-settings-section` split to four destinations, no renames.
+- Access & Roles roster mirroring the ten `require_member_of_*` toggles, built
+ from the page rather than a list. Mirrors carry **no `name` attribute**.
---
diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md
index c89efb87f..dcb472088 100644
--- a/docs/explanation/release_notes.md
+++ b/docs/explanation/release_notes.md
@@ -2,6 +2,47 @@
For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/).
+### **(v0.260.017)**
+
+#### New Features
+
+* **All App Role Requirements In One Place**
+ * Ten settings across seven tabs can each require an Entra app role, which made the overall access policy impossible to read without hunting through the whole of Admin Settings.
+ * **Security → Access & Roles** now lists every one of them with a switch and a link to the setting in its own tab. Changing a switch here changes the setting itself.
+ * The list is built from the page, so a new role requirement added anywhere appears here automatically.
+ * (Ref: `app-role-requirements-section`, `admin_access_roles_roster.js`)
+
+#### User Interface Enhancements
+
+* **System Settings Card Split To Where Each Setting Belongs**
+ * One card mixed maximum file size, conversation history, idle timeout, the default system prompt and the access denied message — five unrelated concerns under one heading.
+ * Maximum File Size is now in **Workspaces → Files & Sharing**, Conversation History and Default System Prompt in **Chat → Chat Experience**, and Access Denied Message in **Security → Access & Roles**.
+ * What remains in **Security → Session** is the idle timeout, and the card is now named for it.
+ * Every setting keeps its saved value; nothing needs re-entering.
+ * (Ref: `idle-timeout-section`, `file-size-limit-section`, `conversation-history-section`, `default-system-prompt-section`, `access-denied-message-section`)
+
+### **(v0.260.016)**
+
+#### User Interface Enhancements
+
+* **Backup, Migrate & Restore Split Into Five Tabs**
+ * One tab carried the entire backup, migration, restore, Cosmos editing and job history surface — over 1,600 lines in a single scroll.
+ * Backup & Recovery now has **Backup** (readiness, backup, schedule, storage, encryption), **Migrate**, **Restore**, **Cosmos Editor** and **Jobs**.
+ * The save button, status line and operational-hours warning are shared by all five tabs, so they sit above the tabs and stay available wherever you are in the group.
+ * This completes the Admin Settings restructure: **14 groups and 44 tabs**, from an original 17 flat tabs.
+ * (Ref: `backup`, `migrate`, `restore`, `cosmos-editor`, `jobs`)
+
+#### Bug Fixes
+
+* **Backup Dialogs Remain Available From Every Tab**
+ * The eleven Backup & Recovery dialogs are opened from more than one place and several are opened from code rather than a button. Left inside a tab, a dialog cannot appear while a different tab is showing.
+ * They now sit outside the tabs, so restore, migration cancel, job detail, the Cosmos editor dialogs and the five setup guides all open wherever they are triggered from.
+ * (Ref: Backup & Recovery dialogs, `admin_data_management.js`)
+
+* **Shared Controls Work In Both Navigation Layouts**
+ * Shared group controls resolve their group from whichever navigation is on screen, so the Backup & Recovery save button is present in the sidebar layout as well as the tab layout.
+ * (Ref: `data-admin-group-shared`, `admin_sidebar_nav.js`)
+
### **(v0.260.015)**
#### User Interface Enhancements
diff --git a/functional_tests/test_admin_access_roles_roster.py b/functional_tests/test_admin_access_roles_roster.py
new file mode 100644
index 000000000..268ae0fb5
--- /dev/null
+++ b/functional_tests/test_admin_access_roles_roster.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+"""
+Functional test for the Access & Roles app role roster.
+Version: 0.260.017
+Implemented in: 0.260.017
+
+Settings that can require an Entra app role are spread across seven tabs, which
+makes the overall access policy impossible to read in one go. Access & Roles now
+gathers them into one roster.
+
+The switches themselves stay on their own tabs, so the roster shows mirrors. A
+mirror must never carry a name attribute: the Admin Settings form posts once and
+the backend reads by field name, so a named duplicate would submit the setting
+twice.
+
+This test ensures the roster stays a mirror rather than becoming a second copy.
+"""
+
+import os
+import re
+import sys
+
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+
+from test_support.templates import read_admin_settings_template # noqa: E402
+from test_support.nav import iter_tabs # noqa: E402
+
+APP_ROOT = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "application",
+ "single_app",
+)
+ROSTER_JS = os.path.join(APP_ROOT, "static", "js", "admin", "admin_access_roles_roster.js")
+
+ROLE_FIELD_PATTERN = re.compile(r'name="(require_member_of_[a-z_]+)"')
+
+
+def _read(path):
+ with open(path, "r", encoding="utf-8") as handle:
+ return handle.read()
+
+
+def test_role_fields_are_declared_exactly_once():
+ """A duplicated role field would post the setting twice."""
+ print("Testing app role fields are not duplicated by the roster...")
+
+ markup = read_admin_settings_template()
+ names = ROLE_FIELD_PATTERN.findall(markup)
+ assert names, "Expected app role requirement fields in Admin Settings"
+
+ duplicates = sorted({name for name in names if names.count(name) > 1})
+ assert not duplicates, (
+ "App role fields declared more than once, which would submit them "
+ f"twice: {duplicates}"
+ )
+
+ print(f"All {len(names)} app role fields are declared exactly once.")
+ return True
+
+
+def test_roster_is_rendered_from_the_page():
+ """A hand-written roster would drift as settings move between tabs."""
+ print("Testing the roster builds itself from the page...")
+
+ markup = read_admin_settings_template()
+ assert 'id="app-role-requirements-list"' in markup, (
+ "Expected the roster container in Access & Roles"
+ )
+ assert 'id="app-role-requirements-section"' in markup, (
+ "Expected the roster card in Access & Roles"
+ )
+
+ source = _read(ROSTER_JS)
+ assert 'input[type="checkbox"][name^="require_member_of_"]' in source, (
+ "The roster should discover role switches from the page rather than "
+ "from a hard-coded list, so it cannot fall out of step"
+ )
+
+ print("The roster is built from the page.")
+ return True
+
+
+def test_roster_mirrors_carry_no_name():
+ """The whole point of a mirror is that it is not submitted."""
+ print("Testing roster mirrors are not submitted with the form...")
+
+ source = _read(ROSTER_JS)
+ mirror_block = source[source.index("function buildRow("):source.index("export function")]
+
+ assert "mirror.name" not in mirror_block, (
+ "A roster mirror must not be given a name attribute, or its setting "
+ "would be posted twice"
+ )
+ assert "data-ignore-settings-change" in mirror_block, (
+ "A roster mirror should be excluded from unsaved-change tracking"
+ )
+ assert "data-role-mirror-for" in mirror_block, (
+ "A roster mirror should record which control it drives"
+ )
+
+ print("Roster mirrors carry no name and are excluded from change tracking.")
+ return True
+
+
+def test_roster_lives_in_access_and_roles():
+ """The roster is only useful where access policy is being read."""
+ print("Testing the roster is listed under Access & Roles...")
+
+ section_ids = {
+ section["id"]
+ for _, tab in iter_tabs()
+ if tab["id"] == "access-roles"
+ for section in tab["sections"]
+ }
+ assert "app-role-requirements-section" in section_ids, (
+ "The app role roster should be a section of the Access & Roles tab"
+ )
+
+ print("The roster is listed under Access & Roles.")
+ return True
+
+
+if __name__ == "__main__":
+ tests = [
+ test_role_fields_are_declared_exactly_once,
+ test_roster_is_rendered_from_the_page,
+ test_roster_mirrors_carry_no_name,
+ test_roster_lives_in_access_and_roles,
+ ]
+ results = []
+ for test in tests:
+ print(f"\nRunning {test.__name__}...")
+ try:
+ results.append(bool(test()))
+ except Exception as error: # noqa: BLE001 - report and continue
+ print(f"Test failed: {error}")
+ import traceback
+
+ traceback.print_exc()
+ results.append(False)
+
+ print(f"\nResults: {sum(results)}/{len(results)} tests passed")
+ sys.exit(0 if all(results) else 1)
diff --git a/functional_tests/test_admin_settings_group_shared_regions.py b/functional_tests/test_admin_settings_group_shared_regions.py
new file mode 100644
index 000000000..00a50fd65
--- /dev/null
+++ b/functional_tests/test_admin_settings_group_shared_regions.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+"""
+Functional test for Admin Settings group-shared control regions.
+Version: 0.260.016
+Implemented in: 0.260.016
+
+Some groups share one set of controls across all of their tabs. Backup &
+Recovery is the case that forced this: five tabs share a single save button,
+one status line and one operational warning, all driven by one JavaScript
+module.
+
+Those controls cannot be copied into each pane, because that repeats element
+ids and the module would bind to the wrong one. They cannot live in a single
+pane either, because an inactive tab pane is hidden, so the other four tabs
+would lose the save button. So they sit outside the panes and are revealed
+only while their group is active.
+
+This test ensures that arrangement holds.
+"""
+
+import os
+import re
+import sys
+import collections
+
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+
+from test_support.templates import read_admin_settings_template # noqa: E402
+from test_support.nav import iter_tabs # noqa: E402
+
+APP_ROOT = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "application",
+ "single_app",
+)
+SIDEBAR_JS = os.path.join(APP_ROOT, "static", "js", "admin", "admin_sidebar_nav.js")
+
+SHARED_REGION_PATTERN = re.compile(r'data-admin-group-shared="([a-z0-9-]+)"')
+
+
+def _read(path):
+ with open(path, "r", encoding="utf-8") as handle:
+ return handle.read()
+
+
+def test_shared_regions_name_a_real_group():
+ """A shared region must belong to a group that exists in the nav map."""
+ print("Testing group-shared regions reference real groups...")
+
+ markup = read_admin_settings_template()
+ owners = SHARED_REGION_PATTERN.findall(markup)
+ assert owners, "Expected at least one group-shared region"
+
+ known_groups = {group["id"] for group, _ in iter_tabs()}
+ unknown = sorted(set(owners) - known_groups)
+ assert not unknown, f"Shared regions reference groups that do not exist: {unknown}"
+
+ print(f"All {len(owners)} shared region(s) reference a real group.")
+ return True
+
+
+def test_shared_regions_sit_outside_every_pane():
+ """A shared region inside a pane would be hidden with that pane."""
+ print("Testing group-shared regions sit outside the tab panes...")
+
+ markup = read_admin_settings_template()
+ for match in SHARED_REGION_PATTERN.finditer(markup):
+ before = markup[: match.start()]
+ # Every pane opens with a tab-pane div and the tab content container
+ # comes after all shared regions, so a shared region declared after the
+ # container has been opened would be inside it.
+ assert 'id="adminSettingsTabContent"' not in before, (
+ f"Shared region '{match.group(1)}' is declared inside the tab content "
+ "container, where it would be hidden along with the active pane"
+ )
+
+ print("Shared regions are declared before the tab content container.")
+ return True
+
+
+def test_shared_controls_are_not_duplicated():
+ """The whole point is one control serving many tabs, not one per tab."""
+ print("Testing shared controls appear exactly once...")
+
+ markup = read_admin_settings_template()
+ ids = collections.Counter(re.findall(r'\sid="([A-Za-z0-9_-]+)"', markup))
+ duplicates = sorted(element_id for element_id, count in ids.items() if count > 1)
+ assert not duplicates, (
+ "Element ids declared more than once in the composed Admin Settings "
+ f"template: {duplicates}"
+ )
+
+ print(f"All {len(ids)} element ids in Admin Settings are unique.")
+ return True
+
+
+def test_shared_regions_are_synced_on_tab_change():
+ """A region that is never toggled would stay hidden forever."""
+ print("Testing shared regions are synced when the tab changes...")
+
+ source = _read(SIDEBAR_JS)
+ assert "function syncAdminGroupSharedRegions(" in source, (
+ "Expected a helper that reveals the shared region for the active group"
+ )
+ # Called from the programmatic path, the initial-load path, and Bootstrap's
+ # own tab event, so no route into a tab leaves the region stale.
+ call_count = source.count("syncAdminGroupSharedRegions(")
+ assert call_count >= 4, (
+ f"Expected the sync helper to be defined and called from every path "
+ f"that activates a tab, found {call_count} references"
+ )
+ assert "shown.bs.tab" in source, (
+ "Clicking a tab button directly does not go through showAdminTab, so "
+ "Bootstrap's shown.bs.tab event must also sync the shared regions"
+ )
+
+ print("Shared regions are synced from every path that activates a tab.")
+ return True
+
+
+def test_shared_region_lookup_handles_both_navigations():
+ """Only one navigation renders at a time, so both must resolve the group."""
+ print("Testing group lookup works in the tab and sidebar layouts...")
+
+ source = _read(SIDEBAR_JS)
+ match = re.search(r"function syncAdminGroupSharedRegions\(tabId\) \{.*?\n\}", source, re.S)
+ assert match, "Expected the shared region sync helper"
+ body = match.group(0)
+
+ # The top tab strip is skipped entirely in the sidebar layout, so a lookup
+ # that only reads `.admin-tab-item` would leave the region hidden for good
+ # and Backup & Recovery would lose its only save button.
+ assert ".admin-tab-item[data-admin-group]" in body, (
+ "Expected the tab layout lookup"
+ )
+ assert ".admin-nav-tab[data-tab=" in body, (
+ "Expected a sidebar layout fallback so the region resolves when the top "
+ "tab strip is not rendered"
+ )
+
+ print("Both navigation layouts can resolve the owning group.")
+ return True
+
+
+if __name__ == "__main__":
+ tests = [
+ test_shared_regions_name_a_real_group,
+ test_shared_regions_sit_outside_every_pane,
+ test_shared_controls_are_not_duplicated,
+ test_shared_regions_are_synced_on_tab_change,
+ test_shared_region_lookup_handles_both_navigations,
+ ]
+ results = []
+ for test in tests:
+ print(f"\nRunning {test.__name__}...")
+ try:
+ results.append(bool(test()))
+ except Exception as error: # noqa: BLE001 - report and continue
+ print(f"Test failed: {error}")
+ import traceback
+
+ traceback.print_exc()
+ results.append(False)
+
+ print(f"\nResults: {sum(results)}/{len(results)} tests passed")
+ sys.exit(0 if all(results) else 1)
diff --git a/functional_tests/test_admin_settings_template_composition.py b/functional_tests/test_admin_settings_template_composition.py
index 4b0e73f2f..1e71cea5a 100644
--- a/functional_tests/test_admin_settings_template_composition.py
+++ b/functional_tests/test_admin_settings_template_composition.py
@@ -41,6 +41,31 @@
)
+def test_every_pane_partial_is_balanced():
+ """An unbalanced pane silently nests the panes that follow it.
+
+ A stray or missing closing tag does not fail to render. It changes the
+ document tree, so later panes end up inside the broken one and are hidden
+ with it, which only shows up as a confusing failure somewhere unrelated.
+ """
+ print("Testing Admin Settings pane partials are balanced...")
+
+ panes_dir = PARTIAL_DIR / "_panes"
+ unbalanced = []
+ for path in sorted(panes_dir.glob("*.html")):
+ markup = path.read_text(encoding="utf-8")
+ for tag in ("div", "section"):
+ opened = len(re.findall(rf"<{tag}\b", markup))
+ closed = len(re.findall(rf"{tag}>", markup))
+ if opened != closed:
+ unbalanced.append(f"{path.name}: {opened} <{tag}> vs {closed} {tag}>")
+
+ assert not unbalanced, "Unbalanced tab pane partials:\n " + "\n ".join(unbalanced)
+
+ print(f"All {len(list(panes_dir.glob('*.html')))} pane partials are balanced.")
+ return True
+
+
def test_parent_template_delegates_panes_to_partials():
"""The parent template should include partials rather than inline panes."""
print("Testing Admin Settings partial delegation...")
diff --git a/functional_tests/test_data_management_security_patterns.py b/functional_tests/test_data_management_security_patterns.py
index 284c123be..982adfcca 100644
--- a/functional_tests/test_data_management_security_patterns.py
+++ b/functional_tests/test_data_management_security_patterns.py
@@ -414,8 +414,13 @@ def test_admin_ui_exposes_data_management_without_external_assets():
sidebar = read_text(SIDEBAR_TEMPLATE)
for marker in [
- 'id="data-management"',
- 'id="data-management" role="tabpanel" aria-labelledby="data-management-tab" data-testid="data-management-tab-pane" data-ignore-settings-change="true"',
+ # Backup, Migrate & Restore is now five tabs rather than one, so each
+ # pane is asserted individually.
+ 'id="backup" role="tabpanel" aria-labelledby="backup-tab" data-testid="backup-tab-pane" data-ignore-settings-change="true"',
+ 'id="migrate" role="tabpanel" aria-labelledby="migrate-tab" data-testid="migrate-tab-pane" data-ignore-settings-change="true"',
+ 'id="restore" role="tabpanel" aria-labelledby="restore-tab" data-testid="restore-tab-pane" data-ignore-settings-change="true"',
+ 'id="cosmos-editor" role="tabpanel" aria-labelledby="cosmos-editor-tab" data-testid="cosmos-editor-tab-pane" data-ignore-settings-change="true"',
+ 'id="jobs" role="tabpanel" aria-labelledby="jobs-tab" data-testid="jobs-tab-pane" data-ignore-settings-change="true"',
'id="data-management-save-settings-btn"',
'id="data-management-save-settings-btn" disabled aria-disabled="true"',
'id="data-management-operational-warning"',
@@ -564,27 +569,32 @@ def test_admin_ui_exposes_data_management_without_external_assets():
assert "window.updateAdminSettingsSaveButtonState = updateSaveButtonState;" in admin_settings_js
assert 'Target Cosmos' not in sidebar
# Sidebar labels now come from the navigation map, which both the sidebar
- # and the top tab strip render from.
+ # and the top tab strip render from. Backup, Migrate & Restore is spread
+ # across the Backup & Recovery group's tabs, so scope by group rather than
+ # by a single tab id.
+ backup_recovery_tabs = [
+ tab for group, tab in iter_tabs() if group["id"] == "backup-recovery"
+ ]
+ assert backup_recovery_tabs, "Backup & Recovery group missing from the navigation map"
+
data_management_sections = {
section["label"]
- for _, tab in iter_tabs()
- if tab["id"] == "data-management"
+ for tab in backup_recovery_tabs
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"
+
+ tab_labels = {tab["label"] for tab in backup_recovery_tabs}
+ for expected_label in ("Backup", "Migrate", "Restore", "Jobs"):
+ assert expected_label in tab_labels, (
+ f"Backup & Recovery is missing the {expected_label} tab"
+ )
assert 'cdn.jsdelivr.net' not in read_text(ADMIN_JS)
- 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 tab in backup_recovery_tabs
for section in tab["sections"]
}
for expected_section in (