docs(teams): Team pages and the activity feed, and what phase 3 disproved
TEAMS.md gains a dated amendment on phase 3 with five corrections, all found by
building the thing it describes:
- §3.2 and §3.4 contradict each other about `team.member.row`'s props, and
§3.2 wins because it is the security rule. A client slot can only receive
what the browser was sent, so §3.4's `{ memberKey, userId, displayName }`
means publishing both identifiers in every public roster, module installed
or not. The slot is redeclared with what core can honestly supply.
- §3.3's projection is an EIGHTH MODULE_API member where 1.6.0 listed seven.
Settled by the org lead: 1.6.0 is amended in place, on the rule Protocol 4
was given in phase 2 — a contract owes a bump only once it has reached
`main`.
- "the module declines" needed splitting in two before it could be built. No
module at all withholds nothing and must serve the roster whole; a module
whose rungs could not be consulted must serve none of it. Only the second
fails closed, or bare core shows an empty roster on every Team page.
- the module answers with member KEYS, not rows, so it can narrow what is
published and cannot widen it.
- core's five activity kinds are four until the forum lands, and a Team's
FIRST roster emits no join items at all.
§2.11's route table gains the activity endpoint it never had, and MODULE_API.md
documents `projectRoster`, the inverted fail-closed semantics that make it
different from every other provider call, and `ctx.teams.activity.push`'s item
shape and its four contractual properties.
BACKEND_DESIGN.md: the seventh Team table, its retention, and the three public
routes' new behaviour — `enabled` on the index, the slot props on the single
Team, the per-caller row projection on the roster, and the feed.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -717,7 +717,10 @@ place it bites is uploads, where "forget me" has to mean the bytes go too, not j
|
||||
```
|
||||
GET /api/v1/public/teams list (active), paged, {stale,lastSyncAt}
|
||||
GET /api/v1/public/teams/:slug overview + counts
|
||||
GET /api/v1/public/teams/:slug/members roster, field-projected per audience rung (§3.3)
|
||||
GET /api/v1/public/teams/:slug/members roster, row-projected per audience rung (§3.3)
|
||||
GET /api/v1/public/teams/:slug/activity the feed, paged, filtered to what the caller may
|
||||
see (§4.3) — added in phase 3; a session is
|
||||
optional on this route and on /members
|
||||
|
||||
GET /api/v1/player/teams the caller's Teams (membership + grants), with the
|
||||
reason for each: 'membership' | 'grant' | both
|
||||
@@ -815,7 +818,14 @@ named for a *place* and never for a meaning):
|
||||
| Slot | Rendered in | Props |
|
||||
| --- | --- | --- |
|
||||
| `team.overview` | the Team overview page, below the counts | `{ teamId, externalId, moduleId }` |
|
||||
| `team.member.row` | each roster row, trailing cell | `{ memberKey, userId, displayName }` |
|
||||
| `team.member.row` | each roster row, trailing cell | `{ displayName, isLeader, linked }` |
|
||||
|
||||
> **Amended 2026-08-17 (phase 3).** `team.member.row` was specified with
|
||||
> `{ memberKey, userId, displayName }`, which §3.2 forbids: a slot component runs in the browser, so
|
||||
> those props can only reach it by being published in the roster response to every visitor. The two
|
||||
> identifiers are dropped. `team.overview`'s three are kept — a core row id, a game-side group id and
|
||||
> a module name name no person — and they are served on `GET /teams/:slug` only, not on the index,
|
||||
> which has no slot to feed.
|
||||
|
||||
Both unfilled on bare core, which renders exactly the page core writes. Neither is typed by content —
|
||||
`team.overview` is "the spot under the counts", not "where the game puts guild stats".
|
||||
@@ -2059,6 +2069,50 @@ guild called "Admin" cannot put an official-looking page on the site.
|
||||
|
||||
### Phase 3 — Team pages, roster, nav, activity feed (`website` + `module-uo`)
|
||||
|
||||
> **Amended 2026-08-17, while building this.** Five corrections, all found by building or testing the
|
||||
> thing described below.
|
||||
>
|
||||
> **§3.2 and §3.4 contradict each other, and §3.2 wins.** §3.2 says a member key is a game-internal
|
||||
> identifier and a user id names a site account, and that **neither is published**; §3.4 then declares
|
||||
> the `team.member.row` slot with props `{ memberKey, userId, displayName }`. A client-side slot can
|
||||
> only receive what the browser was sent, so honouring §3.4 means putting both identifiers into every
|
||||
> public roster response — for every visitor, module installed or not. **Settled: the slot is declared
|
||||
> with `{ displayName, isLeader, linked }`.** module-uo leaves it unfilled, because the useful thing to
|
||||
> put there is a link to the character behind a row and these props do not identify one; filling it
|
||||
> with a guess from a display name is worse than an empty cell. A future phase that wants this back
|
||||
> needs an opaque per-response row token, not the raw key.
|
||||
>
|
||||
> **§3.3's projection is an EIGHTH `MODULE_API` member and 1.6.0's list said seven.** Settled by the
|
||||
> org lead: **1.6.0 is amended in place** rather than bumped, applying the same rule Protocol 4 got in
|
||||
> phase 2 — a contract owes a bump only once it has landed on `main`, and 1.6.0 is on `edge` only.
|
||||
>
|
||||
> **"The module declines" needed splitting in two before it could be implemented.** §3.3 says a module
|
||||
> that declines yields the public projection, fail closed. But *no module at all* and *a module whose
|
||||
> rung system could not be consulted* are opposite situations: the first is withholding nothing and
|
||||
> must serve the roster whole, the second must serve none of it. The refusal therefore carries
|
||||
> `projects` — `false` for "there is no audience model here", `true` for "there is one and I could not
|
||||
> ask it" — and only the second fails closed. Without that split, bare core serves an empty roster on
|
||||
> every Team page.
|
||||
>
|
||||
> **The module answers with member KEYS, not rows.** §3.3's "the module returns the rows it permits"
|
||||
> would let a module widen what is published by handing back a `userId` core had withheld, and core's
|
||||
> field guarantee would then rest on every module's good behaviour. Core asks which rows, keeps what a
|
||||
> row looks like, and re-normalises whatever comes back through its own public shape.
|
||||
>
|
||||
> **Core's five activity kinds are four here.** `core.forum.thread` has nothing to emit it until the
|
||||
> forum lands in phase 4. Separately, and not in the doc at all: **the first roster for a Team emits
|
||||
> no join items.** Importing a 155-member guild is one Team arriving, not 155 people joining, and
|
||||
> emitting a join per member would bury every real event under the import and reach the row cap on day
|
||||
> one. `roster_synced_at IS NULL` is the condition, which covers both a new Team and a newly installed
|
||||
> module adopting an existing one.
|
||||
>
|
||||
> **§2.11's route table has no activity endpoint** though §4.3 describes a feed filtered by the
|
||||
> viewer's access. Added on the org lead's decision: `GET /api/v1/public/teams/:slug/activity`, paged,
|
||||
> with the visibility resolved from the session and never from a parameter. It is the first public
|
||||
> route whose *content* depends on identity, which needed a new `optionalAuth` middleware —
|
||||
> `attachSession` only decodes a token, so a banned or logged-out account would have kept reading the
|
||||
> members-only half until its JWT expired.
|
||||
|
||||
`/teams`, `/teams/:slug`, `/teams/:slug/roster`, `/player/teams`; the linked/unlinked/guest surface;
|
||||
`team.overview` + `team.member.row` slots; nav registration; `team_activity` +
|
||||
`ctx.teams.activity.push` + core's own five activity kinds + the retention prune.
|
||||
|
||||
Reference in New Issue
Block a user