Needs you's time zone, twelve months of analytics, and a way onward from a join request (#727, #728) #762

Merged
claude-bot merged 4 commits from fix/owner-answers-code into main 2026-09-23 00:29:51 +00:00
Collaborator

The three code answers the owner gave to questions lanes 1 and 2 left open. The decisions themselves are #761; this is the work behind two of them, plus one defect.

1. Needs you states a session's time in the campaign's zone (#727)

On Home, "Needs you" stated a session's time in the reader's zone while the campaign card immediately beside it stated the same session in the campaign's. Two different times for one session, side by side.

The rule was already written down, in Home.jsx's own comment on whenText: the campaign's zone wins, "because that is the night the table agreed on; a player travelling should still see the same one." /api/me/attention simply carried no zone to render in.

Each attention row now carries its campaign's timezone, and Needs you renders through the same helper the card uses. A campaign with no zone set falls back exactly as the card already does.

One line stays in the reader's zone, deliberately, and now says why in a comment: the live row's "started 8:14 PM". It is the only time on the page printed without a zone on it — whenText prints the abbreviation, so a campaign-zone time says which clock it means, while a bare "started 8:14 PM" in a foreign zone would quietly disagree with the clock on the reader's wall. That line exists to say how long ago something started, which is the reader's question, not the table's.

2. The shared analytics page counts twelve months (D89, #728)

The drawing reads "Every 13 days on average · 13 sessions in six months". The public endpoint reported only sessions_last_30_days, so lane 2 built the lesser sentence — "2 sessions in the last 30 days".

GET /public/analytics/{share_token} gains sessions_last_12_months, counted the same way the existing figure is (completed sessions, soft-deleted campaigns excluded) and beside sessions_last_30_days rather than replacing it — that field still has a consumer, the authenticated CampaignAnalytics.jsx page, which was checked rather than assumed.

A year rather than the drawing's six months, on the owner's answer: it tells a reader about a campaign's rhythm and survives a table that plays fortnightly through a quiet summer.

The window is timedelta(days=365), matching the 30-day code beside it rather than introducing calendar-month arithmetic and a new dependency for one line.

3. A join request has somewhere to go (SH-088, #728)

When a join needs the GM's approval, the invitation page said so in a quiet banner with no action — so somebody who had just asked to join was looking at a page with nowhere to go. The drawing draws it that way; the ledger row says it "explains the wait and links to the dashboard", and the ledger is right.

The banner gains Home, from vocabulary.js rather than a hard-coded string. The copy is untouched. That state is only reachable signed in, which was verified rather than assumed, so the link is always somewhere the reader can go.

Checks

Backend 3881 passed, 13 skipped. Ruff 0.4.4, both format --check and check, clean over webapp/backend/ scripts/ webapp/frontend/e2e/. Frontend 1394 tests in 123 files, lint 0 errors (239 pre-existing warnings, none in any file this touches), build clean.

No migration: both new fields are a response field and a computed service value.

🤖 Generated with Claude Code

The three code answers the owner gave to questions lanes 1 and 2 left open. The decisions themselves are #761; this is the work behind two of them, plus one defect. ## 1. Needs you states a session's time in the campaign's zone (#727) On Home, "Needs you" stated a session's time in **the reader's** zone while the campaign card immediately beside it stated the same session in **the campaign's**. Two different times for one session, side by side. The rule was already written down, in `Home.jsx`'s own comment on `whenText`: the campaign's zone wins, "because that is the night the table agreed on; a player travelling should still see the same one." `/api/me/attention` simply carried no zone to render in. Each attention row now carries its campaign's `timezone`, and Needs you renders through the same helper the card uses. A campaign with no zone set falls back exactly as the card already does. **One line stays in the reader's zone, deliberately, and now says why in a comment**: the live row's "started 8:14 PM". It is the only time on the page printed *without* a zone on it — `whenText` prints the abbreviation, so a campaign-zone time says which clock it means, while a bare "started 8:14 PM" in a foreign zone would quietly disagree with the clock on the reader's wall. That line exists to say how long ago something started, which is the reader's question, not the table's. ## 2. The shared analytics page counts twelve months (D89, #728) The drawing reads *"Every 13 days on average · 13 sessions in six months"*. The public endpoint reported only `sessions_last_30_days`, so lane 2 built the lesser sentence — "2 sessions in the last 30 days". `GET /public/analytics/{share_token}` gains `sessions_last_12_months`, counted the same way the existing figure is (completed sessions, soft-deleted campaigns excluded) and **beside** `sessions_last_30_days` rather than replacing it — that field still has a consumer, the authenticated `CampaignAnalytics.jsx` page, which was checked rather than assumed. A year rather than the drawing's six months, on the owner's answer: it tells a reader about a campaign's rhythm and survives a table that plays fortnightly through a quiet summer. The window is `timedelta(days=365)`, matching the 30-day code beside it rather than introducing calendar-month arithmetic and a new dependency for one line. ## 3. A join request has somewhere to go (`SH-088`, #728) When a join needs the GM's approval, the invitation page said so in a quiet banner with **no action** — so somebody who had just asked to join was looking at a page with nowhere to go. The drawing draws it that way; the ledger row says it "explains the wait and links to the dashboard", and the ledger is right. The banner gains **Home**, from `vocabulary.js` rather than a hard-coded string. The copy is untouched. That state is only reachable signed in, which was verified rather than assumed, so the link is always somewhere the reader can go. ## Checks Backend **3881 passed, 13 skipped**. Ruff 0.4.4, both `format --check` and `check`, clean over `webapp/backend/ scripts/ webapp/frontend/e2e/`. Frontend **1394 tests** in 123 files, lint **0 errors** (239 pre-existing warnings, none in any file this touches), build clean. **No migration**: both new fields are a response field and a computed service value. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Home stated a session's time in the reader's zone on a Needs you row
and in the campaign's zone on the card beside it — two different times
for the same session. `whenText`'s own rule (Home.jsx) is that the
campaign's zone wins, because that is the night the table agreed on;
this was the one place that skipped it.

`GET /api/me/attention` now carries each row's campaign `timezone`
(nullable, same as the campaign card's), and Home's Needs you renders
through the same `whenText` helper the card uses, falling back to the
reader's zone exactly as the card already does when a campaign has
none set.

No existing test asserted the reader's zone on a Needs you row, so
nothing needed correcting there — two rows were added instead: the
endpoint carrying a set zone, and a campaign with none still rendering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The drawing reads "Every 13 days on average · 13 sessions in six
months"; the public endpoint reported only sessions_last_30_days, so
the page was built saying "2 sessions in the last 30 days" — the
lesser sentence. D89 (owner, 2026-09-22) settles it on the drawn
sentence, over the last 12 months.

`GET /public/analytics/{share_token}` gains sessions_last_12_months —
completed sessions with a confirmed_time inside the last 365 days, the
same day-window approach sessions_last_30_days already uses — counted
by the same analytics_service.get_campaign_analytics every analytics
endpoint shares, so it inherits the existing soft-delete and
completed-only filtering rather than reimplementing it.

sessions_last_30_days is left in place, both in the service and on the
public response: the authenticated GET /campaigns/{id}/analytics page
(CampaignAnalytics.jsx, "Completed in last 30 days") still reads it.
The new field sits beside it rather than replacing it.

PublicAnalytics.jsx's "How often they play" now reads off
sessions_last_12_months and says "in the last 12 months"; the "two
played sessions" note is untouched. Its test's fixed numbers matched
the drawing already (13 sessions), so only the window in the assertion
text changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The invitation page said a join needed the GM's approval in a quiet
banner with no action, so a signed-in person who had just asked to
join was looking at a page with nowhere to go. The drawing and the
SH-088 ledger row both call for a link to the dashboard; the owner has
confirmed the ledger is right.

The banner's words are unchanged — this adds an action, it does not
rewrite the copy. The new action is a Home button (`/dashboard`),
using the vocabulary's HOME rather than a hard-coded string, on the
Banner's existing `actions` slot. The request-sent state is reached
only signed in (the join button that sets it, and ?requested=1, both
require `user`), so the link is always there when this state renders;
the other states — dead link, sign-in, already a member — are
untouched and were re-checked against the test suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(frontend): why the live row's time is the one in the reader's zone (#727)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m0s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Feedback site tests (pull_request) Successful in 48s
CI / Backend lint (ruff) (pull_request) Successful in 1m8s
CI / Docker image build (pull_request) Successful in 49s
CI / Bot tests and audit (pull_request) Successful in 3m28s
CI / Frontend tests, audit, and build (pull_request) Successful in 5m35s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 4m50s
CI / Backend migration, tests, and audit (pull_request) Successful in 14m38s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m48s
2e6c76c4ae
Every other time on Home is the campaign's now, so the live row looks like it
was missed. It was not: it is the only one printed without a zone on it.
`whenText` prints the abbreviation, so a campaign-zone time says which clock it
means; a bare "started 8:14 PM" does not, and in a zone that is not the
reader's it would quietly disagree with the clock on their wall. The line is
there to say how long ago it started, which is the reader's question.

Comment only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.