# 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.