From e40ea3eeaa69418dd2b3e01e5dcfd4e4fdf83d74 Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Wed, 13 May 2026 03:58:45 +0300 Subject: [PATCH] [AZ-513] Add task spec: /classes CRUD routes (POST + PATCH + DELETE) Cross-workspace prerequisite filed by ui/ autodev cycle 3 batch 15 BLOCKING gate. The ui/ workspace's AdminPage.tsx already calls POST /classes and DELETE /classes/{id} today (broken end-to-end against this service - pre-existing bug); ui/ AZ-512 needs PATCH /classes/{id} for the in-place edit affordance promised by Architecture Vision P12. Spec covers: - POST /classes - creates a detection class - PATCH /classes/{id} - partial-merge update - DELETE /classes/{id} - removes a class - All three guarded by apiAdminPolicy (matches /users precedent) - IDetectionClassService + DetectionClassService in Azaion.Services - DTOs in Azaion.Common/Requests - 10 ACs covering happy + auth + 404 paths Tracker: AZ-513 (Jira project AZ, parent epic AZ-509, Blocks AZ-512). Cross-workspace context: ui/_docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md Complexity: 3 points. Co-authored-by: Cursor --- .../todo/AZ-513_classes_crud_routes.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 _docs/02_tasks/todo/AZ-513_classes_crud_routes.md diff --git a/_docs/02_tasks/todo/AZ-513_classes_crud_routes.md b/_docs/02_tasks/todo/AZ-513_classes_crud_routes.md new file mode 100644 index 0000000..eaab44a --- /dev/null +++ b/_docs/02_tasks/todo/AZ-513_classes_crud_routes.md @@ -0,0 +1,140 @@ +# Detection Classes CRUD Routes + +**Task**: AZ-513_classes_crud_routes +**Name**: POST + PATCH + DELETE /classes routes for detection-class CRUD +**Description**: Add the three missing /classes endpoints to the admin API. The UI already calls POST and DELETE today (broken end-to-end — pre-existing bug); PATCH is required by AZ-512 (UI workspace) for the in-place edit affordance. +**Complexity**: 3 points +**Dependencies**: None on the admin/ side. **Unblocks**: AZ-512 in the UI workspace. +**Component**: Admin API +**Tracker**: AZ-513 +**Epic**: AZ-509 +**Filed by**: cross-workspace prerequisite from `ui/` autodev cycle 3 batch 15 (BLOCKING gate). See `ui/_docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md`. + +## Problem + +The UI workspace (`ui/src/features/admin/AdminPage.tsx`) calls three `/classes` endpoints today, but the admin/ service does not expose any of them — verified 2026-05-13 by grepping `Azaion.AdminApi/Program.cs` for `MapPost|MapPatch|MapDelete` against `classes` (zero matches): + +- `POST /api/admin/classes` — UI calls this to add a new detection class (`handleAddClass`). Today: 404. **Pre-existing bug** — the "Add" button on the Detection Classes table has been broken end-to-end against the live admin/ service. +- `DELETE /api/admin/classes/{id}` — UI calls this to delete a class (`handleDeleteClass`). Today: 404. **Pre-existing bug** — the row delete affordance has been broken end-to-end too. +- `PATCH /api/admin/classes/{id}` — UI does NOT call this today; AZ-512 (UI workspace) needs to call it to deliver the in-place edit affordance promised by Architecture Vision principle P12 ("the Admin Detection Classes table supports the full CRUD surface, not add+delete only"). Currently the route does not exist. + +The UI's `endpoints.admin.classes()` and `endpoints.admin.class(id)` builders pin the URLs (`ui/src/api/endpoints.ts`) and are wire-contract tested in `ui/src/api/endpoints.test.ts`. nginx routes `/api/admin/` → `http://admin:8080/`, so inside this service the paths are `/classes` and `/classes/{id}`. + +## Outcome + +- POST /classes creates a new detection class. +- PATCH /classes/{id} updates an existing detection class (full or partial body — both accepted). +- DELETE /classes/{id} deletes a detection class. +- All three guarded by the same auth middleware as `/users` (`apiAdminPolicy`). +- After this lands, the UI's existing add/delete affordances start working end-to-end, and AZ-512 (UI workspace) un-blocks for its in-place edit form. + +## Scope + +### Included + +- `IDetectionClassService` interface + `DetectionClassService` implementation in `Azaion.Services/`, injected via DI alongside `IUserService` (the existing pattern in `Program.cs`). +- DTOs in `Azaion.Common/Requests/`: `CreateDetectionClassRequest`, `UpdateDetectionClassRequest`. Field set: `name`, `shortName`, `color`, `maxSizeM` (and `photoMode` if the live backend already supports it for ADD — verify via the existing read-path service before adding). +- 3 minimal-API handlers in `Program.cs` mounted under the existing `/classes` prefix, each calling `RequireAuthorization(apiAdminPolicy)` per the `/users` precedent. +- DB schema work: verify `detection_classes` table exists on the admin/ DB. If it does not, add a migration following the existing `Azaion.Test`/`docker.test` schema-init pattern. +- Tests in `Azaion.Test` mirroring the existing user-CRUD test shape. + +### Excluded + +- UI-side changes — handled by AZ-512 in the UI workspace once this lands. +- Read path `GET /classes` — UI today already reads classes successfully. Verify which service serves the read (likely `annotations/`); if it is NOT `admin/`, do NOT migrate the read path to `admin/` as part of this ticket. Leave the read wire shape alone — the UI will keep calling whichever service answers today. +- Bulk operations (no batch POST/PATCH/DELETE) — out of scope. +- Soft-delete vs hard-delete policy — match whatever `/users` does today (presumably hard-delete). + +## Implementation Details + +### Endpoints (illustrative — match the `/users` pattern in `Program.cs`) + +```csharp +app.MapPost("/classes", + async (IDetectionClassService svc, CreateDetectionClassRequest req, CancellationToken ct) + => await svc.Create(req, ct)) + .RequireAuthorization(apiAdminPolicy) + .WithSummary("Creates a new detection class"); + +app.MapPatch("/classes/{id}", + async (IDetectionClassService svc, int id, UpdateDetectionClassRequest req, CancellationToken ct) + => await svc.Update(id, req, ct)) + .RequireAuthorization(apiAdminPolicy) + .WithSummary("Updates an existing detection class (partial-merge accepted)"); + +app.MapDelete("/classes/{id}", + async (IDetectionClassService svc, int id, CancellationToken ct) + => await svc.Delete(id, ct)) + .RequireAuthorization(apiAdminPolicy) + .WithSummary("Deletes a detection class"); +``` + +### PATCH semantics + +The UI sends the complete body on edit (per AZ-512 spec Risk 2 mitigation — "send all fields on PATCH so the implementer doesn't have to choose between full-replace and partial-merge"). Implementing PATCH as a partial-merge over the loaded entity is therefore safe: missing fields keep their existing value, present fields overwrite. Either full-replace or partial-merge works for the UI; partial-merge is the more conservative choice. + +## Acceptance Criteria + +**AC-1: POST /classes creates a class** +Given a valid ApiAdmin JWT and a body `{ "name": "Tank", "shortName": "T", "color": "#FF0000", "maxSizeM": 5.0 }` +When POST /classes is called +Then 200 or 201 is returned with the created class object including its assigned id + +**AC-2: POST /classes requires ApiAdmin authorization** +Given a request without a JWT or with a non-ApiAdmin JWT +When POST /classes is called +Then 401 or 403 is returned + +**AC-3: PATCH /classes/{id} updates an existing class** +Given a class with id 7 exists with `name: "Tank"` +When PATCH /classes/7 is called with body `{ "name": "Heavy Tank", "shortName": "T", "color": "#FF0000", "maxSizeM": 5.0 }` +Then 200 is returned with the updated class object reflecting `name: "Heavy Tank"` + +**AC-4: PATCH /classes/{id} accepts partial body** +Given a class with id 7 exists with `name: "Tank", color: "#FF0000", maxSizeM: 5.0` +When PATCH /classes/7 is called with body `{ "color": "#00FF00" }` (only the changed field) +Then 200 is returned with `color: "#00FF00"` and the other fields unchanged + +**AC-5: PATCH /classes/{id} returns 404 for unknown id** +Given no class with id 9999 exists +When PATCH /classes/9999 is called +Then 404 is returned + +**AC-6: PATCH /classes/{id} requires ApiAdmin authorization** +Given a request without a JWT or with a non-ApiAdmin JWT +When PATCH /classes/{id} is called +Then 401 or 403 is returned + +**AC-7: DELETE /classes/{id} removes a class** +Given a class with id 7 exists +When DELETE /classes/7 is called +Then 200 or 204 is returned and the class is gone from the DB + +**AC-8: DELETE /classes/{id} returns 404 for unknown id** +Given no class with id 9999 exists +When DELETE /classes/9999 is called +Then 404 is returned (or 200/204 if idempotent-delete is the established pattern in this service — match `/users` behavior) + +**AC-9: DELETE /classes/{id} requires ApiAdmin authorization** +Given a request without a JWT or with a non-ApiAdmin JWT +When DELETE /classes/{id} is called +Then 401 or 403 is returned + +**AC-10: After this ticket lands, UI add/delete affordances work end-to-end** +Given `ui/src/features/admin/AdminPage.tsx` is deployed against this admin/ build +When the user clicks the existing "Add Class" button or the row delete button on the Detection Classes table +Then the operation succeeds against the live admin/ service (no 404) +(This AC is verified by the UI workspace's e2e harness in `ui/e2e/`, not by tests in this workspace. Cross-workspace coordination point: notify the UI team via the AZ-509 epic when this ships so they can re-attempt AZ-512 batch 15.) + +## Cross-workspace notes + +This ticket exists because of a BLOCKING gate hit during the UI workspace's autodev cycle 3. Full context: + +- UI leftover record: `ui/_docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md` +- UI task spec (parked in backlog until this ships): `ui/_docs/02_tasks/backlog/AZ-512_admin_edit_detection_class.md` +- UI epic linkage: AZ-509 (Cycle 3 — Auth bootstrap fix + classColors carve-out + admin class edit). AZ-513 is linked under AZ-509 because that is the epic the prerequisite unblocks. + +When this ticket ships, the UI workspace's leftovers replay step (next `/autodev` invocation) will: +1. Re-run the verification grep against `Azaion.AdminApi/Program.cs`. +2. If routes exist → move `ui/_docs/02_tasks/backlog/AZ-512_*.md` back to `todo/` and re-attempt batch 15. +3. If routes still missing → leave the UI leftover as-is, surface to the user.