PC3 Phase 1.3: backend avatar loadout persistence
New avatar_loadouts table (one row per user, loadout stored as a single JSONB document matching the client's AvatarLoadout.to_json() shape) and GET/PUT /api/avatar/loadout. The server has no copy of the asset catalog, so it validates only the envelope (JSON object, non-empty base_body, object slots, size cap) -- which-asset-in-which-slot validation stays client-side. New accounts get a server default (base_human_male_01, no slots) with is_default:true so the client can tell "never customised" from "customised back to base". - models/avatar_loadout.py + 0004 migration + User.avatar_loadout relation - routers/avatar.py wired into main.py - tests/test_avatar_loadout.py (7 tests: default, save/read, replace, validation, auth) -- full suite 68 passed - lifted the duplicated _override_db fixture from 3 test files into conftest.py (new test files need it too) - docs/architecture/avatar.md §9a: the persistence design Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
@@ -449,6 +449,37 @@ The catalog is a JSON manifest mapping asset ids to metadata and resource paths:
|
||||
|
||||
---
|
||||
|
||||
## 9a. Loadout Persistence (implemented, PC3 Phase 1)
|
||||
|
||||
**Storage.** One row per user in `avatar_loadouts` (`user_id` PK/FK,
|
||||
`data` JSONB, timestamps). The entire loadout is stored as a single JSON
|
||||
document in the exact shape the client's `AvatarLoadout.to_json()` emits
|
||||
(`{"base_body": str, "slots": {...}, "skin_tone"?: {r,g,b,a}}`) rather than
|
||||
exploded into columns.
|
||||
|
||||
**Why one JSON blob, not columns.** The server has no copy of the asset
|
||||
catalog (`assets/avatars/catalog.json` ships with the client). It therefore
|
||||
cannot meaningfully validate *which* asset belongs in *which* slot, whether
|
||||
an asset exists, or slot-exclusivity rules — that validation stays on the
|
||||
client (`AvatarLoadout.validate()` against the loaded `AvatarCatalog`). The
|
||||
server enforces only the envelope: a JSON object with a non-empty string
|
||||
`base_body`, an object `slots`, and a sane size cap. Keeping the schema
|
||||
opaque to the server also means catalog/loadout-shape changes don't need a
|
||||
migration.
|
||||
|
||||
**Endpoints** (`/api/avatar`, bearer auth):
|
||||
|
||||
| Method | Path | Behaviour |
|
||||
|---|---|---|
|
||||
| `GET` | `/loadout` | The user's saved loadout, or the server default (`{"base_body": "base_human_male_01", "slots": {}}`, `is_default: true`) if they have never saved one. |
|
||||
| `PUT` | `/loadout` | Replace the user's loadout (full document, not a patch). |
|
||||
|
||||
**Defaults.** A brand-new account has no row; `GET` returns
|
||||
`models.avatar_loadout.DEFAULT_LOADOUT` with `is_default: true` so the client
|
||||
can distinguish "never customised" from "customised back to the base look".
|
||||
|
||||
---
|
||||
|
||||
## 10. Summary
|
||||
|
||||
| Concern | Decision |
|
||||
|
||||
Reference in New Issue
Block a user