fix(teams): four defects the live rig found in the phase 4 forum #154
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/teams-phase4-live-rig"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Phase 4 merged (website#153) without a live-rig walk. This is the walk, and what it
found. Four defects, none of which could fail a unit test, and three of which
break the feature for the operator rather than for the code.
1. The uploads acknowledgement was a one-way door 🔴
The worst of the four. A settings form sends every field it owns, so once
teams_forum_imageswasuploads, every later save re-sentuploads— and thegate fired on that value being present rather than on the mode being selected.
So after enabling uploads, the operator could never change a forum setting again.
Unticking "Enable Team forums" came back 400 "Enabling uploads requires
acknowledging the current notice" — the thing they would reach for in a hurry was
exactly the thing refused.
The gate now passes when an acknowledgement for the version in force is already on
record and uploads is already the stored mode: there is no new consent to take.
A transition into uploads still asks, and a reworded notice is still caught by
assertSettingsWritable. Both halves verified live.2. An uploaded image could never become a picture 🔴
uploadsmode hands the composer/uploads/<name>.png; the composer puts it in thebody as text (the author never writes markup — that is the whole design); and the
renderer only rewrites anchors. The linkifier matched absolute
http(s)URLsonly, so the write path could not produce the anchor the read path looks for — even
though
isEmbeddableImageUrlhad accepted those paths since the first commit. Thetwo halves disagreed and only a real upload showed it.
3. The embed sat beside its link, and nothing capped its size
An
<img>is inline, so it landed next to the URL rather than beneath it as §5.5.3specifies; and a remote image is whatever size its host decided, so one post could
blow the column out. Core now emits
class="forum-embed"and the stylesheet ownsboth — a class rather than an inline style, because the style would then have to
survive the client's DOMPurify pass and its CSS sanitiser is a larger thing to reason
about than one class name.
4. The panel's buttons had no button styling
btn-ghostis a modifier — every other call site in this repo pairs it with thebase
btn— so alone it contributed colours and no geometry, and the controlsrendered as bare boxes. Small inline actions use
pill. Same class of mistake as theMaterial one in Android M12: the modifier carries no base. Form controls also now
use
input/textarea, and the body usesprose(serifwas a class I invented andthis codebase does not have).
Plus one thing the walk improved rather than fixed
The post body now re-sanitises client-side like every other body-HTML surface here,
with
ADD_ATTR: ['referrerpolicy']. That argument is load-bearing: DOMPurify'sdefault allowlist carries
loadingbut notreferrerpolicy, so a plainsanitize()would silently strip the one attribute limiting what a remote embedleaks to the host serving it — the privacy property the admin help text promises an
operator.
How it was tested
The full rig: real uo-link sidecar (protocol 4, guild board served from its
store) → website with
module-uoinstalled → real MariaDB, browsed as a real leader,a real admin, and a real unlinked guest.
What the walk proved, beyond the four defects:
activity feed and core's forum, inside
module-uo's guild page. Turning forums offremoves the forum section entirely and leaves the feed untouched.
the grant route still answers. A caller with no access gets 404, never 403.
disabled→remote, identicalstored MD5, different rendering.
the forum with
reason: 'grant', holds zeroteam_membersrows, and is absentfrom the roster and both counts.
uploadswithout the acknowledgement is refused 400 — and therefusal is atomic, so
teams_forums_enabledin the same PUT is not written either.image/pngwas rejected 400 andremoved from disk; the real PNG stored, with its attribution row.
Enable uploadsstays inert with one box ticked,and the recorded acknowledgement names the admin and the time.
isLeadertrue at read time while the projection still says
is_leader = 0.npm testinserver/— 975 pass, 0 fail (3 new regression tests: the one-waydoor, the upload-path linkify, and prose-with-a-slash).
client/— 224 pass.Checklist
AI-assisted contributions (required)
Claude Code. I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.