`Query.myCrate` is in `schema.graphql` and `isInCrate` lets a user add and remove albums, but there is no resolver for `myCrate` and no page that lists what is in the crate. A client that queries it today gets a non-null field error, and the README lists it as planned for M4 without an owner. ## Tasks ### Server - [ ] `myCrate(first, after)` resolver with keyset paging on `(added_at DESC, id DESC)`. The index for it already exists: `crate_item_user_added_at_idx` on `(user_id, added_at, id)`. - [ ] Cursor encoding/decoding with the same validation as the album cursors, so a malformed or foreign cursor fails instead of paging from the start. - [ ] `totalCount` for the user's crate. - [ ] An `albumById` DataLoader for `CrateItem.album`. The existing loaders fan out from albums; without this, a page of 20 crate items runs 20 queries. - [ ] Anonymous callers get an **empty connection**, not an error: the field is non-null, and reads are supposed to degrade rather than fail (architecture decision 5). Record the decision. - [ ] PostgreSQL tests: paging order and stability when several items share `added_at`, cross-user isolation (user A never sees B's crate), anonymous returns empty. ### Client - [ ] `CratePage` at `/crate`, linked from the header, reusing `AlbumCard` and `CrateButton`. - [ ] `myCrate` cache policy, and eviction of the field after `addToCrate`/`removeFromCrate` so a toggle on the detail page does not leave a stale list. - [ ] Empty, loading and error states with `role="status"`, plus an axe-core test, matching the album page. - [ ] Component test: list renders, remove removes, empty state appears. ### Documentation - [ ] README implementation status and the summary in `docs/architecture.md`. - [ ] AI usage log row. ## Acceptance A user can open their crate, see the albums they saved in the order they saved them, remove one, and reload the page to find the list as expected. Another user's crate is never visible. The field is never returned as an error for an anonymous request.