Skip to content

Phase 17 — Bulk finding actions

Last updated: 2026-05-08

A partner reviewing 50–200 findings shouldn't have to click each one. Phase 17 adds checkbox selection in the finding list and a single endpoint that applies the same status (accepted / rejected / refined) to many findings at once. Per-id outcomes are returned so a partial failure doesn't poison the batch.

Endpoint

POST /auditforge/findings/bulk-action?engagement_id=<id>
Content-Type: application/json

{
  "finding_ids": ["f-abc", "f-def", "f-ghi"],
  "action": "accepted",        // accepted | rejected | refined
  "auditor_notes": "approved as part of remediation backlog (partner)"
}

Auth: same as the singular endpoints — admin token or session token belonging to a user with admin or partner role within the engagement's firm. Associates get 403 (Phase 15 read-only enforcement).

finding_ids must be non-empty and ≤200 IDs per call.

Response

{
  "ok": true,
  "engagement_id": "eng-abc123",
  "action": "accepted",
  "total": 3,
  "succeeded": 2,
  "failed": 1,
  "results": [
    { "id": "f-abc", "ok": true, "status": "accepted" },
    { "id": "f-def", "ok": true, "status": "accepted" },
    { "id": "f-ghi", "ok": false, "error": "finding_not_found" }
  ]
}

The endpoint returns 200 even with partial failure — succeeded / failed counts and per-id results let the UI show "47/50 accepted, 3 not found" without aborting the whole call.

UX

In the engagement detail view, every finding row now has a checkbox at its left edge. Selecting one or more findings reveals the bulk action bar above the list:

  • Accept all — flips selection to accepted
  • Reject all — flips selection to rejected
  • Mark refined — flips selection to refined
  • Clear selection — empties the selection set

A confirmation dialog appears before the call fires (Accept 47 findings?). Following the action, a banner reports the outcome (accept → 47/50 · 3 failed).

The Select all visible button in the filter bar selects every finding that passes the current filters — useful for "accept every low-severity finding flagged in the last run" without scrolling.

Why per-id results instead of fail-fast

A 200-id bulk could fail on the 50th id (e.g., a stale UI showing a deleted finding). Aborting at id 50 would leave 49 already-flipped findings in the new state and 150 untouched, with no clean way for the partner to recover or retry. Per-id outcomes mean the operation is commutative: the partner can re-select the failures and try again, or just dismiss the banner.

The store-level update_status is itself idempotent — applying accepted to an already-accepted finding is a no-op. So a retry of the whole batch (failures + successes) is also safe.

Files

  • app/auditforge_endpoints.pyPOST /findings/bulk-action handler + BulkFindingActionRequest
  • frontend/src/api/auditforge.tsbulkFindingAction typed client
  • frontend/src/components/FindingList.tsx — checkbox column + select-all button
  • frontend/src/components/EngagementDetail.tsx — selection state + bulk action bar
  • tests/test_auditforge_endpoints.py — 8 endpoint tests covering auth, role gating, partial failure, validation