Moderation dashboard: staff dashboard, user history, notes, event capture (Phase 6a + 6b) #42

Merged
whitlocktech merged 3 commits from feature/moderation-dashboard into main 2026-07-05 20:51:40 +00:00
Member

Surfaces the Discord bot's moderation data on the admin panel and captures the event streams needed to power it. This is the first half of Step 6 (dashboard + history + notes + event capture); appeals (6c) and the bot reversal callback (6d) will land as a separate feature branch.

Phase 6a — Dashboard, user history, notes, moderator role

  • Schema: users.role ENUM gains moderator (CREATE + idempotent ALTER); new server-owned mod_notes table (staff_only/admin_only visibility).
  • Server: new model/moderation reads the bot's mod_actions directly via the shared pool (a documented, read-only cross of the bot/server ownership boundary), correlates accounts through user_identities (provider=discord), and flags automated actions via staff_user_id === bot_config.application_id. New /api/v1/admin/moderation/* routes gated by requireRole('admin','moderator'); admin_only note writes require admin.
  • Client: /admin/moderation overview (window tiles, type-filterable recent feed, user lookup) and /user/:discordId history (tabs + notes). RoleGate + AdminLayout confine moderators to their section.

Phase 6b — Bot event capture

  • Schema (bot-owned): member_events (join/leave + invite attribution), filter_hits (word/invite), spam_hits (rate_limit/mass_mention/mass_emoji).
  • Bot: records joins with best-effort invite attribution (new inviteTracker.js keeps an invite-use cache via the GuildInvites intent and diffs it on join); new guildMemberRemove records leaves; messageFilter records filter/spam hits alongside the existing warn/mute (additive — mod_actions still logs the action).
  • Server: summary extended with joins/leaves/invite_joins/filter_hits/spam_hits per window; new feeds /moderation/{members,filter-hits,spam-hits}.
  • Client: overview now shows 8 tiles (mod actions + joins/leaves/filter/spam) plus an Events panel with Members/Filter/Spam tabs.

Notes

  • The bot's member_events/filter_hits/spam_hits tables live in the same physical DB and are ensured by the main server on boot (same pattern as the existing bot tables).
  • No public/player accounts exist yet, so user-facing appeals are deferred to 6c (they need that account type first).

Verification

  • 119 server unit tests pass; client builds; Swagger regenerated.
  • DB-backed smoke tests against real MariaDB (6a: 18 checks, 6b: 14 checks) covering schema, model SQL/joins, window counts, notes visibility gating, and invite attribution.
  • Full HTTP/RBAC e2e and a browser click-through of both phases (dashboard tiles, user history, adding a note, moderator confinement, and every event tab).

🤖 Generated with Claude Code

https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV

Surfaces the Discord bot's moderation data on the admin panel and captures the event streams needed to power it. This is the first half of Step 6 (dashboard + history + notes + event capture); appeals (6c) and the bot reversal callback (6d) will land as a separate feature branch. ## Phase 6a — Dashboard, user history, notes, moderator role - **Schema:** `users.role` ENUM gains `moderator` (CREATE + idempotent ALTER); new server-owned `mod_notes` table (`staff_only`/`admin_only` visibility). - **Server:** new `model/moderation` reads the bot's `mod_actions` directly via the shared pool (a documented, read-only cross of the bot/server ownership boundary), correlates accounts through `user_identities` (provider=`discord`), and flags automated actions via `staff_user_id === bot_config.application_id`. New `/api/v1/admin/moderation/*` routes gated by `requireRole('admin','moderator')`; `admin_only` note writes require admin. - **Client:** `/admin/moderation` overview (window tiles, type-filterable recent feed, user lookup) and `/user/:discordId` history (tabs + notes). `RoleGate` + `AdminLayout` confine moderators to their section. ## Phase 6b — Bot event capture - **Schema (bot-owned):** `member_events` (join/leave + invite attribution), `filter_hits` (word/invite), `spam_hits` (rate_limit/mass_mention/mass_emoji). - **Bot:** records joins with best-effort invite attribution (new `inviteTracker.js` keeps an invite-use cache via the `GuildInvites` intent and diffs it on join); new `guildMemberRemove` records leaves; `messageFilter` records filter/spam hits alongside the existing warn/mute (additive — `mod_actions` still logs the action). - **Server:** `summary` extended with joins/leaves/invite_joins/filter_hits/spam_hits per window; new feeds `/moderation/{members,filter-hits,spam-hits}`. - **Client:** overview now shows 8 tiles (mod actions + joins/leaves/filter/spam) plus an Events panel with Members/Filter/Spam tabs. ## Notes - The bot's `member_events`/`filter_hits`/`spam_hits` tables live in the same physical DB and are ensured by the main server on boot (same pattern as the existing bot tables). - No public/player accounts exist yet, so user-facing appeals are deferred to 6c (they need that account type first). ## Verification - 119 server unit tests pass; client builds; Swagger regenerated. - DB-backed smoke tests against real MariaDB (6a: 18 checks, 6b: 14 checks) covering schema, model SQL/joins, window counts, notes visibility gating, and invite attribution. - Full HTTP/RBAC e2e and a browser click-through of both phases (dashboard tiles, user history, adding a note, moderator confinement, and every event tab). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
wtclaude added 2 commits 2026-07-05 20:34:22 +00:00
Surface the Discord bot's moderation data on the admin panel: a read-only
staff dashboard over the existing mod_actions log, per-user history, staff
notes, and a new moderator role. No bot changes.

