Commit Graph

6 Commits

Author SHA1 Message Date
c57310c505 feat(guilds): a third place on the guild page, and where that page lives
All checks were successful
PR Checks / server-tests (pull_request) Successful in 23s
PR Checks / client-build (pull_request) Successful in 17s
PR Checks / frozen-manifest (pull_request) Successful in 34s
Two lines only core cannot supply for itself.

`uo.guild.header` is a third declared slot, at the top of the page, for core's
per-Team notification control. A third rather than a corner of the feed because a
slot holds one component and the first fill wins: the control is an action ON this
page and the other two are content IN it, and separate slots are what let this
module say so.

`pageUrlTemplate` tells core where a guild page actually is. Teams are a contract
primitive with no core surface — core owns the tables and the access rules, this
module owns the word "guild" and therefore the page — which leaves core unable to
write a link to one. A notification email that cannot take you to the thread it is
about is most of the way to useless. Core substitutes `{externalId}` and does
nothing else with it; a template naming its own host is refused at registration.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 14:35:36 -05:00
9d0a197008 feat(guilds): declare a second place on the guild page, for core's forum
Some checks failed
PR Checks / client-build (pull_request) Successful in 14s
PR Checks / frozen-manifest (pull_request) Failing after 34s
PR Checks / server-tests (pull_request) Successful in 8m40s
The mirror of the activity feed, one phase later. Core owns the Team forum —
membership, manual grants and the member/guest split are all core's rules, and a
module reimplementing any of them would be reimplementing a security boundary — but
core publishes no Team page, because it does not own the word "guild". So this
module declares the place and core puts the forum in it.

TWO declarations rather than one, and that is the interesting part. A slot holds one
component and the first fill wins, so folding the forum into `uo.guild.detail`
alongside the feed would hand core the decision about where each of its two
contributions sits on a page this module owns. Separate slots also keep them
independent: with the forum switched off, the feed renders exactly as before.

The registration test now asserts the set of declared slots and that EVERY one of
them is rendered by the page that owns it, rather than naming a single slot twice.
A slot nothing renders is a slot core fills into the void.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 07:24:37 -05:00
dda0e32dd3 feat(guilds): a guild detail page, and the slot core puts the feed in
Some checks failed
PR Checks / client-build (pull_request) Successful in 16s
PR Checks / frozen-manifest (pull_request) Failing after 33s
PR Checks / server-tests (pull_request) Successful in 8m46s
The module's half of the org lead's correction: Teams is the contract, guilds
are the presentation, and the presentation is this module's.

Adds `/uo/guilds/:id` — the detail view the board never had — with the roster
from this module's OWN board, which is the same data it answers core's Team
provider from. Reading core's projection of our own answer back would be a round
trip through a staler copy of it.

The page declares `uo.guild.detail` and core fills it with the Team activity
feed. That is the one part of this page core cannot hand over: only core can
resolve whether the viewer is inside the Team, and the public/members split on
that feed is a security boundary. The guild is named in OUR terms — core maps
its own Team from the module id and the external id — so this module never holds
core's row id or slug.

`TeamOverviewStrip` is deleted with the core Team page it filled.
`team.member.row` is not declared here either: the useful thing to put in a
roster row is a link to the character behind it, and nothing core could supply
identifies one.

`GET /public/shard/guilds/:id` backs the page, gated and projected through the
same `guilds` feature as the board — so an operator who raises that audience
raises this too, and the locked acct/webId fields never survive below admin. A
roster is where those appear in bulk, which makes this the endpoint where
getting the projection wrong would matter most.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 20:58:30 -05:00
d4aa5ade12 feat(teams): project rosters by audience rung, and add to the Team page
The module's half of TEAMS.md phase 3.

`projectRoster` is the optional fourth provider method and the only one core
calls on a request path. Core holds the roster and owns its public shape; the
question that is this module's is who is allowed to look, because the audience
rungs and their configuration live here.

The answer is all-or-nothing, which is the honest translation rather than a
shortcut: a rung is a property of the FEATURE, and there is no configuration in
which some members of a guild are public and others are not.

The refusal semantics INVERT here, and the tests say so. For the other three
methods a refusal means "change nothing" and an empty array would be
destructive. Core fails CLOSED on this one, so the dangerous answer is the
opposite — returning every key because the config could not be read would
publish a roster an operator gated to staff. Every path that cannot reach a
confident answer refuses, including the catch.

The anonymous case is answered directly rather than by handing `viewerLevel` a
synthetic request. Given one with no `req.user` it falls through to
`auth.getUserFromRequest`, which expects real cookies and throws on a fake — and
that throw would have become a refusal, so every anonymous visitor would have
been served an empty roster on a shard whose guilds are public. Caught by the
tests, not by reading.

`team.overview` gets a live population reading beside core's stored one. Core's
number comes from the last roster sync and is coarse by construction; this is
the `presence.online` feed this module already holds. It is explicitly not a
per-Team presence figure — the shard publishes a global aggregate and no
per-guild breakdown exists on the wire, so claiming one would be inventing a
number — and it renders nothing at all when it has nothing true to say.

`team.member.row` is left unfilled. The useful thing to put there is a link to
the character behind a row, and the props core can supply do not identify one:
the member key and the site account id are withheld from every public roster.
An empty cell beats a guess.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 20:16:22 -05:00
9d559091c5 fix(client): give the portal row an icon, and assert every gated nav has one
All checks were successful
PR Checks / server-tests (pull_request) Successful in 19s
PR Checks / client-build (pull_request) Successful in 8m49s
The §7.7 smoke, running the pair together: registering the player row without an
`icon` blanked the whole portal with React error #130, because core's
PlayerPortalLayout rendered `<n.icon />` unguarded. Core is guarded now
(website), and this is the other half — the row had an icon before it moved and
should have kept one.

A second glyph rather than reusing IconShard: these two rows sit in different
navs and each matched its neighbours before the extraction. The admin sidebar's
UO rows were gems; the portal's Characters row was a person beside Appeals'
shield and Account's gear. Matching the nav a row lands in is the whole reason
`icon` is in the contract.

`registration.test.js` now asserts it for admin AND player rows, which is the
cheap place to catch the next one. The public header is text buttons and is
deliberately excluded.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 18:54:42 -05:00
e4af7dd9a8 test(client): check what the chunk registers, and fix two defects in the checks
Three things, all about checks that had never met a real chunk.

`checkExternals.js` rejected slice 3's build outright, naming a fragment of
minified JSX as an imported specifier: a button reading "Approve and import"
puts the token immediately before a quote, and no regexp can tell that from a
statement. Same wall the server's `checkImports.js` hit, answered the same way —
a character walk. A mask rather than a rewrite, because a real import has its
keyword outside a string and its specifier inside one.

Writing the test for that false positive found the false NEGATIVE underneath
it: the pattern required whitespace after `import`, so it could not see
`import{useState}from"react"` — the one shape a minified build actually emits,
and the most likely way for a missed alias to reach production. It has never
been able to see it.

`registration.test.js` is new: stand up a fake `window.__rg` with a recording
registry and the real React, import the BUILT chunk, and read back what it
asked for. No DOM, because nothing renders. It holds the agreement that rots
quietly — every nav row points at a route this module actually registered —
rather than restating both lists.

CI now builds before it tests, because both of those read `dist/entry.js` and
skip without it. Run the other way round they are green and asking nothing.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 18:00:50 -05:00