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 <cursoragent@cursor.com>
8.6 KiB
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
IDetectionClassServiceinterface +DetectionClassServiceimplementation inAzaion.Services/, injected via DI alongsideIUserService(the existing pattern inProgram.cs).- DTOs in
Azaion.Common/Requests/:CreateDetectionClassRequest,UpdateDetectionClassRequest. Field set:name,shortName,color,maxSizeM(andphotoModeif the live backend already supports it for ADD — verify via the existing read-path service before adding). - 3 minimal-API handlers in
Program.csmounted under the existing/classesprefix, each callingRequireAuthorization(apiAdminPolicy)per the/usersprecedent. - DB schema work: verify
detection_classestable exists on the admin/ DB. If it does not, add a migration following the existingAzaion.Test/docker.testschema-init pattern. - Tests in
Azaion.Testmirroring 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 (likelyannotations/); if it is NOTadmin/, do NOT migrate the read path toadmin/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
/usersdoes today (presumably hard-delete).
Implementation Details
Endpoints (illustrative — match the /users pattern in Program.cs)
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:
- Re-run the verification grep against
Azaion.AdminApi/Program.cs. - If routes exist → move
ui/_docs/02_tasks/backlog/AZ-512_*.mdback totodo/and re-attempt batch 15. - If routes still missing → leave the UI leftover as-is, surface to the user.