# CRM lead permissions and detail readback — locally verified

The local learner Nora Ellis had Sales membership, no direct roles, and inherited LeadEditor with Read/Create/Update only. She created the zero-value Permissions Practice probe and successfully deleted it through the actual CRM UI: `DELETE /crm/leads/detail/6aaeee86721998990a188582` returned 200. Existing Maya Bennett and Daniel Reyes records were untouched. This disproved the course’s expected Delete refusal. Captures 34–36 in `assets/local-roles/` preserve the probe and outcome.

Separately, updating Maya Bennett’s title returned 200 and closed the edit form, but the still-open Lead Details showed the old title. Full reload displayed the saved new title (captures 32–33), establishing a stale-view defect rather than a failed save.

## Backend repair

`appengine/src/crm/leads.controller.ts` previously declared no content permissions, so the global JWT guard authenticated the request without checking its action. Each lead, pipeline, stage, activity and import endpoint now declares the applicable Read/Create/Update/Delete permission through the existing `RequirePermissions` mechanism. A lead creator does not gain a Delete exception on CRM routes. Permission checks execute before the service method, and no frontend hiding is used as the security boundary.

The controller’s manual SLA sweep scans every organization. It now requires RootAdmin or RootSystem; a tenant editor’s Update grant does not authorize a global sweep.

## Detail repair

`websitemint/packages/ui/src/components/crm/leads/components/lead-detail.tsx` now resolves the selected lead ID against the current store record on every render. The details and next edit form therefore receive confirmed saved values rather than the original selection snapshot.

Deletion now awaits the server before closing the confirmation and details. A refused/failed delete keeps the lead and confirmation visible, displays the server error, and releases the busy state. In-flight repeat submissions are ignored.

## Verification scope

- Eight HTTP tests run the actual LeadsController and JwtAuthGuard authorization logic. Token verification is replaced with explicit test identities; service methods are spies. Narrow Read/Create/Update succeeds; self-created lead Delete returns 403 without a service call; an effective Delete grant succeeds. Reader/empty-grant/anonymous paths and the global sweep restriction are covered. Every ordinary controller route must declare a verb permission.
- Combined with invitation/direct-role tests, **60 backend tests pass**.
- Five tests execute actual detail selection and delete handlers, covering immediate fresh values, unrelated lead isolation, waiting for server success, visible denied deletion and duplicate submission prevention.
- Source diff checks pass. Outputs: `assets/lead-action-permissions/backend-tests.txt` and `ui-tests.txt`.

The combined build passed and the running API/Studio now load this follow-up: API 98501 is healthy, emitted controller Delete guards were checked, and Studio 98017 serves the corrected source. Root owns the next fresh zero-value probe and actual UI refused-delete/readback proof after a coordinated build/restart. This implementation worker has not mutated users or leads.

## Fresh local proof and additional service-layer correction

After the compiled backend repair, fresh Nora Sales/LeadEditor access created the zero-value Permissions Verified probe (`6aaef2896408c738121ec060`, POST 201), updated its title (PUT 200) and saw the saved title immediately in the detail view (capture 50). DELETE returned **403 Forbidden**, proving the backend now refuses the action.

The first frontend retest still failed: the drawer closed and the lead disappeared from the board despite the 403 (capture 51); full reload restored it (52). The initial handler-only test did not cover the service layer. `LeadsService.deleteLead` caught the thrown error and returned false, which the store ignored. The service now propagates the error and requires `success: true`; the store requires `true` before changing records or selection.

A new integration test runs actual local HTTP responses through the source request error guard, request-queue `processData`, LeadsService method, pipeline-store deletion and detail handler. HTTP 403 preserves the lead and selected ID, keeps confirmation/details open and surfaces Forbidden. Confirmed HTTP 200 deletes only the selected record and closes; HTTP 200 with `success: false` cannot pretend success. Combined with the existing detail tests, **8 UI tests pass**. Output: `assets/lead-action-permissions/delete-integration-tests.txt`.

The backend refusal and immediate title readback are live-verified. The additional service/store fix is saved in source and awaits Studio restart and another actual refusal retest on the retained probe. Do not yet claim visible UI refusal has passed.

## Final visible-refusal proof

After the frontend service/store repair was loaded, Nora repeated Delete against the retained Permissions Verified probe. The server returned 403, the confirmation remained open with Forbidden, and the record card remained present (capture `assets/local-roles/55-delete-refusal-visible-and-record-retained.png`). This completes the actual backend refusal plus frontend state/error proof. No successful deletion is claimed for Nora. Immediate saved-title readback is capture 50; full-reload retention is capture 52.

The review runtime subsequently moved to the isolated API on port 3311, with owned Studio HTTP/socket configuration pointed at it. The same compiled permission and service changes are present; background jobs are explicitly outside this replica’s verification scope.

## Completed browser verification

Nora’s fresh session held Sales/LeadEditor only, with no direct roles. Creating Permissions Verified returned201; changing its title returned200 and appeared immediately (capture50). Delete returned403. The first frontend pass exposed service/store error swallowing; after that additional repair, the actual confirmation showed **Forbidden**, kept the record visible and stayed open (capture55, visually inspected). Reload before final retest also confirmed the record remained (52).

The owner then deleted only that disposable record through the same UI: DELETE200, followed by reload showing the original Maya Bennett and Daniel Reyes records still present (61–62). The restricted refusal and authorised success were both exercised. Evidence: [narrow-role request statuses](assets/local-roles/repaired-lead-operation-responses.json), [visible refusal](assets/local-roles/55-delete-refusal-visible-and-record-retained.png), [owner success](assets/local-roles/owner-cleanup-responses.json).
