Repository navigation
Player redesign 8b-1: household members (server + console) - #127
Merged
Merged
Conversation
…er rule Migration 0044 adds users.managed_by/member_kind/member_access/can_add_members/ display_name and tokens.created_on_token_id. auth/members.go creates, lists, changes and deletes an owner's members (derived usernames, the owner checks, a kid's credentials cleared), signs them in on the owner's device, mints their pairing token, and the admin's assign / make independent; DeleteUser takes a keep list. catalog's UserScope/UserScopes/AccessibleLibraries add the member rule (all: the owner's current scopes; collections: the owner's shared collection items the owner still reaches), and long path lists bind as one json_each parameter. Kids are hidden from share targets and house listening.
People chips, the person page's Household card and member access, the delete dialog's member list, Settings > Household, the 409 refusals and the member.* audit events.
AdminUser carries managed_by, member_kind, member_access, can_add_members, members_count and display_name; the user detail its members and member_collections. updateUser takes the member fields (Assign to someone needs kind and access beside managed_by; managed_by null makes someone independent), deleteUser sends ?keep_members=, settings gain a household section, and the new 409 codes read as plain English.
Modelled on Demo mode: one card, locked fields show what sets them, only changed fields are sent. Findable from the palette.
People keeps each household together (members follow their owner; one whose owner isn't listed keeps its place) with Managed by and Kid or Grown-up chips. The Account tab gets a Household card: the per-person switch and the person's members, or for a member who manages them, Make independent and Assign to someone (owner, kind, access). The role select never offers admin to a member. The Access tab says where a member's books come from, read-only, beside the direct grants. Deleting an owner lists their members, each ticked to go too; unticked ones are kept.
A Household members area, member.* actions in words, and the member fields user.update and user.delete now record.
/me/household/members (list, create, session, pair, devices, device sign-out, change, delete) in handlers_members.go, refused for API keys, demo accounts and members; adding also for the server's switch (household.members_enabled), the person's (can_add_members) and the cap (household.max_members). A kid gets 403 kid_account on a password, API keys, pairing, recovery, house sharing and sharing collections. /me/devices rows name the members signed in on them (member_sign_ins) and DELETE takes ?also_members=1. The admin's user objects carry the member fields, PATCH assigns / makes independent / switches, DELETE takes ?keep_members=. member.* audit events; household_members capability.
The single-parameter filter walked json_each for every row (a 1,000-item member's browse took 30 ms against 0.27 ms); it now looks a row's own path and its ancestors up in the list, which SQLite builds once, with the same rows as the OR form (which can't hold 1,000 arms at all: expression depth). BenchmarkMemberScopes measures browse, search, the progress list and the scope load for a 1,000-item collections kid against a whole-library listener.
…one-transaction admin PATCH
- Assigning a demo account as someone's member is refused (409
member_is_demo); the console maps it to its existing "A demo account
can't join a household" copy.
- writeMemberError no longer turns every auth.ErrNotFound into "device not
found": the device revoke route answers that itself, a member gone since
is "member not found", a failed lookup of the caller is a logged 500.
MemberSession reads the member inside its own transaction.
- PATCH /admin/users/{id} is one auth.UpdateUser transaction that checks
every field (password, household, role, disabled, can_add_members) against
the account as the earlier ones leave it before writing, so a refused
field changes nothing; a password beside assigning a kid is 400
kid_no_password (console copy in six locales). SetPassword, SetRole,
SetDisabled, SetCanAddMembers, AssignMember and MakeIndependent are thin
wrappers over it.
UpdateUser refuses a non-empty password for an account that is, or in the same change ends up as, a kid (400 kid_no_password); clearing one stays allowed, beside assigning a kid too. The console's Sign-in tab offers a kid no password and says how they sign in instead.
…-owner copy, can-add text, role copy, member cap range check
…p, and leaner account writes The token lookup reads managed_by, member_kind and the member rule through a left join in its one statement, so a non-member's scopes cost what they did before Phase 8b-1 (no rule read) and a kid's refusal needs no account read. catalog.Caller carries the rule (CallerScope/CallerScopes/CallerLibraries); UserScope/UserScopes/AccessibleLibraries read it themselves for callers known only by id. One AccessibleLibraries path for every non-admin; grantScopes dedupes in SQL; MemberCollections computes the owner's scopes itself and shares scopedItemCounts with countAndPreview. A kid's household_sharing row is cleared when they become a kid (so the house filters go; a kid made a grown-up again opts in again). auth.MayAddMembers is the one may-add ladder; CreateMember returns the signed-in account, MemberPairing and RevokeMemberDevice the member, each in one transaction. UpdateUser writes one UPDATE with one kid-password check; DeleteUser keeps and deletes members set-based; deriveUsername reads once; the also_members revoke uses RETURNING. The test-only SetRole/SetDisabled/SetCanAddMembers/ AssignMember/MakeIndependent wrappers are gone (tests call UpdateUser). auth.User's display_name moves to the player's /me view (api.meUser).
…s, and smaller transport
notKid (api.go) refuses a kid (403 kid_account) on POST /auth/password|tokens|
pair|recovery, PUT /me/household and PUT /me/collections/{id}/shares, reading
the member_kind the token lookup carries (handlePair loads no account any
more). The members list names its reason from auth.MayAddMembers. A member's
admin detail carries owner_libraries and owner_is_admin; the admin view of
managed_by adds the owner's display_name (never /me's); adminMember embeds
auth.Member. The settings envelope carries household.max_members_default,
_min and _max as read-only facts, and config validates max_members once
(Validate). /me/devices reads no member sign-ins for a member. pathID is the
one path id + 400 helper; parseIDList reads each id with parseOptionalID.
…pieces A member's owner is named from managed_by's display_name (ownerName(u), no People list fetched), and the Access tab reads owner_libraries and owner_is_admin from the member's own detail. Settings > Household takes the cap's default and range from the server's read-only facts. One AccessRow (with an action slot) for the direct grants and the owner's access, one KindBadge, one namedApart rule for showing a household name (case-insensitive), counted() for item counts, bookQuery for keep_members. The delete dialog tracks the members it keeps. The audit log reads member_kind and member_access with the kind and access words (their duplicate keys go), and the dead admin/demo assign cases and their unused strings are gone.
…d the owner facts; shared fixtures Tests: the request's user carries the member rule as GetUser reads it (bad data derives nothing), becoming a kid clears house sharing, MayAddMembers' ladder, a non-member Caller's scopes read nothing but their grants (users renamed away), and a member's admin detail carries owner_libraries, owner_is_admin and the owner's display_name (never on /me). The benchmark reuses the member fixture and measures the Caller path (plus scopes_by_id); createCollection lives on testEnv; the console's sam fixture spreads member. CLAUDE.md: the rule on the request, notKid, the kid's house sharing reset, the admin and settings facts, and the new benchmark numbers.
…ter, admin guard - Joining another owner's household signs out sessions made on other people's devices and revokes open pairing codes (leaveOwners); Make independent and a kept member revoke the codes only. - Becoming a kid also retires live invites and drops collection shares with anyone but the owner, both ways; minting an API key, recovery code, own pairing token or house sharing refuses a kid inside its own transaction. - The single-parameter path filter splits on bytes; a non-UTF-8 path gets its own OR arm instead of sending the whole list to the OR form. - DeleteUser checks the last admin inside its transaction; keep_members reads repeated values; a disabled member's session or pairing is 409 member_disabled; usernames number without a cap; demo and member callers are refused before the account limiter counts them; also_members sign-outs are audited; display_name changes only on a rename. - Denied tests for every member route (API key, demo, member caller) and a test that auth and catalog derive the same member rule.
…nsole copy POST /auth/password racing the owner's switch to kid answers 403 kid_account (SetPassword's ErrKidNoPassword) like the other in-transaction kid refusals; the admin PATCH keeps 400 kid_no_password. SetCollectionShares refuses a kid caller inside its transaction (ErrKidAccount -> 403 kid_account), so a share that passed notKid before the switch can't re-add a non-owner. The race test covers both; a catalog test pins the kid refusal (allowed: grown-up member, independent). Console: moveBody says the move signs the person out on other people's devices but the new owner's; kidClears adds the recovery code, invites and house sharing, and sharing with anyone but the managing person. All six locales, no gendered pronouns.
POST /admin/users/{id}/authcode and POST /admin/authcodes/{id}/rotate answer
409 kid_account for a kid's account (auth.refuseKid inside CreateInvite and
RotateAuthCode, so a used-up invite kept as history can't be revived into a
kid's account); a missing user is now 404 rather than 500. The console offers
no pairing or Rotate on a kid's page and says who sets up their devices, and
maps kid_account. Migration 0044's comment (unreleased) says what DeleteUser
does with an owner's members.
This was referenced Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Player redesign Phase 8b-1 (★): household members, server side and the admin console. Pairs with KodeStar/audiosilo-frontend#100, KodeStar/audiosilo-docs#130 and KodeStar/audiosilo#54 (plan + CROSS-REPO §22). Merge order: this -> frontend -> docs -> workspace. No release.
What it does
Any non-demo, non-member person can add kids and grown-ups as real accounts from the player, with no admin and no invite. A member is a user with
managed_by(the owner),member_kind(kid/grownup) andmember_access(all/collections).all, the owner's current access (an admin owner: every library as content scope, never the role), or forcollections, the books in collections the owner owns and shares with them, each only while the owner can reach it. Read per request from the request's user (lookupToken->callerOf->CallerScope/CallerScopes/CallerLibraries); the by-idUserScope/UserScopes/AccessibleLibrariesapply the same rule. Long path lists use a single-parameter, byte-safe filter (one bound parameter for 1,000 items).users.managed_by,member_kind,member_access,can_add_members,display_name;tokens.created_on_token_id(information only).handlers_members.go):GET/POST /me/household/members,PATCH/DELETE /me/household/members/{id},POST .../{id}/session,POST .../{id}/pair,GET .../{id}/devices,DELETE .../{id}/devices/{device_id}. API keys, demo and member callers refused; another owner's member is 404;409 member_limit,409 member_disabled.notKidroute wrapper plus refusals inside every minting write, collection shares and the admin's invite/rotate. Switching to kid clears all of these (sessions stay). Kids are visible in share targets only to their owner and never in Listening in the house.user.managed_by/member_kind(+display_namefor members) on/meand session envelopes;member_sign_insand?also_members=1on My devices; capabilityhousehold_members; admin user fields,PATCH(can_add_members, Assign to someone, Make independent; one transaction),DELETE ?keep_members=; settingshousehold.members_enabled(default on) /household.max_members(default 6, 1-50); auditmember.*.Tests
Every row of the plan's test table has allowed and denied tests on file-backed stores (
TestMembers_*ininternal/api/members_test.go,internal/auth/members_test.go,internal/catalog/members_test.go), plusTestPathFilterForms,TestCallerScopes_NonMemberReadsNoRule, race tests for the kid switch, a test that auth and catalog derive the same member rule, andBenchmarkMemberScopes(3,000 books: a 1,000-item kid browse 2.4 ms vs 0.26 ms for a whole-library listener; a non-member's scope load is cheaper than before).Gates
go build,go vet,go test -race ./...,golangci-lint(0 issues),GOOS=windows go build,scripts/build-admin.sh(730 console tests). Manager: full Go gate passes against this branch through itsreplace; no manager change.Quality passes
UpdateUsertransaction, set-based deletes, console cleanups.kid_account; collection shares refuse a kid inside the transaction; the admin's invite/rotate refuse a kid.Deliberately not fixed (follow-ups)
delete_memberslist)./code-review ultranot run (optional).