Snapshot now populates frames + devices from the DB (slice 1 left them as
empty arrays).
Frame store:
- CreateFrame requires positive width/height; rejects empty name; UNIQUE
(project_id, name) collisions surface as ErrConflict via mapWriteErr.
- GetFrame is project-scoped — wrong-project read returns ErrNotFound.
- UpdateFrame applies a partial; project_id is not exposed (moving a
frame across projects would orphan its devices).
- DeleteFrame relies on the schema's ON DELETE SET NULL to drop
devices' frame_id refs cleanly; verified by test.
Device store:
- CreateDevice defaults color to #1e1e1e if blank; rejects empty name,
non-positive size; validates frame_id is in the same project (returns
ErrInvalidInput on cross-project ref).
- UpdateDevice uses a FrameRef tri-state for frame_id so callers can
distinguish "leave alone" from "clear to NULL" from "move to frame X".
- Cross-project frame_id on PATCH is rejected with ErrInvalidInput.
- ListDevices supports an optional frame_id filter.
13 new table-driven tests, all green with -race.