Lane 3: the members' server — who a GM can find, and stepping down (D56, D57, #729) #764
Loading…
Reference in a new issue
No description provided.
Delete branch "build/lane3-members-server"
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?
Lane 3's second PR: the two server changes CAMP-8 needs. Backend only.
Who "Add someone" can find (D56,
IS-382)search_non_membersILIKEd display names across every non-system account on the instance. Harmless on a private server; on a hosted one it is a way for any GM to enumerate strangers, which is what D56 is about:A new
member_search_scopesetting (a key in the existing JSON store, no migration), withplayed_withmeaning "shares at least one campaign with the searcher".The default is
anyone, which is today's behaviour. Same rule the retention cap followed in #763: a setting that appears must not change behaviour until somebody sets it. Defaulting toplayed_withwould have silently narrowed what every existing self-hosted GM can find, on upgrade day, with nothing said. Lane 9 draws the admin control that changes it.An archived shared campaign counts — you did play together. A soft-deleted one does not, matching #405 everywhere else.
The scope is on
CampaignResponse, because the line sits under an empty box and so cannot come from the search response.Each result says how the person relates
The drawing makes this load-bearing, printing a sentence under every name. The response carries the fact, not the sentence:
Corrected during review. This first carried
current, meaning "both are members now" — which was always true and could never be anything else:CampaignMemberrows are hard-deleted on leave and removal, so there is no record of a campaign either person has left, and every match the query can find is by construction one they are both still in. A field that cannot be false is worse than no field, because somebody will eventually branch on it.It was also not the distinction the page needs. The drawing prints "Played with you in 'Low Tide', a one-shot" against "Plays in 'The Bramble Court', with you" — past tense or present. What decides that is whether the campaign is finished, and a one-shot is not the only way to be finished; an archived campaign is too. So
is_archivedreplaced it, with a test for the case that motivated it.The owner may step down as GM (D57,
IS-383)This deliberately narrows one #644 behaviour, and only one
#644 removed exactly this class of guard on purpose, and says so in four places: the last GM can leave, the owner may remove the last GM, "a campaign with no GM is a legitimate state". So the guard is scoped as tightly as it can be — the owner, changing their own role, away from GM, when no other member is GM — and everything else is untouched.
One existing test asserted the behaviour D57 changes (
test_the_last_gm_can_be_demoted, where the owner demotes themselves as sole GM). It is nowtest_the_owner_cannot_step_down_as_the_only_gm, asserting the 400 and that the role is unchanged. That is a decided reversal, not a regression — flagged here so nobody reading #644 later mistakes it for one.The three #644 behaviours the guard must not touch each keep a test proving they still work: the last GM can leave; the owner can remove a GM, even the last one; and demoting somebody else is unaffected. All three pass unmodified.
The UI hides the control when there is no other GM, so the refusal is a backstop — but it has to exist, because the page is not the only caller.
Checks
Ruff 0.4.4 clean (
format --checkandcheck). Full backend suite 3993 passed, 13 skipped; the two touched files run 69 passed after theis_archivedcorrection. No migration; no frontend file touched; ownership offer/accept untouched, as D57 requires.🤖 Generated with Claude Code
Two independent CAMP-8 decisions, both about who may do what to whom. D56 (IS-382): who "Add someone" / "Attach to an account" can find is now the instance's choice, not always every non-system account on the server. A new member_search_scope app_setting ("anyone" | "played_with") gates search_non_members; the default is "anyone", matching today's unscoped behaviour, so no existing self-hosted GM's search narrows on upgrade day with nothing said — the same rule D24's retention cap followed one PR ago. The setter is deliberately not built here; it belongs to lane 9 (ADMIN). Each result now also carries the fact behind the relationship line the page prints under a name: a shared_campaign object (id, name, is_one_shot, current) or null, chosen by preferring a campaign both are still in, then the most recently created. member_search_scope also rides on CampaignResponse, read-only, because the line explaining the scope sits under an empty search box, before anything is typed. D57 (IS-383): the owner may step down as GM and keep the campaign, but only while another member is GM. #644 deliberately removed this whole class of guard, so the new check in ownership_service.set_member_role is scoped as narrowly as possible — only the owner, changing their own role, away from GM, with nobody else holding it — leaving leaving (including as the last GM), demoting someone else, and the owner removing the last GM exactly as #644 left them. Tests cover all three explicitly so nobody reads this as a reversion. No migration: both settings and facts are read from the existing app_settings JSON store and campaign_members rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>