- GET /api/device-types — built-ins only (read-only).
- GET /api/projects/:pid/device-types — built-ins + project-custom merged.
- POST/PATCH/DELETE /api/projects/:pid/device-types — project-custom only.
Mutating a built-in row returns 403 via the new ErrForbidden → 403 map
in writeError.
- devicePatch / deviceCreate JSON shapes accept type_id (tri-state for
PATCH via the existing parseFrameRef helper applied to type_id too).
- POST /api/projects/:pid/devices with type_id seeds ports in one tx
server-side; response carries the device row + the snapshot will then
carry the new ports.
All 8 endpoints (list, create, patch, delete) for both resources. Path
params parsed via Go 1.22 ServeMux PathValue.
devicePatch uses json.RawMessage for frame_id so the wire format
distinguishes:
- key absent → leave as-is
- "frame_id": null → clear (device leaves all frames)
- "frame_id": 42 → move to that frame
parseFrameRef translates that into the store's db.FrameRef tri-state.
Sentinel-error mapping unchanged (writeError covers ErrInvalidInput,
ErrConflict, ErrNotFound, etc.). Cross-project frame_id refs surface as
400.