feat: party loot ledger, XP & gold tracking (#112) #192

Merged
claude-bot merged 2 commits from feat/112-loot-xp-ledger into main 2026-07-18 00:49:09 +00:00
Contributor

Closes #112. First feature of the v3.7.0 Table Tools milestone — the foundational ledger data model the loot generator (#149) will build on.

Backend (a35d611)

  • New models (models/loot.py): LootEntry (soft-deletable), XpAward (party/member scope), GoldLedgerEntry. Holder/member FKs are nullable → NULL means party pool / party fund; ondelete SET NULL so removing a member preserves history as party-owned.
  • Gold amounts stored as Decimal (serialized as JSON strings) to avoid float drift.
  • Migration e2f3a4b5c6d7 (down_revision d1e2f3a4b5c6) — round-trips clean (upgrade → downgrade → upgrade) on a reset test DB.
  • Endpoints inline in campaigns.py: GET/POST /{id}/loot, PATCH /loot/{lid}, POST /loot/{lid}/transfer, DELETE /loot/{lid}; GET/POST /{id}/xp + DELETE; GET/POST /{id}/gold + DELETE; GET /{id}/loot-totals. Session-scoped reads under /sessions/{id}/{loot,xp,gold}.
  • Permissions: GM full CRUD; players read-only unless the campaign's players_can_add_loot is on, and then may modify only entries they created (creator-only, enforced server-side with 403).
  • Export/import schema bumped 7→8 (member-scoped XP collapses to party on import, since imported members are unlinked stubs).

Frontend (1293a3d)

  • api/loot.js — shared-client wrappers; gold passed through as opaque strings.
  • components/LootLedger.jsx — campaign panel: loot table (holder filter + transfer/edit/delete), party & per-member XP totals + award form, gold ledger with running balances + add form. Write controls gated on isGm || players_can_add_loot; per-entry ownership is the backend's 403 to enforce, surfaced inline.
  • CampaignDetail — renders the panel; adds the "players can add loot/XP" setting toggle.
  • SessionDetail — read-only "Loot & XP this session" block.

Tests

  • Backend: 21 new (tests/test_loot.py); full suite 456 pass.
  • Frontend: +33 (loot api 17, LootLedger 14, SessionDetail 2); 202/202 pass, eslint clean, vite build succeeds.

Notes

  • No bot-contract change (BOT_CONTRACT_VERSION untouched) — no /api/bot/* surface touched.
  • Per-entry ownership is enforced only server-side; the client shows write controls to any eligible user and surfaces the backend 403 if they touch an entry they didn't create.

🤖 Generated with Claude Code

Closes #112. First feature of the **v3.7.0 Table Tools** milestone — the foundational ledger data model the loot generator (#149) will build on. ## Backend (`a35d611`) - New models (`models/loot.py`): `LootEntry` (soft-deletable), `XpAward` (party/member scope), `GoldLedgerEntry`. Holder/member FKs are nullable → `NULL` means party pool / party fund; `ondelete SET NULL` so removing a member preserves history as party-owned. - Gold amounts stored as `Decimal` (serialized as JSON **strings**) to avoid float drift. - Migration `e2f3a4b5c6d7` (down_revision `d1e2f3a4b5c6`) — round-trips clean (upgrade → downgrade → upgrade) on a reset test DB. - Endpoints inline in `campaigns.py`: `GET/POST /{id}/loot`, `PATCH /loot/{lid}`, `POST /loot/{lid}/transfer`, `DELETE /loot/{lid}`; `GET/POST /{id}/xp` + `DELETE`; `GET/POST /{id}/gold` + `DELETE`; `GET /{id}/loot-totals`. Session-scoped reads under `/sessions/{id}/{loot,xp,gold}`. - Permissions: GM full CRUD; players read-only unless the campaign's `players_can_add_loot` is on, and then may modify only entries they created (creator-only, enforced server-side with 403). - Export/import schema bumped 7→8 (member-scoped XP collapses to party on import, since imported members are unlinked stubs). ## Frontend (`1293a3d`) - `api/loot.js` — shared-client wrappers; gold passed through as opaque strings. - `components/LootLedger.jsx` — campaign panel: loot table (holder filter + transfer/edit/delete), party & per-member XP totals + award form, gold ledger with running balances + add form. Write controls gated on `isGm || players_can_add_loot`; per-entry ownership is the backend's 403 to enforce, surfaced inline. - `CampaignDetail` — renders the panel; adds the "players can add loot/XP" setting toggle. - `SessionDetail` — read-only "Loot & XP this session" block. ## Tests - Backend: 21 new (`tests/test_loot.py`); full suite **456 pass**. - Frontend: +33 (loot api 17, LootLedger 14, SessionDetail 2); **202/202 pass**, eslint clean, vite build succeeds. ## Notes - No bot-contract change (`BOT_CONTRACT_VERSION` untouched) — no `/api/bot/*` surface touched. - Per-entry ownership is enforced only server-side; the client shows write controls to any eligible user and surfaces the backend 403 if they touch an entry they didn't create. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
"Who's carrying the gem / did we split that 800gp" is the most universal unmet
bookkeeping need at the table. Add three campaign-scoped models with CRUD,
totals, permissions, and export/import.

Models (migration e2f3a4b5c6d7, down_revision d1e2f3a4b5c6):
- LootEntry: name/qty/est_value/currency, holder_user_id (nullable FK users;
  NULL = party pool), soft-delete via deleted_at, session link.
- XpAward: amount + scope (party|member); member_user_id required for member scope.
- GoldLedgerEntry: signed Numeric amount, member_user_id (NULL = party fund).
- campaigns.players_can_add_loot (bool, default off).
Holder/member are modelled as nullable user FKs (CampaignMember has a composite
PK, so a member is identified by user within the campaign) with ondelete SET NULL.

Endpoints (inline in campaigns.py, per the arcs/threads precedent): campaign-scoped
list/create/update/delete under /loot, /xp, /gold; /loot-totals; and session-scoped
GET /sessions/{id}/{loot,xp,gold}. Permissions: GM full CRUD; players read-only,
create only when players_can_add_loot is on, and may modify only entries they
created.

Totals: party_xp and per-member XP kept separate (a party award isn't auto-split);
loot grouped party_pool vs per_holder; gold signed total + party_fund + per_member.

Export/import: EXPORT_SCHEMA_VERSION 7 -> 8; the three tables ride the campaign zip
(FKs via the existing session/member key maps; v7 zips still import with empty
ledgers). Note: member-scoped XP collapses to party on import because members
import as unlinked stubs with no resolvable user id (same policy as attendance).

Gold amounts are Decimal, so they serialize as JSON strings (precision) — the
frontend must treat gold as strings.

Verified via the dev-host harness: migration applies from base and downgrade -1 +
upgrade round-trips; 456 passed (21 new incl. transfer-totals + export/import
round-trip + permission matrix); ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(frontend): loot ledger, XP & gold tracking UI (#112)
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 1m12s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m58s
CI / Bot tests and audit (pull_request) Successful in 2m19s
CI / Backend migration, tests, and audit (pull_request) Successful in 4m25s
CI / Docker image build (pull_request) Successful in 5m48s
1293a3d486
Adds the player-facing surface for the loot/XP/gold ledger backed by the
new campaign endpoints:

- api/loot.js: shared-client wrappers for campaign-scoped loot/XP/gold CRUD,
  transfer, combined loot-totals, and read-only session-scoped views.
- components/LootLedger.jsx: campaign panel with a loot table (holder filter +
  transfer/edit/delete), party & per-member XP totals with an award form, and
  a gold ledger with running balances and an add form. Write controls gated on
  isGm || players_can_add_loot; per-entry ownership enforced by the backend 403.
- CampaignDetail: renders LootLedger; adds the "players can add loot/XP"
  campaign setting toggle wired into the PATCH payload.
- SessionDetail: read-only "Loot & XP this session" block.

Gold amounts are treated as opaque Decimal strings end-to-end — never coerced
to Number; all sums come from the totals endpoint.

Tests: +33 (loot api 17, LootLedger 14, SessionDetail 2); 202/202 pass,
eslint clean, vite build succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/112-loot-xp-ledger 2026-07-18 00:49:10 +00:00
Sign in to join this conversation.
No description provided.