docs(teams): phase 4 — the forum's access model, switches and image policy
Records what building phase 4 settled, and what it disproved. The structural correction first: TEAMS.md 3.1 gave the forum a CORE page and phase 3 deleted every core Team page. The ROUTES were unaffected — they are all /player and /admin — but the participant surface had no home, and 5.4's route table did not notice. Settled the way phase 3 settled the activity feed: module-uo declares a second place on its guild page and core fills it, so the phase spans two repos rather than the one the plan named. Two slots rather than one, because a slot holds one component and the first fill wins; the panel navigates by search param because a thread must be linkable and core cannot mount a route on a page it does not own. Two findings from the sanitiser worth not re-deriving: `rel` has to be on the allowlist for the transform that WRITES it to survive, or every forum link ships without noopener; and the bare-URL linkifier runs after sanitising, over escaped text only, which is the property that makes it safe rather than an injection point. Also recorded: the upload sweep runs regardless of the current image mode, which is the mechanism behind the dialog's promise that disabling uploads does not delete what is already there; the two routes the table lacked; and the org lead's decision that all three proposed acknowledgement additions ship. BACKEND_DESIGN gains the four forum tables and the reasoning a reader of the schema alone would miss — why the guard is at the route and never at the data, why no stored body ever contains an <img>, what `uploads` mode hardens, and what the acknowledgement actually records. MODULE_API's inverted-slot section gains the rule a module needs: one slot per PLACE, not one per page. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -833,6 +833,7 @@ named for a *place* and never for a meaning):
|
||||
> | Slot | Declared by | Rendered in | Filled by core with | Props |
|
||||
> | --- | --- | --- | --- | --- |
|
||||
> | `uo.guild.detail` | `module-uo` | its guild detail page | the Team activity feed (§4.3) | `{ externalId, moduleId }` |
|
||||
> | `uo.guild.forum` | `module-uo` | the same page, below the feed | the Team forum (Part 5) — added in phase 4 | `{ externalId, moduleId }` |
|
||||
>
|
||||
> Core's fills are applied at MOUNT, not eagerly: core's bundle evaluates before every module chunk,
|
||||
> so when core registers a fill the slot does not exist yet. A fill for a slot no installed module
|
||||
@@ -1057,6 +1058,19 @@ Every forum route above answers **404** while `teams_forums_enabled` is off, and
|
||||
answer 404 in any image mode but `uploads` — the same guard, applied at two levels, for the same
|
||||
reason (§5.5.1).
|
||||
|
||||
> **Amended 2026-08-18 (phase 4).** §3.1's `/teams/:slug/forum/*` core page is gone with the rest of
|
||||
> them. The routes below are unchanged — every one is `/player` or `/admin` — but the participant
|
||||
> surface is core's fill of the module-declared `uo.guild.forum` slot (§3.4), so a reader is on
|
||||
> `module-uo`'s guild page throughout. Two routes were added that this table did not have:
|
||||
> `GET /api/v1/player/teams/:slug/grants` (a leader has to SEE the guests before managing them) and
|
||||
> `GET /api/v1/admin/teams/forum/settings`, which serves the one piece of forum state that is not a
|
||||
> public settings key — whether the uploads acknowledgement has been given, by whom, and whether the
|
||||
> notice has been reworded since (§5.5.6 keeps that key unpublished).
|
||||
>
|
||||
> The grant routes deliberately answer while the forum is switched OFF, which no line below says: a
|
||||
> toggle-off revokes no grant and the rows stay authoritative (§5.5.1), so the access list has to stay
|
||||
> manageable during one. What the switch guards is the forum's CONTENT.
|
||||
|
||||
Under `/player` for the same reason as §2.11: a forum participant may be a plain player, and the tier
|
||||
gate is `requireAuth`. Every route resolves access through the §2.5 resolver — never by checking
|
||||
membership directly, which is how paths 1 and 3 would drift back together.
|
||||
@@ -1248,6 +1262,9 @@ text **version**. Recording two booleans would add nothing — there is no reach
|
||||
operator consented to one clause and not the other and proceeded anyway — while the version is what
|
||||
actually answers the question that matters later: *which text did they agree to?*
|
||||
|
||||
> **Settled 2026-08-18 (org lead): all three additions below are IN**, and the build ships them —
|
||||
> 1 and 3 in the help text, 2 in the dialog.
|
||||
|
||||
**Three additions proposed on top, marked so they can be dropped.** Each closes a gap the text above
|
||||
does not currently cover; none is liability language, so none changes what is being agreed to:
|
||||
|
||||
@@ -2150,7 +2167,43 @@ guild called "Admin" cannot put an official-looking page on the site.
|
||||
|
||||
**Ships:** the whole public Team experience. Independently valuable with no forum and no Discord.
|
||||
|
||||
### Phase 4 — Forum 5a: access model + announcements + admin controls (`website`)
|
||||
### Phase 4 — Forum 5a: access model + announcements + admin controls (`website` + `module-uo`)
|
||||
|
||||
> **Amended 2026-08-18, while building this.** Six corrections. The first is structural and follows
|
||||
> from phase 3; the rest were found by building the thing described below.
|
||||
>
|
||||
> **The forum had nowhere to live, and §5.4's route table did not notice.** §3.1 gave it
|
||||
> `/teams/:slug/forum/*` — a CORE page — and phase 3 deleted every core Team page. The routes are
|
||||
> unaffected (they are all `/player` and `/admin`), but the participant SURFACE had no home. Settled
|
||||
> by the org lead the same way phase 3 settled the activity feed: **`module-uo` declares a second
|
||||
> place on its guild page, `uo.guild.forum`, and core fills it.** So this phase spans two repos, not
|
||||
> the one named above.
|
||||
>
|
||||
> **Two slots rather than one**, because a slot holds one component and the first fill wins. Stacking
|
||||
> the feed and the forum into a single fill would take from the module the ability to place core's
|
||||
> two contributions separately on its own page, which is the whole point of the module owning it.
|
||||
>
|
||||
> **The forum panel navigates by SEARCH PARAM (`?thread=12`), not by route.** A thread has to be
|
||||
> linkable and core cannot mount a route for one — the route belongs to the module's page. A search
|
||||
> param gives a shareable URL under whatever path the module chose, with no core route anywhere in
|
||||
> it. It is why the fill is one component holding both a list view and a detail view.
|
||||
>
|
||||
> **`rel` had to be added to the forum sanitiser's allowed attributes to make links SAFER, not
|
||||
> laxer.** The profile writes `rel="noopener noreferrer nofollow"` through a transform, and
|
||||
> sanitize-html strips any attribute not in the allowlist — including one its own transform just
|
||||
> added. Without the entry every forum link shipped without `noopener`, silently.
|
||||
>
|
||||
> **The bare-URL linkifier is a second pass, and its ordering is the security property.** §5.5.3 says
|
||||
> an author writes a URL and core renders the picture, which requires the URL to have become an
|
||||
> anchor on the way in. Linkifying runs AFTER sanitising, over the sanitiser's own output and only on
|
||||
> text outside tags: every text node is HTML-escaped by then, so the matched URL is safe in both the
|
||||
> href and the link text. Running it first would be an injection point.
|
||||
>
|
||||
> **The upload sweep runs whether or not `uploads` is the current mode**, which is not obvious and is
|
||||
> the point. An operator who turns uploads off after a problem still has the files; a sweep that
|
||||
> switched itself off with the setting would strand exactly the bytes they were trying to be rid of —
|
||||
> and it is the mechanism behind the dialog's promise that disabling does not delete.
|
||||
|
||||
|
||||
`team_forum_grants`, the grant/revoke flow with audit into `activity_log`, leader vs staff authority,
|
||||
the full forum schema, announcement threads, and the leader/staff grant UI.
|
||||
|
||||
Reference in New Issue
Block a user