feat(webapp): the server's owner, offers, About, the campaigns' and audit log's names, and Admin's overview (lane 9 A) #855

Merged
claude-bot merged 3 commits from build/lane9-a-owner-overview into main 2026-09-26 22:50:33 +00:00
Collaborator

Lane 9 (#735), server PR A: the owner's D140–D142 (A3, A4 and A5) and D148 (A11). No pages; those are 4a–4d.

The server's owner (D68, D140, D141)

Migration f1a3c5e7b9d2 (on c4e6a8b0d2f1) adds:

  • users.is_server_owner, with a partial unique index so there's at most one owner;
  • a server_ownership_offers table (one open offer at a time);
  • the notification kinds server_ownership_offer and campaign_handed_on.

Backfill (A3): the owner is whoever finished setup, if they're still an admin. Failing that, it's the earliest admin, and failing that, nobody.

Guards:

  • An admin can't remove the owner's admin status: 409, "Hand the server on first".
  • The owner can't be deleted, either by an admin or through /me. This comes on top of #837's last-admin guard.

Offers (A4):

  • Endpoints: offer, accept, decline and cancel, plus GET /admin/server-owner.
  • Offers lapse after 7 days, checked lazily, and the owner hears the outcome.
  • The offer rings the bell. The "Needs you" item is deferred to page 4a, because AttentionItem needs a server-level kind first.

Vanished owner: CLI python -m app.cli set_owner --email|--oidc-sub/--oidc-issuer, documented in docs/OPERATIONS.md.

UserResponse.is_server_owner is added.

Also

  • D56: PUT /admin/settings/member-search-scope.
  • About: GET|PUT /admin/settings/about.
    • The contact is split into an email and a link, by shape.
    • Saving resets the public numbers cache.
    • Changes are audited.
  • Campaigns:
    • the list gains delivery_failing_since, owner_last_seen_at and uses_own_ai;
    • new GET /admin/campaigns/{id}/members;
    • a hand-on notifies both owners.
  • Audit log:
    • filters by actor and campaign name, and by category;
    • responses carry actor_name (with "The system" for events no user triggered), campaign_name and target_label, resolved with two queries per page.
  • GET /admin/overview (A5, D142): a single request with cheap reads only, including the skipped setup steps with their section keys. The bot status is cached for 60 s, with a 2 s timeout.

Tests

  • New tests: test_server_owner.py and test_server_owner_migration.py, the latter covering the backfill cases, the CHECKs, and up, down and up.
  • 115 targeted tests pass, plus a wider regression pass (447 in total).
  • Migration: up, down and up on real Postgres 16.
  • ruff: clean.

Merge notes

  • B2 (another in-flight PR) also adds a migration on c4e6. Whichever merges second gets re-chained.
  • After #854, the new Depends(get_db) uses here are converted to scope="function" at merge.

🤖 Generated with Claude Code

Lane 9 (#735), server PR A: the owner's D140–D142 (A3, A4 and A5) and D148 (A11). No pages; those are 4a–4d. ## The server's owner (D68, D140, D141) **Migration `f1a3c5e7b9d2`** (on `c4e6a8b0d2f1`) adds: - `users.is_server_owner`, with a partial unique index so there's at most one owner; - a `server_ownership_offers` table (one open offer at a time); - the notification kinds `server_ownership_offer` and `campaign_handed_on`. **Backfill (A3):** the owner is whoever finished setup, if they're still an admin. Failing that, it's the earliest admin, and failing that, nobody. **Guards:** - An admin can't remove the owner's admin status: 409, "Hand the server on first". - The owner can't be deleted, either by an admin or through `/me`. This comes on top of #837's last-admin guard. **Offers (A4):** - Endpoints: offer, accept, decline and cancel, plus `GET /admin/server-owner`. - Offers lapse after 7 days, checked lazily, and the owner hears the outcome. - The offer rings the bell. The "Needs you" item is **deferred to page 4a**, because `AttentionItem` needs a server-level kind first. **Vanished owner:** CLI `python -m app.cli set_owner --email|--oidc-sub/--oidc-issuer`, documented in `docs/OPERATIONS.md`. **`UserResponse.is_server_owner`** is added. ## Also - **D56:** `PUT /admin/settings/member-search-scope`. - **About:** `GET|PUT /admin/settings/about`. - The contact is split into an email and a link, by shape. - Saving resets the public numbers cache. - Changes are audited. - **Campaigns:** - the list gains `delivery_failing_since`, `owner_last_seen_at` and `uses_own_ai`; - new `GET /admin/campaigns/{id}/members`; - a hand-on notifies both owners. - **Audit log:** - filters by `actor` and `campaign` name, and by `category`; - responses carry `actor_name` (with "The system" for events no user triggered), `campaign_name` and `target_label`, resolved with two queries per page. - **`GET /admin/overview` (A5, D142):** a single request with cheap reads only, including the skipped setup steps with their section keys. The bot status is cached for 60 s, with a 2 s timeout. ## Tests - **New tests:** `test_server_owner.py` and `test_server_owner_migration.py`, the latter covering the backfill cases, the CHECKs, and up, down and up. - **115 targeted tests pass,** plus a wider regression pass (447 in total). - **Migration:** up, down and up on real Postgres 16. - **ruff:** clean. ## Merge notes - **B2** (another in-flight PR) also adds a migration on `c4e6`. Whichever merges second gets re-chained. - **After #854,** the new `Depends(get_db)` uses here are converted to `scope="function"` at merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(webapp): the server has an owner (D68, IS-397, lane 9 A)
Some checks failed
CI / Backend migration, tests, and audit (pull_request) Failing after 6m6s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m46s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Bot tests and audit (pull_request) Successful in 4m52s
CI / Bot/backend version sync (pull_request) Successful in 30s
CI / Docker image build (pull_request) Successful in 17s
CI / Does this change touch the recording path? (pull_request) Successful in 27s
CI / Feedback site tests (pull_request) Successful in 2m0s
CI / Frontend tests, audit, and build (pull_request) Successful in 19m57s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m36s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 18m56s
b2293c526d
Migration f1a3c5e7b9d2 adds users.is_server_owner (partial unique index)
and server_ownership_offers, backfilled from the earliest
admin.setup.finished actor if still an admin, otherwise the
earliest-created admin, otherwise nobody. The owner cannot be demoted,
deleted, or delete their own account without handing the server on
first; the offer flow mirrors a campaign's own (offer/accept/decline/
cancel, a bell notification, the outcome told to the previous owner);
the CLI set_owner moves ownership without an offer for a vanished
owner.

Also in this PR: D56's member-search-scope write, About this server's
GET/PUT, the admin campaign list's delivery_failing_since/
owner_last_seen_at/uses_own_ai fields plus a members read for the
hand-on picker, notifying both owners on an admin's campaign
reassignment (campaign_handed_on), the audit log's name filters
(actor/campaign/category) and per-page name resolution, and one cheap
GET /admin/overview for the future Admin index.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merge origin/main into lane 9 A (#855): scope="function" for the branch's get_db, and the inventory's metadata keys
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m1s
CI / Backend lint (ruff) (pull_request) Successful in 35s
CI / Feedback site tests (pull_request) Successful in 53s
CI / Docker image build (pull_request) Successful in 25s
CI / Bot/backend version sync (pull_request) Successful in 1m7s
CI / Does this change touch the recording path? (pull_request) Successful in 31s
CI / Bot tests and audit (pull_request) Successful in 2m12s
CI / Backend migration, tests, and audit (pull_request) Failing after 27m1s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
41bbc62c51
- CHANGELOG: kept lane 9 A's entry and main's D131-D178 line.
- Every Depends(get_db) the branch adds in admin.py and users.py is now
  scope="function" (#854), so test_commit_before_response's walk passes.
- test_admin_campaign_inventory: EXPECTED_KEYS gains delivery_failing_since,
  owner_last_seen_at and uses_own_ai, each commented as metadata rather
  than campaign content.

f1a3c5e7b9d2 already sat on c4e6a8b0d2f1; one head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): a server handover clears the old owner before setting the new (#855)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m57s
CI / Backend lint (ruff) (pull_request) Successful in 1m17s
CI / Bot tests and audit (pull_request) Successful in 2m3s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Docker image build (pull_request) Successful in 20s
CI / Does this change touch the recording path? (pull_request) Successful in 30s
CI / Feedback site tests (pull_request) Successful in 2m2s
CI / Backend migration, tests, and audit (pull_request) Successful in 13m58s
CI / Frontend tests, audit, and build (pull_request) Successful in 41m31s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 19m15s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 33m6s
7d12eb3be0
uq_users_one_server_owner is a partial unique index, checked row by row and
not deferrable, and one flush writes both rows in primary-key order - so the
handover failed whenever the new owner's id sorted first (CI hit it; local runs
happened not to). The CLI's set_owner no longer clears its own target either.
A parametrized test forces both id orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.