Lane 1, You: Profile in sections, Notifications as a grid, Plan & billing on a hosted instance (#727, #642 phase 1) #750

Merged
claude-bot merged 5 commits from build/lane1-you into main 2026-09-22 09:14:22 +00:00
Collaborator

Lane 1's last PR (#727, series item 5: HOME-5 Profile, HOME-6 Notifications, HOME-7 Plan & billing). Frontend and a small backend change; the last of the lane's six.

Profile, one page in sections (/profile, IS-366, D21): You (the sign-in name, display name, pronouns with three chips, IS-229, time zone), When you can play (weekly times with their own Save, time away with its own Add, SH-139/SH-142), Notifications, Connected accounts (Discord in its three states, other accounts, the post-link banner), Calendar (the feed with Copy, a new link and turning it off behind their confirms), and Your data and leaving (export first; the typed-name delete; the owner-blocked state naming the campaigns to hand on). The sections are in the Contents as Profile's sub-list, the one in view current. Its own duplicate header goes; the frame has the bar. Saving and Saved are SaveState; a failed save is an inline error with the drawing's own sentence, because SaveState's failure words promise a local copy that a page with Save buttons does not keep.

Notifications as a grid (/profile/notifications and the section, D48, IS-361): a row per category with the kit's words, "Always" in Quest Board, an Email column only when the instance sends mail, a Discord DM column only when it can work, mutes per campaign with "Muted since". New library component NotifyGrid; each cell is named by the two headings it sits under, so a reader hears "Sessions Email" rather than five controls all called "Email".

Backend for the grid: a recaps category (kinds: summary_ready), with migration b8c9d0e1f2a3 moving the carried-over recap opt-in rows from (sessions, email) to (recaps, email) and the recap email's gate and PATCH /api/me {recap_email_opt_in} following; calendar invites as a mail-only row in the grid backed by users.calendar_invite_opt_in; muted_at on mutes. Discord DM is listed as unavailable ("Discord DMs arrive with a later release.") until lane 10 delivers it, and the delivery task skips that channel before claiming its at-most-once row, so nothing is marked delivered that never went out.

Plan & billing (/profile/plan, D49, D71, IS-362 to IS-376): #642's phase 1, the entitlements interface with the self-hosted provider. GET /api/me/plan answers {"hosted": false} on a self-hosted instance, which is every real instance today, and then the account menu's entry, the sheet's entry and the page are absent. When a hosted provider exists, the page renders the contract: slots and what they cost, the failed-payment banner with the grace date, the credits meter, used this month, what things cost, and the four dialogs (buy in blocks, a first slot, add a slot with the new monthly total, remove a slot choosing which campaign stays active); paying goes to the provider's page and the return states come from ?return=. Prices are read once from Admin's plan pricing key (IS-398), never typed twice. The three write endpoints answer 501 on a self-hosted instance. Verified by rendering every state headless with the API faked (eighteen stills, desktop and phone) since no real instance can show it yet.

The journeys found two faults, fixed here: the phone's Contents sheet stayed open over the page a row had just opened (the sheet now closes when the frame routes a chrome link); and J0 asserted Dana had no pronouns by design, which the seed now sets and J0 asserts positively.

Coverage (lane1b.py): every drawn and replaced placement on HOME-5, HOME-6 and HOME-7, with SH-128 and SH-132 replaced and SH-133 moved into the grid. Tests: frontend 1,233 (Profile 25, Notifications 8, Plan 18, NotifyGrid 5, the frame 7 more); backend 3,817 (tests/test_plan.py 24 new; notifications and recap email extended); journeys 21 passed in the three projects, /profile and /profile/notifications clean under the strict gate. Lint, ruff and the build clean.

🤖 Generated with Claude Code

Lane 1's last PR (#727, series item 5: HOME-5 Profile, HOME-6 Notifications, HOME-7 Plan & billing). Frontend and a small backend change; the last of the lane's six. **Profile, one page in sections** (`/profile`, IS-366, D21): You (the sign-in name, display name, **pronouns** with three chips, IS-229, time zone), When you can play (weekly times with their own Save, time away with its own Add, SH-139/SH-142), Notifications, Connected accounts (Discord in its three states, other accounts, the post-link banner), Calendar (the feed with Copy, a new link and turning it off behind their confirms), and Your data and leaving (export first; the typed-name delete; the owner-blocked state naming the campaigns to hand on). The sections are in the Contents as Profile's sub-list, the one in view current. Its own duplicate header goes; the frame has the bar. Saving and Saved are `SaveState`; a failed save is an inline error with the drawing's own sentence, because `SaveState`'s failure words promise a local copy that a page with Save buttons does not keep. **Notifications as a grid** (`/profile/notifications` and the section, D48, IS-361): a row per category with the kit's words, "Always" in Quest Board, an Email column only when the instance sends mail, a Discord DM column only when it can work, mutes per campaign with "Muted since". New library component `NotifyGrid`; each cell is named by the two headings it sits under, so a reader hears "Sessions Email" rather than five controls all called "Email". **Backend for the grid:** a `recaps` category (kinds: `summary_ready`), with migration `b8c9d0e1f2a3` moving the carried-over recap opt-in rows from `(sessions, email)` to `(recaps, email)` and the recap email's gate and `PATCH /api/me {recap_email_opt_in}` following; calendar invites as a mail-only row in the grid backed by `users.calendar_invite_opt_in`; `muted_at` on mutes. **Discord DM is listed as unavailable** ("Discord DMs arrive with a later release.") until lane 10 delivers it, and the delivery task skips that channel before claiming its at-most-once row, so nothing is marked delivered that never went out. **Plan & billing** (`/profile/plan`, D49, D71, IS-362 to IS-376): #642's phase 1, the entitlements interface with the self-hosted provider. `GET /api/me/plan` answers `{"hosted": false}` on a self-hosted instance, which is every real instance today, and then the account menu's entry, the sheet's entry and the page are absent. When a hosted provider exists, the page renders the contract: slots and what they cost, the failed-payment banner with the grace date, the credits meter, used this month, what things cost, and the four dialogs (buy in blocks, a first slot, add a slot with the new monthly total, remove a slot choosing which campaign stays active); paying goes to the provider's page and the return states come from `?return=`. Prices are read once from Admin's plan pricing key (IS-398), never typed twice. The three write endpoints answer 501 on a self-hosted instance. Verified by rendering every state headless with the API faked (eighteen stills, desktop and phone) since no real instance can show it yet. **The journeys found two faults, fixed here:** the phone's Contents sheet stayed open over the page a row had just opened (the sheet now closes when the frame routes a chrome link); and J0 asserted Dana had no pronouns by design, which the seed now sets and J0 asserts positively. Coverage (`lane1b.py`): every drawn and replaced placement on HOME-5, HOME-6 and HOME-7, with SH-128 and SH-132 replaced and SH-133 moved into the grid. Tests: frontend 1,233 (Profile 25, Notifications 8, Plan 18, NotifyGrid 5, the frame 7 more); backend 3,817 (`tests/test_plan.py` 24 new; notifications and recap email extended); journeys 21 passed in the three projects, `/profile` and `/profile/notifications` clean under the strict gate. Lint, ruff and the build clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Lane 1's last three surfaces, and the server changes two of them need.

**Profile is one document of six sections** (HOME-5, IS-366, D21). It was four
panels in two columns under a "Back to Dashboard" header of its own — the only
page in the app that drew a second app bar (SH-128) — and it is `src/ui` now,
with an id per section that the Contents hangs a sub-list from and
`aria-current="location"` on the one being read. Each section saves on its own
(D22): three independent saves, each saying how it went beside the button that
did it, each in a `SectionBoundary` so a failure in one leaves the other two
with their typed text where it is. Pronouns arrive (IS-229, #653) as a free
field with three chips that fill it; the delete dialog is D12's typed tier and
counts the campaigns it would take you out of; an owner is sent to hand each
one on (SH-148) rather than told no.

**Notifications is D48's grid** (HOME-6), in Profile's `#p-notify` section and
at `/profile/notifications` — one component, because they are one surface. A
column is drawn only where it can work (D36): no mail on this instance, no
Email column; Discord not linked and verified, no Discord DM column and a line
saying where that is fixed. In-app is always on, because the bell is the
record. Calendar invites is the mail-only row (SH-133), so Profile's two old
toggles are both gone from it.

**Plan & billing is drawn for the first time** (HOME-7, D49, D71) and is
reachable only where `GET /api/me/plan` says there is something to pay. Every
instance today answers `{"hosted": false}`, so the account menu's row, the
Contents entry and the page itself are simply not there. Quest Board never sees
a card: buying credits and a first slot are a real navigation to the provider's
own page, and invoices and a failed payment are dealt with there.

**On the server.** Session recaps become their own notification category
(`recaps`), which is the row D48 draws; migration `b8c9d0e1f2a3` moves the
carried-over `(sessions, email)` rows into it and nobody's mail changes,
because every one of those rows *was* the recap opt-in under its old name.
`summary_ready` moves with it, out of `wiki`. Calendar invites join the
settings payload as `calendar_invites`, null where there is no mail. Discord DM
availability is now the person's as well as the instance's — the bot
configured **and** their link verified — and the delivery task skips the
channel outright until lane 10 builds the DM path, so no idempotency row is
spent on a message nothing sends. And #642's phase 1: one entitlements
interface, the self-hosted provider that answers "unlimited, nothing to pay",
`GET /api/me/plan` plus the three actions that answer 501 here, and every price
read from one instance setting (D71) rather than typed.

Two components join the library: `NotifyGrid`, which is the grid, and
`Choices`, the kit's set of options with reasons that the billing dialogs need.

Coverage (lane1b.py): HOME-5 SH-128 to SH-167, IS-229, IS-366, EX-046, EX-049;
HOME-6 SH-132, SH-133, IS-163 to IS-166, IS-169, IS-361; HOME-7 IS-190 to
IS-194, IS-362 to IS-365, IS-372 to IS-376.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`j0b-you.spec.js`, a sibling of J0 rather than one of the brief's seven: it
walks the one lane-1 surface no journey passes through. J1 to J7 are about
campaigns and sessions, and a person's own Profile is reached from the account
menu and from nowhere else, so without this the first page of the redesign to
be rebuilt would be the only one nothing scanned. Dana opens Profile the way a
person does, reads its six sections, sets her pronouns through the real API,
sees the account menu say so, and opens the notification grid — with an axe
scan on both, and no baseline entry, because a page built on the design system
is expected to have nothing to tolerate.

**Two real faults, both the harness's point.**

*The Contents sheet stayed open over the page it had just opened.* Every row in
it is a link, and on a phone the sheet is the *only* way to reach one — so
following a row left the whole screen covered by the thing used to get there.
`ContentsSheet` cannot fix it itself: `/design` draws it with no router at all,
so it has no idea a navigation happened. The frame does, because it is the
frame that hands the click to the router, so the fix is there — in the callback
`useChromeLinks` is given, not in an effect on the address, which would be a
cascading render for something that is already an event.

*J0 asserted Dana had no pronouns.* Deliberately, with a comment saying to turn
it round the day the profile carried them — which is today (IS-229). The seed
now sets them from `identities.json`, where the fiction has always said
"she/her", so they are true from the first screen rather than from whichever
spec happened to set them, and J0's assertion is the positive one it was
always meant to be.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The grid drew no Discord DM column for somebody whose Discord was not linked,
and then drew one for somebody whose was — with a switch that nothing delivers
until lane 10 builds the bot's DM path (#736). D36 says a control exists only
when it can work, and that one cannot: a switch that does nothing is worse
than no switch, because it reads as a setting somebody made.

So `CHANNELS_NOT_DELIVERED_YET` moves into `notification_settings_service`,
where "is this channel even possible" already lives, and becomes **one switch
with two consumers**: `channel_availability` reports a channel in it as
unavailable, with `why: "Discord DMs arrive with a later release."` taking
precedence over the bot and link checks; and the delivery task skips it before
claiming the permanent `notification_deliveries` row. Taking `discord_dm` out
of that set the day #736 lands turns both back on together, which is the point
of there being one set rather than two constants to remember.

The bot and link checks stay, and stay tested: a `dm_deliverable` fixture
empties the set so the live path — bot configured, link verified, then
`available: true` — is asserted now rather than discovered later.

The task's guard is belt and braces now that availability refuses the channel
first, and it is kept and tested on purpose: a mutation that removes it fails
`test_the_guard_holds_even_if_the_channel_becomes_available` and nothing else,
which is the point. A half-finished #736 that flipped availability without
building the transport would otherwise mark every notification written in the
meantime as delivered, permanently.

On the page, the line under the grid reads the server's `why` rather than
guessing at a reason — except for "Link your Discord under Connected
accounts", where the kit has its own fuller sentence naming where on Profile
it is fixed (SH-157).

The journeys are unaffected and were not re-run: the seeded Dana has no
verified Discord link, so the column was already absent there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
test(e2e): the You journey reads the server's reason for no Discord DM column (#727)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 52s
CI / Bot tests and audit (pull_request) Successful in 1m46s
CI / Bot/backend version sync (pull_request) Successful in 54s
CI / Docker image build (pull_request) Successful in 56s
CI / Backend lint (ruff) (pull_request) Failing after 1m0s
CI / Feedback site tests (pull_request) Successful in 1m12s
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
26c5bfc432
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
style(backend): ruff format on the plan router, the settings service and its test (#727)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 59s
CI / Backend lint (ruff) (pull_request) Successful in 1m6s
CI / Feedback site tests (pull_request) Successful in 1m53s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m12s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m31s
CI / Bot tests and audit (pull_request) Successful in 3m17s
CI / Docker image build (pull_request) Successful in 4m57s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 4m35s
CI / Backend migration, tests, and audit (pull_request) Successful in 12m50s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m49s
076877b57e
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.