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.