Repository navigation
Add the admin Review center with dashboards, workbenches and bulk review - #1710
Merged
Paul Lizer (paullizer) merged 18 commits intoOct 8, 2026
Conversation
…restricted screen Layer 1 of 3 of the Admin Review Center stack. - Warn user executes when the safety reviewer saves the review, without an approval request, and is audited in the activity log. Saving again does not resend it. Suspend and Block still need a second eligible reviewer. - Executed warnings must be acknowledged: new user-scoped routes GET /api/safety/warnings/pending and POST /api/safety/warnings/<id>/acknowledge (owner-only, idempotent, ETag-conditional), a V2 dialog fed by a bootstrap pending count, and the acknowledgment state in the admin review and the user's Violations tab. - Escalate can no longer be chosen; legacy records keep it, labelled "Escalated (legacy)". escalate_count stays in the stats JSON. - Approved suspensions and blocks store the notice the user was sent in settings.access.notice. - user_required answers restricted users with a structured 403 (error: access_restricted) for API calls and redirects pages to /v2/access-restricted or /access-restricted. New login-only routes serve those pages and GET /api/v2/access-restriction (caller's own data only), exempt from the Terms of Use gate. - Version 0.261.296. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the fix and feature notes, updates the safety review and approval request guides and the Content Safety settings page, records the release notes for 0.261.296, lists the new logging tags, and regenerates the docs app surface inventory for the classic Access restricted page. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ser test Render the real classic templates and scripts behind a closed API: no Escalate choice, the legacy label, the second-reviewer help text, the warning acknowledgment line, Blocked statistics, and the localized restore time on the classic Access restricted page. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…restricted branch The base now uses 0.261.296, so this branch moves to 0.261.297: config.py, its tests, docs and a new release-notes section above the base's 0.261.296 entry. Both route-inventory coverage lines are kept. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A reviewer can warn about the same violation again. The V2 store hid any warning whose violation it had already acknowledged, so a newer warning never showed in a tab that stayed open. It now keys each warning on the violation and its send time. The acknowledge route also takes the issued_at of the warning the user read. When the violation now holds a newer warning, it answers 409 safety_warning_replaced and records nothing, and the dialog reads the pending warnings again to show the newer one. A body without issued_at keeps the previous behaviour. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two overlapping saves with Warn user, such as a double-click or two reviewers, both passed the already-sent check, so each sent a notification and only one was recorded. A save now claims the violation with a write conditional on the ETag it read (action_request_status 'sending') before anything is sent; the other save sends nothing and gets 409 safety_warning_in_progress. The outcome is written on the claimed version. While a claim is fresh, other saves and deletes wait, and 'sending' never counts as a warning to acknowledge. A claim older than five minutes reads as a failed send and can be retried once. A send whose claim was lost is audited and reported as safety_warning_not_recorded instead of overwriting another writer. The classic Save Review button is disabled while its request is in flight. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds list search, filters and server-resolved names, select-all-matching ids, dashboard windows on the feedback and safety stats, bulk review endpoints and an approvals summary. Denied and expired remediation requests now unlock their violation, re-saving an applied suspension or block no longer requests it again, and review writes are ETag-conditional. Exports take the list filters. Version 0.261.298. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…hboard Extracts the Approvals rail into CategoryRailPage and the Control Center tiles, charts and data tables into DashboardParts. Approvals gains a Dashboard category and a Safety remediation category, Group requests now lists group requests only, and its list reads status, type and who-it-is-for filters from the address. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
One Review center entry opens a rail of each section's dashboard, workbench and queues. Workbenches list records beside their detail, with search, address filters, multi-select and bulk actions; records open in full-page editors with Back and a dirty guard. Unchecked chat content moves to its own page with sequential bulk recheck. Old routes redirect. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Covers bulk caps and per-item results, select-all-matching ids, stats windows with legacy fields kept, list search by display name, the approvals summary's visibility, denied and expired requests unlocking violations, unchanged restrictions not requested twice, and ETag-conditional review writes. Moves the V2 page source checks to the Review center files and adds the new routes to the policy inventories. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Covers rail access per role with the not-available states and old-route redirects, dashboard figures opening filtered workbenches, click and Shift+click selection with per-record bulk reports, select-all-matching and the counted delete confirmation, the editor's dirty guard, save return and conflict reload, the violation editor's notice and suspension presets, sequential unchecked chat rechecks, and the Approvals Dashboard and Safety remediation categories. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the feature and fix notes, a Review center guide, and updates the feedback, safety violation, chat recheck and approval request guides, the release notes for 0.261.298 and the logging tag inventory. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A Suspend or Block save created its approval request and then wrote the request onto the violation without checking what had changed meanwhile. A warning claimed during that window was overwritten and later recorded as executed over the pending request, and two overlapping restriction saves left the first approval approvable but unlinked. - Record a new request only on the violation's remediation state as it was read, and refuse a fresh warning claim. When the write is refused, withdraw the approval (withdraw_approval_request) so nothing stays approvable, and return 409 record_changed. - The approval executor refuses a request its violation no longer awaits (safety_log_awaits_request). - A save or archive that names an etag is written on that version or refused with record_changed, never retried onto a newer one. Without an etag, a retry merges only the fields the request changed. This covers safety and feedback PATCH, archive and bulk update/archive. - Archive refuses a violation whose warning is being sent. - Re-request messages name the "Request this ... again" control. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The classic safety review never sent reissue, so after a request was denied, expired or failed, saving the same Suspend or Block returned "No new suspension was requested" and dropped the new date and message. - Classic review: add a "Request this suspension/block again" control, shown when the violation already records that action and no request is waiting. It sends reissue: true. The help text describes where the last request stands, the notification and restore time are only sent when something will be requested, and the restore time must be in the future. - Review center editor: the same rule and wording through offersRestrictionReissue and existingRestrictionText. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Oct 8, 2026
CodeQL flagged js/xss-through-dom in the classic safety review: the action select's value, which is DOM text, was written into data-action attributes on the notification and restore-time fields. The select is now read only through readSelectedAction(), which returns the matching constant from ACTION_VALUES (None, WarnUser, SuspendUser, BlockUser and the legacy Escalate). An empty or unknown value reads as None, as an empty one did before. The remediation fields and the save both use it, so only those constants reach the data attributes and the request. Every value the select can hold is in the list, so the review behaves the same. A browser test checks that a forged option reaches neither the data attributes nor the save, while a chosen action is kept as it is. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CodeQL noted three _open_review() results stored in modal and never read before the next assignment. The calls stay; only the unused assignments go. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer)
merged commit Oct 8, 2026
7e2ed6f
into
microsoft:paullizer-react-v2-ui
11 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
lib/reviewAccess.ts); old routes redirect with their query; classic pages are unchanged.GET /api/approvals/stats, counting only requests the caller can see) and a Safety remediation category; Group requests no longer lists warn/suspend/block.search/filters with batched display names,/idsendpoints (cap 500),dayswindows on both stats endpoints (all existing fields kept),GET /api/safety/logs/<id>,POST /feedback/review/bulkandPOST /api/safety/logs/bulk(≤100 ops, per-item results, same code path as the single-record saves). Exports take the list filters. Feedback saves recordadminReview.analyzedBy; an opt-in Notify the user sends afeedback_responsenotification.remediation_already_applied,remediation_unchanged, explicitreissue); review writes are ETag-conditional merges (409 record_changed), so a concurrent user acknowledgment is never overwritten.409 record_changedand withdraws that approval (withdraw_approval_request, which also removes its notices), so no orphaned request stays approvable. The Control Center also refuses to carry out a request its violation no longer awaits.etagis written on that version or refused withrecord_changed. It is never retried onto a newer version. Without anetag, a retry merges only the fields the save changed.reissue: true) after a failed, denied or expired request, with help text that reflects the request's state. The V2 editor follows the same rule.js/xss-through-dom: the classic safety review reads its action select only throughreadSelectedAction(), which returns the matching entry of a fixed list of actions. Only those constants reach thedata-actionattributes and the save. Every value the select can hold is in the list, so behaviour is unchanged.Linked issue
None. The requester asked not to file an issue for this work. Depends on #1709.
Release Notes & Latest Features
Is this visible to end users?
Is this admin-facing (Admin Settings, governance, deployment, config)?
Should this become a Latest Feature card?
Screenshot needed for the card?
Version bump
application/single_app/config.pyVERSIONthird segment bumped (0.261.297 → 0.261.298)deployers/version.txtnot needed becausedeployers/was not changedTesting / validation
All run locally on this branch. The full set ran at head 6570dec; the first bullet covers the later CodeQL fix.
At head ac6e713 (the CodeQL
js/xss-through-domfix):python -m pytest ui_tests/test_classic_safety_review_and_access_restricted.py ui_tests/test_admin_review_list_card_views.py -q: passed (7 passed, 2 skipped withoutSIMPLECHAT_UI_BASE_URL). This includes the new forged-option test, which fails if either read of the select uses its raw value.python -m pytest functional_tests/test_safety_violation_remediation_approvals.py functional_tests/test_safety_escalate_removal.py functional_tests/test_safety_warning_send_claim.py functional_tests/test_xss_guardrails_checker.py -q: passed (27 passed).python scripts/check_xss_sinks.py --full-file application/single_app/static/js/admin/admin-safety-violations.js application/single_app/templates/admin_safety_violations.html: passed.node --check application/single_app/static/js/admin/admin-safety-violations.js: passed.python -m pytest functional_tests/test_safety_warning_acknowledgment.py functional_tests/test_safety_violation_remediation_approvals.py functional_tests/test_safety_escalate_removal.py functional_tests/test_access_restricted_gate.py functional_tests/test_safety_warning_send_claim.py functional_tests/test_safety_remediation_approval_state.py functional_tests/test_review_center_bulk_and_dashboards.py functional_tests/test_approvals_dashboard_stats.py functional_tests/test_v2_admin_review_pages.py functional_tests/test_v2_terms_and_approvals.py functional_tests/test_v2_notifications_bell.py functional_tests/test_v2_sidebar_account_menu.py functional_tests/test_agent_editor_hides_internal_ids.py -q: passed (96 passed). This covers layer 1's suites plus the new bulk/ids/stats, approvals summary and remediation-state tests, which run under normal and-OPython in fresh offline processes. The remediation-state tests include the review follow-up probes:etag.With the guard removed, the overlap and version probes fail.
node functional_tests/test_v2_review_center_logic.mjs: passed.node functional_tests/test_v2_access_restriction_and_safety_warning_logic.mjs: passed (11 checks).node functional_tests/test_v2_workflow_run_link_routing.mjs: passed (10 pass, 0 fail).python functional_tests/route_tests/test_route_blueprint_policy_inventory.py: passed (14/14).python functional_tests/route_tests/test_route_unauthenticated_policy_contract.py: passed (9/9).python functional_tests/route_tests/test_route_policy_test_coverage.py: passed (3/3).In
application/v2_ui:npm cisucceeded,npm run typecheckpassed,npm run buildpassed (only the existing chunk-size warning).python -m pytest ui_tests/test_v2_review_center.py ui_tests/test_v2_approvals_dashboard.py ui_tests/test_v2_approvals_and_terms_pages.py ui_tests/test_admin_review_list_card_views.py ui_tests/test_admin_app_role_settings_copy.py ui_tests/test_v2_access_restricted_and_safety_warning.py ui_tests/test_v2_control_center_dashboard.py ui_tests/test_v2_control_center_data_health_removed.py ui_tests/test_v2_settings_workspace_open.py ui_tests/test_classic_safety_review_and_access_restricted.py -q: passed (45 passed, 3 skipped;test_admin_review_list_card_views.pyskips withoutSIMPLECHAT_UI_BASE_URL). This includes the new classic and V2 "Request … again" tests.python functional_tests/test_docs_app_surface_coverage.py: passed (7/7).python functional_tests/test_docs_site_quality.py: passed (6/6).python scripts/build_docs_inventory.py: no change todocs/_data/app_surface.yml.python scripts/check_xss_sinks.py --base-sha 7e192f61b --head-sha HEAD <60 changed files>: passed.python scripts/check_broken_access_control.py --full-file <23 changed .py files>: passed.git diff --check 7e192f61b HEAD: clean.Pre-existing failures, not caused by this branch:
functional_tests/test_profile_and_admin_review_tabs.py: 3 failures, identical before these changes (classic template markers and an ASTNameError).functional_tests/test_v2_stats_parity.py::test_charts_use_the_shared_vendored_runtimeandfunctional_tests/test_v2_conversation_deep_link.py::test_incoming_link_is_captured_before_any_effect_runs: both fail on files this branch doesn't touch (InlineChart.tsx, the chat URL hook).functional_tests/test_logging_tag_standardization.py: fails on undocumented tags in other modules (EditorAssist,WorkflowHandoffDrafts, …). The[REVIEW_CENTER]and[FEEDBACK_REVIEW]tags added here are documented.ui_tests/test_v2_notifications_bell.py: 47 passed, 10 teardown errors from unstubbed/api/profile/fact-memoryand/api/m365/bindingsrequests. Those come from Settings components this branch doesn't change.PYTHONPATH=ui_tests/fixtures,python -m pytest ui_tests/test_v2_workspace_authoring.py ui_tests/test_v2_workspace_draft_navigation.py ui_tests/test_v2_group_actions.py ui_tests/test_v2_group_agents.py ui_tests/test_v2_admin_global_agents_actions.py: 162 passed, 12 failed. Every failure is agent/MCP editor content (tool labels, MCP preconfiguration catalog), not navigation or saving. The only shared-frame change here,WorkspaceEditorFrame's save bypass comparingbackTowithout its query, behaves identically for those editors' query-lessbackTo.Not run: the known flaky
ui_tests/test_v2_sidebar_conversation_scroll.py::test_the_phone_drawer_holds_search_under_new_chat.Documentation
docs/explanation/release_notes.md, v0.261.298)docs/explanation/features/V2_ADMIN_REVIEW_CENTER.md; new guidedocs/guides/admin-review-center.md; updatedadmin-review-feedback.md,admin-review-safety-violations.md,recheck-chat-content.md,review-approval-requests.md; logging tag inventory)docs/explanation/fixes/SAFETY_REMEDIATION_APPROVAL_STATE_FIX.md)Security checklist
@swagger_route(security=get_auth_security())(and the same role/feature decorators as the single-record routes beside them; added to the route policy inventories)sanitize_settings_for_user()(no new settings payloads)