mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 11:01:09 +00:00
[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>
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user