Schema
- users.role ENUM gains 'moderator' (CREATE + idempotent ALTER for existing DBs)
- new server-owned mod_notes table (staff_only/admin_only visibility)

Server
- model/moderation: read mod_actions via the shared pool (documented read-only
  cross of the bot/server ownership boundary), correlate accounts through
  user_identities (provider='discord'), flag automated actions via
  staff_user_id === bot_config.application_id; pure reshaping helpers isolated
  in moderation.pure.js so they unit-test without opening a DB pool
- model/modNotes: list/add with role-gated admin_only visibility
- admin/moderation.controller + routes under /api/v1/admin/moderation/* gated by
  requireRole('admin','moderator'); admin_only note writes require admin
- allow assigning 'moderator' in the user create/update validators

Client
- /admin/moderation overview (window tiles, type-filterable recent feed, user
  lookup) and /user/:discordId history (tabs + notes with add-note)
- RoleGate; AdminLayout filters nav and confines moderators to their section
- moderator badge + action-type/auto badges

Deferred (see plan): 6b bot event capture (joins/leaves/filter/spam), 6c appeals
(needs public accounts), 6d /internal/mod-reverse bot reversal callback.

Verified: 116 server unit tests, client build, DB-backed model smoke, full
HTTP/RBAC e2e, and a browser click-through of the dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
Light up the moderation dashboard's previously-empty widgets by persisting the
event streams the bot only reacted to in-memory before.

Schema (bot-owned)
- member_events: join/leave, with invite_code/inviter_* for best-effort invite
  attribution on joins
- filter_hits: word / foreign-invite filter deletions (matched + action_taken)
- spam_hits: rate_limit / mass_mention / mass_emoji detections

Bot
- new models memberEvents/filterHits/spamHits
- guildMemberAdd records the join with invite attribution; new inviteTracker.js
  keeps an invite-use cache (GuildInvites intent + inviteCreate/inviteDelete) and
  diffs it on join to find which invite was used — best-effort, never blocks
  auto-role
- new guildMemberRemove records leaves
- messageFilter records filter/spam hits alongside the existing warn/mute;
  inviteFilter now returns the offending code; detectSpam identifies which spam
  rule tripped (preserving the rate-limit-first side-effect order)
- mod_actions still logs the resulting warn/mute — the new tables are additive

Server
- summary extended with joins/leaves/invite_joins/filter_hits/spam_hits per window
- new feeds: /api/v1/admin/moderation/{members,filter-hits,spam-hits}

Client
- overview now shows 8 tiles (mod actions + joins/leaves/filter/spam, joins tile
  notes "N via invite") plus an Events panel with Members/Filter/Spam tabs;
  removed the coming-soon note

Verified: 119 server unit tests, client build, 14-check DB-backed smoke, and a
browser click-through of every tile and events tab (incl. invite attribution).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
whitlocktech approved these changes 2026-07-05 20:51:10 +00:00
whitlocktech added 1 commit 2026-07-05 20:51:20 +00:00
whitlocktech merged commit 4ab46410be into main 2026-07-05 20:51:40 +00:00
Sign in to join this conversation.
No description provided.