feat(teams): the public Team pages, the two slots and the nav flag

TEAMS.md §3.1–§3.5. Four core pages — the index, a Team's overview, its full
roster and the player portal's "My Teams" — plus the two extension slots a
module adds to them, and the nav rows that lead there.

These are CORE routes, not module ones. A Team is a core platform entity that a
module merely populates, so the whole experience renders on bare core; a module
adds to these pages rather than supplying them.

`team.member.row` is declared with `{ displayName, isLeader, linked }` and not
§3.4's `{ memberKey, userId, displayName }`. The two documents contradict each
other and §3.2 is the one that is a security rule: a slot component runs in the
browser, so those props can only reach it by publishing a game-internal
identifier and a site account id in every public roster response, for every
visitor, module installed or not. Recorded as an amendment.

The presentation logic is split into lib/teams.js with its own tests, following
lib/teamAdmin.js, because these pages have to state differences that read as
bugs unless they are worded deliberately:

  - "37 members · 21 linked" — the gap is information (a character with no site
    account behind it), and the header says what each number IS rather than
    showing both and hoping;
  - an empty roster has three unrelated causes — nobody in the Team, a rung
    that shows nobody, and a module that could not be asked — and reporting the
    last as the first is a statement about the game that happens to be false;
  - a stale projection says how old it is rather than presenting itself as
    current.

`teams` is the first CORE nav row to carry a `feature` since the shard rows left
with the module cutover, and it brings core's own feature provider back with it.
It gates on whether this deployment has Teams AT ALL, not on who is looking —
Team pages are public and the server gates them. It fails open, so an unknown
answer shows the link: a Teams link leading somewhere empty is a far cheaper
mistake than a Team page nobody can find.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-17 20:15:54 -05:00
parent 03631d7d40
commit 8f4aff6946
12 changed files with 861 additions and 3 deletions

View File

@@ -3,7 +3,8 @@ import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.jsx'
import { publishSharedDependencies } from './modules/shared.js'
import { declareSlot } from './modules/registry.js'
import { declareSlot, registerFeatureProvider } from './modules/registry.js'
import { useCoreFlags } from './modules/coreFeatures.js'
import './styles/theme.css'
// Publish window.__rg BEFORE rendering and before any module chunk evaluates.
@@ -18,8 +19,12 @@ publishSharedDependencies()
// and namespace `uo`, so that the seam was exercised by real content from the
// day it was built. That prediction paid out exactly as written: the extraction
// deleted the registration and the hook it named, and SiteHeader was not touched.
// There is nothing for core to register now — no core nav row carries a
// `feature` — and the filter is a correct no-op until a module supplies one.
//
// Teams put a core row back on the seam. `feature: 'teams'` on the three Team nav
// rows resolves against owner id `core` (featureGate.js: a row with no `moduleId`
// belongs to core), and this is the provider that answers it — hiding the rows on
// a deployment that has no Teams at all, and failing open everywhere else.
registerFeatureProvider('core', 'core', useCoreFlags)
// ── Extension slots (MODULE_API.md §3.7) ───────────────────────────────────
//
@@ -50,6 +55,18 @@ declareSlot('admin.users.detail')
// over `onDone`. With the slot unfilled the invitee goes straight to the portal,
// which is what core's own code did whenever the flag was off.
declareSlot('player.invite.accepted')
// The two Team slots (TEAMS.md §3.4, MODULE_API 1.6.0). Both named for a place:
// `team.overview` is the spot under the counts on a Team page, not "where the
// game puts guild stats", and `team.member.row` is the trailing cell of a roster
// row. Core renders the whole Team experience with both unfilled — the pages are
// core's, and a module adds to them rather than supplying them.
//
// `team.overview` is where a live "online now" strip belongs: core's online count
// is the durable floor refreshed at the reconcile interval (§3.3), and a module
// that already holds a live presence feed can render the current number here
// without core acquiring an SSE stack to do it.
declareSlot('team.overview')
declareSlot('team.member.row')
// Core filled the first two itself until slice 3, with the components that were
// inline in SiteFooter.jsx and UserDetail.jsx. Both are gone: the module fills