feat(backend): admin-only list of every campaign on the instance, metadata only (#437) #538

Merged
claude-bot merged 1 commit from feat/437-admin-campaigns into main 2026-09-06 20:45:15 +00:00
Contributor

Backend half of #437 (#535 phase 4). The issue stays open for the admin screen, which lands in the rebuild's last lane.

What changes

  • GET /api/admin/campaigns in the admin router: every campaign on the instance regardless of the caller's membership, sorted case-insensitively by name with the id as a stable tie-break. Each row carries id, name, game system, the GMs in join order, gm_count (two-GM campaigns are the #411 shape), member and session counts, has_guild, has_notification_channel, discord_delivery_failing (true only when #369's status says failing; untested reads as false), retention_overrides holding only the fields actually set, is_archived, created_at, deleted_at. Soft-deleted campaigns are excluded unless ?include_deleted=true.
  • Metadata only, by decision. No notes, summaries, wiki, transcripts, description, webhook, invite code or share token. The test asserts the exact key set and greps the response body for seeded secrets to catch a leak through a nested object. Drill-through, if ever wanted, is a separate issue with an audit entry.
  • Two SELECTs regardless of campaign count (correlated count subqueries plus one keyed GM fetch); a test with a statement listener pins that at exactly two across five campaigns.
  • Documented in docs/API.md; unpaginated like the other admin lists. No migration; nothing on the bot contract.

Two judgment calls, stated in the code: session_count includes cancelled sessions (it answers "how much does this campaign hold", unlike the participation stat in the admin user detail); the response models live inline in admin.py, which is that file's existing convention.

Verification (Docker): full backend suite 2,516 passed, 13 skipped (+12 new); ruff check and format clean.

🤖 Generated with Claude Code

Backend half of #437 (#535 phase 4). The issue stays open for the admin screen, which lands in the rebuild's last lane. **What changes** - `GET /api/admin/campaigns` in the admin router: every campaign on the instance regardless of the caller's membership, sorted case-insensitively by name with the id as a stable tie-break. Each row carries id, name, game system, the GMs in join order, `gm_count` (two-GM campaigns are the #411 shape), member and session counts, `has_guild`, `has_notification_channel`, `discord_delivery_failing` (true only when #369's status says `failing`; untested reads as false), `retention_overrides` holding only the fields actually set, `is_archived`, `created_at`, `deleted_at`. Soft-deleted campaigns are excluded unless `?include_deleted=true`. - **Metadata only, by decision.** No notes, summaries, wiki, transcripts, description, webhook, invite code or share token. The test asserts the exact key set and greps the response body for seeded secrets to catch a leak through a nested object. Drill-through, if ever wanted, is a separate issue with an audit entry. - Two SELECTs regardless of campaign count (correlated count subqueries plus one keyed GM fetch); a test with a statement listener pins that at exactly two across five campaigns. - Documented in `docs/API.md`; unpaginated like the other admin lists. No migration; nothing on the bot contract. Two judgment calls, stated in the code: `session_count` includes cancelled sessions (it answers "how much does this campaign hold", unlike the participation stat in the admin user detail); the response models live inline in `admin.py`, which is that file's existing convention. **Verification** (Docker): full backend suite 2,516 passed, 13 skipped (+12 new); ruff check and format clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(backend): admin-only list of every campaign on the instance, metadata only (#437)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m15s
CI / Backend lint (ruff) (pull_request) Successful in 36s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m46s
CI / Docker image build (pull_request) Successful in 20s
CI / Bot/backend version sync (pull_request) Successful in 35s
CI / Bot tests and audit (pull_request) Successful in 2m27s
CI / Backend migration, tests, and audit (pull_request) Successful in 8m8s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 14m18s
80f49ab52e
Campaign visibility is scoped to membership everywhere else, so an instance
admin had no way to answer the first question any support request raises: what
campaigns exist here, and who runs them. There was no campaign view in the admin
panel at all, and a restored database that left the operator's own login holding
no memberships made the instance read as having no campaigns.

`GET /api/admin/campaigns` returns every campaign regardless of the caller's
memberships, sorted by name (case-insensitively — the C collation would file
"aztec" after "Zephyr"). Each row carries the GMs in join order, member/session/
GM counts, `has_guild` / `has_notification_channel` / `discord_delivery_failing`
for bot-routing questions, and the retention fields the campaign actually
overrides. That covers the four things an operator is asked: diagnose a GM's
report, find the per-campaign retention overrides (#119, #427), spot the two-GM
campaigns behind the journal 500 (#411), and confirm a campaign's guild and
channel when notifications go missing (#369).

Metadata only, deliberately: no notes, summaries, wiki, transcripts, not even
the description. "The operator can diagnose Discord routing" and "the operator
can read your table's private notes" are different powers and only the first is
granted here; drill-through is a separate decision and would come with an audit
entry. A test pins the response key set exactly so widening it fails CI rather
than passing review.

Two queries whatever the instance holds — counts are correlated scalar
subqueries, the GMs are one keyed fetch — and a test asserts exactly two SELECTs
across five campaigns, because the relationship-loading version that walks the
sessions table once per campaign produces an identical response.

Archived campaigns are always listed (archiving is a lifecycle state, not a
deletion); soft-deleted ones (#405) are excluded unless `?include_deleted=true`,
which returns them with `deleted_at` populated. Unpaginated, matching
`/admin/users` and `/admin/recordings`; only the unbounded audit log paginates.

No migration — every column this reads already exists. The Admin screen that
consumes it lands later in the milestone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rbrooks force-pushed feat/437-admin-campaigns from 80f49ab52e
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m15s
CI / Backend lint (ruff) (pull_request) Successful in 36s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m46s
CI / Docker image build (pull_request) Successful in 20s
CI / Bot/backend version sync (pull_request) Successful in 35s
CI / Bot tests and audit (pull_request) Successful in 2m27s
CI / Backend migration, tests, and audit (pull_request) Successful in 8m8s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 14m18s
to 0f6177cafc
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m13s
CI / Backend lint (ruff) (pull_request) Successful in 31s
CI / Docker image build (pull_request) Successful in 12s
CI / Bot/backend version sync (pull_request) Successful in 26s
CI / Bot tests and audit (pull_request) Successful in 2m16s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m41s
CI / Backend migration, tests, and audit (pull_request) Successful in 9m47s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 15m44s
2026-09-06 20:28:05 +00:00
Compare
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-09-06 20:30:26 +00:00
claude-bot deleted branch feat/437-admin-campaigns 2026-09-06 20:45:16 +00:00
Sign in to join this conversation.
No description provided.