61dc692088f75ffb61634167c031ac28636883b7
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 68f038f456 |
fix(admin): style the Teams admin screen with the site's own classes
The Teams admin screen was written against four CSS classes that do not exist anywhere in the project — `.table`, `.kv`, `.list` and `.notice` — and against `.btn-ghost` / `.btn` used without the `.btn` box they depend on. The result rendered as unstyled UA tables and bare browser buttons sitting flush against unpadded panels, and looked nothing like the rest of the admin panel. Nothing here changes behaviour, data or routes; it is presentation only. - Tables become `adm-table` / `adm-th` / `adm-td` inside `panel-flat`, the markup the other fourteen admin views use, and scroll rather than clip when a row is wider than the shell (a status badge and the action buttons are both nowrap by design, so a narrow viewport can always overflow one). - Buttons take the full `btn btn-primary btn-sq` / `btn btn-ghost btn-sq` triplet. `.btn` carries the padding, border and radius; the variants carry only colour, so a bare `.btn-ghost` had none of the box and a bare `.btn` fell back to the UA's light button face. - `.panel` supplies no padding, so every panel now sets it explicitly at 22px, as ModulesAdmin and EmptyState already do. - Headings become `h2.display`, and the in-page `<h1>Teams</h1>` goes away in favour of AdminLayout's topbar title — which needed `/admin/teams` adding to TITLES, the reason the bar read "ADMIN". - Status pills use the existing `badge-pub` / `badge-moderator` / `badge-ban` / `badge-draft` modifiers. `.badge` alone declares no border, so the old inline `borderColor` was inert. - The bridge and voice panels drop their private palette (`#e08b77` / `#8fbf7a` / `#e0b877`) for the site's `#d98b84` / `#7fd0a4` / `#e0b070`, and a literal `rgba(255,255,255,.12)` rule and a `borderRadius: 4` for `var(--line-soft)` and `var(--radius-input)`. Walked live against the dev DB as an admin: sync panel, review queue, all Teams, the forum ledger, the bridge draft form and its acknowledgement dialog. Client 288/288, server 1162/1162, client build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnDSWzpUjw8t8C2hghysNz |
|||
| 61abb3ec89 |
feat(teams): phase 9 — one voice channel per Team, granted by a role
TEAMS.md §7.3. Each qualifying Team gets a Discord voice channel of its own
and a role that opens it, kept in step by a reconciler that rides the Team
reconcile it already depends on.
Access is a per-Team ROLE, always. §7.3 designed per-member overwrites with
escalation to a role above ~90 members; the org lead settled on roles always
(2026-08-18), which deletes `voice_overwrite_max`, the escalation and the
`mode` column — and moves the ceiling. Overwrites are capped per channel, so
the old shape's limit was "how big can one Team be"; roles are capped per
guild at 250, so the new one is "how many Teams can have voice at all". That
is a limit an operator must be told about before they hit it, so the panel
reports it and the pass refuses the create rather than letting Discord do it.
Three things §7.3 named that this codebase does not have, all settled by
asking the operator because nothing in the data model can answer:
- "the staff role" — there is no staff-role concept anywhere. Now a list of
role ids the admin designates; empty is a normal answer, since guild
administrators bypass overwrites and what is really missing is a way to
let NON-admin staff in.
- the parent category — §7.3 said the bot creates it and gave the id nowhere
to live (`team_integrations.team_id` is NOT NULL). The bot creates it and
the server stores the id in settings.
- whether the bot can act at all — nothing has ever checked. The operator
invites the bot by hand and no invite URL with a permission integer exists
in the tree, so a deployment can be one unticked box from every call
failing. A preflight is now a PRECONDITION to enabling (422), not a
per-Team error discovered afterwards.
Two more, decided rather than asked:
- the threshold counts every active member, not linked ones. §7.3 wrote
`voice_min_linked_members`; the operator is judging whether a Team is real,
and link state answers a different question.
- hidden Teams are never provisioned. A channel name is a game-sourced string
published outside the site, which is exactly §2.8's concern —
reservedNames.js already names "and eventually a Discord channel name" as a
surface it protects — so the screen that suppresses a Team's page suppresses
its channel, and a Team that becomes hidden takes the grace window.
Turning voice OFF tears nothing down: the pass suspends in both directions and
the panel offers per-row removal. A checkbox must not delete structure in
somebody's guild.
Fixes a phase 8 defect that blocks this phase's own artifact: `npm run swagger`
has been unable to run on `edge` at all. `param('teamId').custom((v) => ... ||
/^[0-9]+$/.test(v))` makes swagger-autogen's parser run away — a regex literal
followed directly by `.test(`. Hoisted to a const, as modules.router.js
already does. Underneath it, `teams.router.js` sits exactly at that parser's
per-file limit: at twenty `teamsRouter.*` statements it dies, at nineteen it
generates, and one more statement of ANY shape tips it — an unannotated route
does, and so does a bare `use`. So the voice routes are their own router file
mounted from `admin/index.js`, and teams.router.js keeps its nineteen.
Also breaks a require cycle this phase would have introduced:
teamSync -> teamVoiceSync -> teams.model -> teamSync left `teams.model` holding
the reconciler's exports object as it stood mid-load — the empty one, since
`module.exports = {…}` replaces rather than fills. The symptom is not in the
new code: it is `teamSync.intervalSeconds is not a function` thrown out of
`syncStatus()`, the freshness banner on every public Team page.
Tests: 1160 server (+40), 53 bot (+21), 284 client (+21). Swagger, routes
manifest and guards regenerated; the guard shape of the four new routes is
byte-identical to the existing admin-only ones.
Co-Authored-By: Claude <noreply@anthropic.com>
|