Skip to content

Add the admin Review center with dashboards, workbenches and bulk review - #1710

Merged
Paul Lizer (paullizer) merged 18 commits into
microsoft:paullizer-react-v2-uifrom
paullizer:paullizer-admin-review-center
Oct 8, 2026
Merged

Paul Lizer (paullizer) merged 18 commits into
microsoft:paullizer-react-v2-uifrom
paullizer:paullizer-admin-review-center

Conversation

@paullizer

@paullizer Paul Lizer (paullizer) commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #1709 (layer 1 of 3). Until #1709 merges, this diff includes its commits; review only the commits after 7e192f6.

This is layer 2 of 3 of the Admin Review Center work. Layer 3 (AI assist, #1711) builds on this branch. Merge order: #1709 → #1710 → #1711.

Summary

  • Review center replaces the V2 Feedback Review and Safety Violations pages. One account-menu entry opens a rail (shared with Approvals) of each section's Dashboard, a Workflows-style workbench (list beside detail, search, address-driven filters, multi-select with Shift+range and "select all matching", per-record bulk results), and full-page editors with Back, a dirty guard, and Reload on ETag conflict. Unchecked chat content gets its own page with sequential bulk recheck. Section access mirrors the server decorators (lib/reviewAccess.ts); old routes redirect with their query; classic pages are unchanged.
  • Approvals gains a Dashboard category (new GET /api/approvals/stats, counting only requests the caller can see) and a Safety remediation category; Group requests no longer lists warn/suspend/block.
  • Backend additions: list search/filters with batched display names, /ids endpoints (cap 500), days windows on both stats endpoints (all existing fields kept), GET /api/safety/logs/<id>, POST /feedback/review/bulk and POST /api/safety/logs/bulk (≤100 ops, per-item results, same code path as the single-record saves). Exports take the list filters. Feedback saves record adminReview.analyzedBy; an opt-in Notify the user sends a feedback_response notification.
  • Pre-existing bugs fixed (fix doc): denied/expired remediation requests now unlock their violation (deny hook plus batched reconcile, never unlocking a genuinely pending request); re-saving an applied or requested suspension/block no longer creates a duplicate request (remediation_already_applied, remediation_unchanged, explicit reissue); review writes are ETag-conditional merges (409 record_changed), so a concurrent user acknowledgment is never overwritten.
  • Code review follow-ups (commits after 223aaee):
    • A Suspend/Block save is recorded only while the violation's request and warning state are as the save read it. If a warning send, a warning claim or another request lands while its approval is being created, the save returns 409 record_changed and 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.
    • A PATCH, archive or bulk update/archive that names an etag is written on that version or refused with record_changed. It is never retried onto a newer version. Without an etag, a retry merges only the fields the save changed.
    • The classic safety review gains Request this suspension/block again (sends 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.
    • CodeQL js/xss-through-dom: the classic safety review reads its action select only through readSelectedAction(), which returns the matching entry of a fixed list of actions. Only those constants reach the data-action attributes 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

  • New Feature
  • Bug Fix
  • UI Enhancement
  • Breaking Change
  • Internal only

Is this visible to end users?

  • Yes (users can receive a feedback-response notification that opens their feedback)
  • No

Is this admin-facing (Admin Settings, governance, deployment, config)?

  • Yes
  • No

Should this become a Latest Feature card?

  • Yes
  • No
  • Already added

Screenshot needed for the card?

  • Yes
  • No
  • Attached

Version bump

  • application/single_app/config.py VERSION third segment bumped (0.261.297 → 0.261.298)
  • deployers/version.txt not needed because deployers/ was not changed

Testing / 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-dom fix):

    • 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 without SIMPLECHAT_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.
    • The PR's CodeQL check: success, with no high-severity alert.
  • 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 -O Python in fresh offline processes. The remediation-state tests include the review follow-up probes:

    • Warn vs Suspend, including a claim that lands during approval creation, with and without an etag.
    • Suspend vs Block, and Suspend vs Suspend.
    • Notes that land meanwhile, and a stale claim.
    • A failed withdrawal, with the Control Center executor refusing the request.
    • Named-version saves for safety and feedback PATCH, archive and bulk.

    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 ci succeeded, npm run typecheck passed, npm run build passed (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.py skips without SIMPLECHAT_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 to docs/_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 AST NameError).
    • functional_tests/test_v2_stats_parity.py::test_charts_use_the_shared_vendored_runtime and functional_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-memory and /api/m365/bindings requests. Those come from Settings components this branch doesn't change.
    • With 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 comparing backTo without its query, behaves identically for those editors' query-less backTo.
  • Not run: the known flaky ui_tests/test_v2_sidebar_conversation_scroll.py::test_the_phone_drawer_holds_search_under_new_chat.

Documentation

  • Release notes updated (docs/explanation/release_notes.md, v0.261.298)
  • Feature documentation updated (docs/explanation/features/V2_ADMIN_REVIEW_CENTER.md; new guide docs/guides/admin-review-center.md; updated admin-review-feedback.md, admin-review-safety-violations.md, recheck-chat-content.md, review-approval-requests.md; logging tag inventory)
  • Fix documentation updated (docs/explanation/fixes/SAFETY_REMEDIATION_APPROVAL_STATE_FIX.md)

Security checklist

  • New Flask routes include @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)
  • Settings sent to non-admin frontends use sanitize_settings_for_user() (no new settings payloads)
  • Browser JavaScript is served from local SimpleChat static assets only; no CDN-hosted JS (charts use the vendored Chart.js runtime)
  • No secrets, keys, connection strings, or local-only artifacts are included

Paul Lizer (paullizer) and others added 12 commits October 7, 2026 22:46
…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>
Comment thread application/single_app/functions_authentication.py
Comment thread application/single_app/route_backend_safety.py
Comment thread ui_tests/test_classic_safety_review_and_access_restricted.py Fixed
Paul Lizer (paullizer) and others added 4 commits October 8, 2026 02:56
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>
Comment thread application/single_app/static/js/admin/admin-safety-violations.js Fixed
Comment thread ui_tests/test_classic_safety_review_and_access_restricted.py Fixed
Comment thread ui_tests/test_classic_safety_review_and_access_restricted.py Fixed
Paul Lizer (paullizer) and others added 2 commits October 8, 2026 05:11
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>
@paullizer
Paul Lizer (paullizer) merged commit 7e2ed6f into microsoft:paullizer-react-v2-ui Oct 8, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants