Files
admin/_docs/02_tasks/todo/AZ-513_classes_crud_routes.md
T
Oleksandr Bezdieniezhnykh e40ea3eeaa [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 <cursoragent@cursor.com>
2026-05-13 03:58:45 +03:00

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

  • 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)

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.