feat(frontend): Content packs screen for admins and GMs, with the import affirmation (#553) #563
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/553-content-packs-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 1 of #553, lane 6 of the content-packs spec §10: the one screen, mounted twice.
The screen.
ContentPacks.jsxis an Admin → Content packs tab (instance scope, plus the starters list) and a panel on the campaign page for GMs (campaign scope). Upload a file or give a URL; pick the game system; format auto-detected with an override; default visibility; the affirmation text from the server with a required checkbox; progress (polling the pack while it isimporting, outside the shared GET cache) and warnings; the list with status, licence, counts, and disable/enable (admins only, as the API is), remove and export. The importer's name comes from a map the parent already holds; an unknown id degrades to the import date rather than a guess.A fix on the backend side. The affirmation 422 put an object in
detail, which broke the project's one error shape ({"detail": "Human-readable message"}) and madeclient.jsshow "[object Object]" for exactly the error whose text matters most. It is now{"detail": "<message>", "affirmation": {"version", "text"}}, returned as aJSONResponsefrom both import paths;docs/API.mdanddocs/CONTENT-PACKS.mddescribe the real body and the frontend reads the sibling key. The old shape was never released, so there is no changelog entry for it.Stacked on #561 (merged). React Compiler rules hold: no state set inside an effect, loading derived, polling driven from the interval callback, keyed inputs so file/URL switching remounts.
Tests: 28 frontend (wire shapes in
contentPacks.test.js, behaviour inContentPacks.test.jsx, mount guards in the page tests); vitest 840 across the suite, eslint clean, vite build green. Backend: 90 in import/admin/notices and 39 in url-fetch/task/export/migration after the 422 change.🤖 Generated with Claude Code
Every error body in this project is `{"detail": "<human-readable message>"}` (CLAUDE.md, "Error response format"), and the web client leans on it: the shared fetch wrapper puts `detail` straight into an `Error`'s message. The content-pack foundation put an object there instead — `{message, affirmation_version, affirmation_text}` — so the one refusal a person most needs to read rendered as "[object Object]" in any client that did the ordinary thing with `err.message`. The body is now: {"detail": "<the message>", "affirmation": {"version": "2026-09", "text": "<the wording>"}} `AffirmationRequired.detail` is the message string, so any future raiser can hand it to `HTTPException` and still be conformant; `AffirmationRequired.payload` assembles the whole body, in one place, so the two router call sites cannot drift. Those two sites return a `JSONResponse` with the 422 rather than raising, because `HTTPException` can only put things inside `detail` — which is the problem. Nothing else about the refusal changes: still 422, still nothing fetched, parsed or written before the affirmation passes. Client-side, `affirmationFromError` reads `err.body.affirmation` for the wording and `err.message` for the sentence, which is now a sentence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>