Skip to content

feat: web dashboard moderation panel (#33)#95

Merged
BillChirico merged 11 commits intomainfrom
feat/moderation-panel
Feb 26, 2026
Merged

feat: web dashboard moderation panel (#33)#95
BillChirico merged 11 commits intomainfrom
feat/moderation-panel

Conversation

@BillChirico
Copy link
Collaborator

Summary

Full moderation panel for the web dashboard — view mod cases, stats, and user history.

Closes #33

Changes

Backend

  • src/api/routes/moderation.js — 3 new API endpoints:
    • GET /cases — paginated list with guild, action, target filters
    • GET /cases/:id — single case + scheduled actions
    • GET /stats — total/24h/7d counts, byAction breakdown, top targets

Frontend (14 new files, 1,749 lines)

  • Stats cards — total cases, 24h/7d activity, unique actions, top targets
  • Cases table — color-coded action badges (warn/kick/ban/timeout/etc), pagination, filters (action type + user search), sortable by date
  • Case detail — expanded view with all fields, scheduled actions
  • Guild selector — mirrors existing dashboard pattern
  • API proxies — 3 Next.js routes with auth + guild admin check
  • shadcn/ui — added Badge, Table, Input, Select components

Testing

71 test files, 1473 tests passing, 0 failures

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 53 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4afdfd7 and 6c40b43.

📒 Files selected for processing (32)
  • src/api/index.js
  • src/api/routes/health.js
  • src/api/routes/moderation.js
  • src/api/ws/logStream.js
  • src/db.js
  • src/index.js
  • src/logger.js
  • src/modules/triage.js
  • src/transports/sentry.js
  • src/transports/websocket.js
  • tests/api/routes/config.test.js
  • tests/api/routes/guilds.test.js
  • tests/api/utils/configAllowlist.test.js
  • tests/api/utils/validateConfigPatch.test.js
  • tests/api/ws/logStream.test.js
  • tests/modules/triage-prompt.test.js
  • tests/modules/triage-respond.test.js
  • tests/sentry.test.js
  • tests/transports/websocket.test.js
  • web/src/app/api/moderation/cases/[id]/route.ts
  • web/src/app/api/moderation/cases/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/src/app/dashboard/moderation/page.tsx
  • web/src/components/dashboard/case-detail.tsx
  • web/src/components/dashboard/case-table.tsx
  • web/src/components/dashboard/moderation-stats.tsx
  • web/src/components/dashboard/moderation-types.ts
  • web/src/components/ui/badge.tsx
  • web/src/components/ui/input.tsx
  • web/src/components/ui/select.tsx
  • web/src/components/ui/table.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/moderation-panel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Request timed out after 900000ms (requestId=26ac3de3-ec61-4bfc-a087-08abec17c646)

@greptile-apps
Copy link

greptile-apps bot commented Feb 26, 2026

Greptile Summary

Comprehensive moderation dashboard implementation with proper security controls and clean separation of concerns.

Key additions:

  • Backend: 4 REST endpoints for cases, stats, and user history with parameterized queries and guildId validation
  • Frontend: Complete moderation dashboard with stats cards, filterable case table, expandable case details, and user history lookup
  • Security: All Next.js proxy routes validate guild admin permissions via authorizeGuildAdmin, API routes protected by requireAuth() middleware
  • UX: Proper loading states, error handling, pagination, client-side sort toggle, and abort signal cleanup

Previously flagged issues:

  • Import path and dependency issues in badge.tsx and select.tsx (already noted in previous threads)

Code quality:

  • Follows project conventions (Winston logging, parameterized queries, proper error handling)
  • Consistent patterns across proxy routes
  • Case detail expansion properly fetches full case data including scheduled actions
  • Tests passing (1473 tests, 0 failures)

Confidence Score: 4/5

  • Safe to merge after addressing import path issues in UI components
  • Score reflects solid implementation with proper security controls and clean architecture, but docked one point due to existing import/dependency issues in badge.tsx and select.tsx that need resolution before deployment
  • web/src/components/ui/badge.tsx and web/src/components/ui/select.tsx need import path corrections before merge

Important Files Changed

Filename Overview
src/api/routes/moderation.js added 4 new REST endpoints for cases, stats, and user history - proper parameterized queries, guildId validation
web/src/app/api/moderation/cases/route.ts Next.js proxy with auth check and param allowlisting - properly delegates to authorizeGuildAdmin
web/src/app/dashboard/moderation/page.tsx main dashboard page with stats, cases table, and user history - proper state management and error handling
web/src/components/dashboard/case-table.tsx case list with filters and expandable details - fetches full case on expand to include scheduledActions
web/src/components/ui/badge.tsx shadcn badge component - import path issue already flagged in previous thread
web/src/components/ui/select.tsx shadcn select component - import and dependency issues already flagged in previous thread

Sequence Diagram

sequenceDiagram
    participant User
    participant Dashboard as Moderation Dashboard
    participant Proxy as Next.js API Proxy
    participant Auth as authorizeGuildAdmin
    participant BotAPI as Bot API (/moderation)
    participant DB as PostgreSQL

    User->>Dashboard: Navigate to /dashboard/moderation
    Dashboard->>Proxy: GET /api/moderation/stats?guildId=X
    Proxy->>Auth: Verify user is guild admin
    Auth-->>Proxy: Authorized ✓
    Proxy->>BotAPI: GET /moderation/stats (with x-api-secret)
    BotAPI->>DB: Query mod_cases stats (total, 24h, 7d, byAction)
    DB-->>BotAPI: Stats data
    BotAPI-->>Proxy: JSON response
    Proxy-->>Dashboard: Stats rendered

    Dashboard->>Proxy: GET /api/moderation/cases?guildId=X&page=1&limit=25
    Proxy->>Auth: Verify user is guild admin
    Auth-->>Proxy: Authorized ✓
    Proxy->>BotAPI: GET /moderation/cases (with filters)
    BotAPI->>DB: SELECT cases with pagination
    DB-->>BotAPI: Cases list
    BotAPI-->>Proxy: Paginated response
    Proxy-->>Dashboard: Cases table rendered

    User->>Dashboard: Click to expand case #42
    Dashboard->>Proxy: GET /api/moderation/cases/42?guildId=X
    Proxy->>Auth: Verify user is guild admin
    Auth-->>Proxy: Authorized ✓
    Proxy->>BotAPI: GET /moderation/cases/42
    BotAPI->>DB: SELECT case + scheduled actions
    DB-->>BotAPI: Full case data
    BotAPI-->>Proxy: Case with scheduledActions
    Proxy-->>Dashboard: Case detail expanded
Loading

Last reviewed commit: 6c40b43

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

34 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

@BillChirico
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@claude
Copy link

claude bot commented Feb 26, 2026

Claude finished @BillChirico's task in 2m 47s —— View job


Code Review — PR #95: Moderation Panel

  • Read AGENTS.md and CLAUDE.md for project conventions
  • Review backend API routes (src/api/routes/moderation.js)
  • Review Next.js API proxies
  • Review frontend components
  • Review UI components (badge, input, select, table)
  • Review test changes
  • Review other source changes
  • Post inline comments for issues found
  • Submit final verdict

2 critical, 2 warnings, 1 nitpick — requesting changes.

🔴 Critical

# File Issue
1 web/src/components/ui/badge.tsx:3 Broken import from "radix-ui" — package not installed. Should be "@radix-ui/react-slot"
2 web/src/components/ui/select.tsx:5 Broken import from "radix-ui" + @radix-ui/react-select missing from package.json

Both will cause build/runtime failures for the entire moderation panel (and any page using these components).

🟡 Warning

# File Issue
3 src/api/routes/moderation.js 329 lines, 4 endpoints, zero test coverage. Every other route file has tests.
4 src/api/routes/moderation.js:134 case_number query lacks LIMIT 1 and no UNIQUE(guild_id, case_number) constraint exists

🔵 Nitpick

# File Issue
5 web/src/components/dashboard/case-table.tsx:175 expandedId in useCallback dep array causes full table re-render on every expand/collapse

Previously addressed ✅

All critical issues from earlier reviews have been resolved — cross-guild data exposure, wrong identifier, missing guildId forwarding, React key warnings, dead code, sequential queries, missing props, and ESLint deps.

🤖 AI Fix Prompt
Fix the following issues on branch feat/moderation-panel:

1. CRITICAL — web/src/components/ui/badge.tsx:3: Change import { Slot } from "radix-ui" to import { Slot } from "@radix-ui/react-slot".

2. CRITICAL — web/src/components/ui/select.tsx:5: Change import { Select as SelectPrimitive } from "radix-ui" to import * as SelectPrimitive from "@radix-ui/react-select". Also run pnpm add @radix-ui/react-select in the web/ directory.

3. src/api/routes/moderation.js: Create tests/api/routes/moderation.test.js with tests for all 4 endpoints following the pattern in tests/api/routes/guilds.test.js.

4. src/api/routes/moderation.js:134: Add LIMIT 1 to the WHERE case_number = $1 AND guild_id = $2 query.

5. Update AGENTS.md Key Files table with entries for src/api/routes/moderation.js and web/src/app/dashboard/moderation/page.tsx.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 26, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

2 critical issues, 4 warnings, 1 nitpick found across 17 files.

🔴 Critical

  1. Cross-guild data exposure in GET /cases/:id (src/api/routes/moderation.js:101-127) — The detail endpoint queries WHERE id = $1 without a guild_id filter. An authenticated user from guild A can read case details from guild B by enumerating database IDs. All other endpoints in this file correctly scope to guild_id.

  2. Wrong identifier in case detail fetch (web/src/components/dashboard/case-table.tsx:161) — The frontend sends case_number (per-guild sequential) as the URL path parameter, but the backend GET /cases/:id queries against the database id column. These are different values, so expanded case details will show the wrong case or return 404.

🟡 Warning

  1. Dead code: moderation-filters.tsx — Not imported anywhere. The CaseTable component has its own inline FilterBar.
  2. Dead code: moderation-table.tsx — Not imported anywhere. The page uses CaseTable from case-table.tsx instead.
  3. Silent error swallowing (case-table.tsx:162-167) — Failed detail fetches are caught and ignored with no fallback or error indication to the user.
  4. Sequential query in /user/:userId/history (moderation.js:296-302) — The summary query could be parallelized into the existing Promise.all.
  5. Suppressed ESLint deps (page.tsx:264-267) — Missing guildId, lookupUserId, fetchUserHistory from the effect dependency array, hidden by eslint-disable comment.

🔵 Nitpick

  1. Page-local sort (page.tsx:253-261) — Client-side sort toggle reverses only the current page, not the global order. Consider adding backend sort support or documenting this behavior.

Missing from PR (per AGENTS.md conventions)

  • No tests for new API routestests/api/routes/moderation.test.js does not exist. Other route files (guilds, config, auth) have corresponding test files.
  • AGENTS.md Key Files table not updated — New files (src/api/routes/moderation.js, web dashboard components) are not listed.
  • Missing React key on Fragmentcase-table.tsx:249-250 uses bare <> fragments inside .map() which will cause React key warnings.

- GET /api/v1/moderation/cases — paginated list with guildId, targetId, action filters
- GET /api/v1/moderation/cases/:id — single case + scheduled actions
- GET /api/v1/moderation/stats — summary stats (totals, 24h/7d, byAction, topTargets)
- Registered in api/index.js behind requireAuth() middleware
Components:
- moderation-types.ts — ModCase, ModStats, CaseListResponse types + ACTION_META
- moderation-stats.tsx — summary cards (total/24h/7d/byAction, top targets)
- case-table.tsx — paginated table w/ action badges, filters, row expansion
- case-detail.tsx — expanded case view w/ scheduled actions

Next.js API proxy routes:
- GET /api/moderation/cases — proxies to bot API, guild admin auth
- GET /api/moderation/cases/[id] — single case proxy
- GET /api/moderation/stats — stats proxy

Dashboard page:
- /dashboard/moderation — full page wiring stats + table + guild selector

shadcn/ui additions: Badge, Table, Input, Select
Adds user-level moderation history endpoint with:
- Full case history for a specific user in a guild
- Pagination (page/limit params)
- Action breakdown summary (byAction map)
- Same auth pattern as other moderation routes (requireAuth via index.js)

Closes part of #33
Next.js API route that proxies to the bot API user history endpoint.
Follows the same pattern as the existing cases and stats proxies:
- guildId-based admin authorization via authorizeGuildAdmin
- Full abort/timeout handling via proxyToBotApi
- Forwards page/limit pagination params

Part of #33
New dashboard components for the moderation panel:

ModerationTable:
- Renders paginated mod cases with case #, action badge, target/moderator
  user info, reason, duration, and formatted timestamp
- Clickable target user row (onUserClick) for history drill-down
- Skeleton loading states, prev/next pagination buttons
- Action badges use ACTION_META color palette

ModerationFilters:
- Action type dropdown (all actions or specific type)
- Target ID filter input (filters case list by targetId)
- Dedicated user history lookup with search input + submit/clear buttons
- Visual indicator when viewing user history mode

Note: CaseTable (existing) is the primary table component used by
the main moderation page. These components are available as alternatives
with slightly different UX patterns.

Part of #33
Extends the existing moderation panel page with a dedicated
'User History Lookup' section:

- Discord user ID search input with lookup button
- Fetches from GET /api/moderation/user/:userId/history
- Displays results in CaseTable with pagination
- Shows total case count for the looked-up user
- Clear button to dismiss results and reset state
- Guild change resets lookup state automatically
- Abort controller for in-flight request cancellation on unmount

Other improvements:
- Guild change clears lookupUserId and history state
- Refresh button also re-fetches open user history

Closes #33
- Fetch case detail endpoint when expanding table row so scheduled
  actions display correctly (list endpoint omits them)
- Remove sortDesc from fetch effect dependency — sort toggle now
  reverses existing data client-side without re-fetching
- Add loading spinner during case detail fetch
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

3 critical, 5 warnings found. Requesting changes.

🔴 Critical

  1. Cross-guild data exposure (src/api/routes/moderation.js:101-128) — GET /cases/:id queries WHERE id = $1 without a guild_id filter. An authenticated user from guild A can read case details from guild B by enumerating database IDs. All other endpoints correctly scope to guild_id.

  2. Wrong identifier in case detail fetch (web/src/components/dashboard/case-table.tsx:161) — The frontend sends case_number (per-guild sequential) as the URL path parameter, but the backend GET /cases/:id queries against the database id column. These are different values, so expanded case details will show the wrong case or return 404.

  3. Missing required guildId prop (web/src/app/dashboard/moderation/page.tsx:435-448) — The user history CaseTable instance is missing the guildId prop. This will cause a TypeScript compile error or runtime crash when expanding a case in the user history section.

🟡 Warning

  1. Missing React key on Fragment (case-table.tsx:249-250) — Bare <> fragments inside .map() can't accept keys. Use <React.Fragment key={c.id}> instead.

  2. Silent error swallowing (case-table.tsx:162-170) — Failed detail fetches are caught and ignored with no fallback or error indication.

  3. Sequential DB query (moderation.js:294-306) — The summaryResult query in /user/:userId/history could be parallelized into the existing Promise.all.

  4. Suppressed ESLint deps (page.tsx:264-267) — Missing guildId, lookupUserId, fetchUserHistory from the effect dependency array, hidden by eslint-disable. Stale closure risk if guild changes while a lookup is active.

  5. Dead codemoderation-filters.tsx and moderation-table.tsx are not imported or used anywhere. The page uses CaseTable from case-table.tsx instead.

Missing per AGENTS.md

  • No tests for new API routestests/api/routes/moderation.test.js does not exist. Other route files (guilds, config, auth) have corresponding test files. AGENTS.md: "Any new code must include tests."
  • AGENTS.md Key Files table not updated — New files (src/api/routes/moderation.js, web dashboard components) are not listed.

🤖 AI Fix Prompt (copy-paste into Claude Code to fix all issues)
Fix the following issues in the feat/moderation-panel branch:

1. **src/api/routes/moderation.js:101-128** — Add `guildId` query param requirement to `GET /cases/:id`. Change the SQL WHERE clause from `WHERE id = $1` to `WHERE id = $1 AND guild_id = $2` and pass `[caseId, guildId]` as params. Return 400 if guildId is missing.

2. **web/src/components/dashboard/case-table.tsx:161** — Change the fetch URL from using `caseNumber` to using `id`: `fetch(\`/api/moderation/cases/${id}?guildId=${encodeURIComponent(guildId)}\`)`. Update `toggleExpand` signature to remove the unused `caseNumber` param, and update the call at line 254 to `toggleExpand(c.id)`.

3. **web/src/app/dashboard/moderation/page.tsx:435-448** — Add the missing `guildId={guildId}` prop to the user history `CaseTable` component.

4. **web/src/components/dashboard/case-table.tsx:249-250** — Replace bare `<>` fragment with `<React.Fragment key={c.id}>` and closing `</React.Fragment>` at line 289. Add `React` to the import from "react".

5. **web/src/components/dashboard/case-table.tsx:162-170** — The catch block for the detail fetch is empty. Set `setExpandedCase(null)` in both the `else` branch (non-ok response) and `catch` block so the fallback at line 284 (`expandedCase ?? c`) kicks in properly.

6. **src/api/routes/moderation.js:294-306** — Move the `summaryResult` query into the existing `Promise.all` at line 263 so all 3 queries run in parallel.

7. **web/src/app/dashboard/moderation/page.tsx:264-267** — Remove the eslint-disable comment and add all missing deps: `[guildId, lookupUserId, userHistoryPage, fetchUserHistory]`.

8. **Delete** `web/src/components/dashboard/moderation-filters.tsx` and `web/src/components/dashboard/moderation-table.tsx` — they are dead code not imported anywhere.

9. **Create** `tests/api/routes/moderation.test.js` with tests for all 4 endpoints (GET /cases, GET /cases/:id, GET /stats, GET /user/:userId/history). Follow the pattern in `tests/api/routes/guilds.test.js` — mock logger, mock db pool, use supertest. Cover: missing guildId → 400, valid requests → 200, guild scoping on /cases/:id, invalid case ID → 400.

10. **Update AGENTS.md** Key Files table — add entries for `src/api/routes/moderation.js` and `web/src/app/dashboard/moderation/page.tsx`.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

1 critical, 2 warnings, 2 nitpicks across the moderation panel PR.

🔴 Critical

  1. cases/[id]/route.ts:42guildId not forwarded to backend proxy: The Next.js proxy validates auth with guildId but never adds it to the upstream URL (upstream.searchParams.set("guildId", guildId) is missing). The backend requires guildId in the query (WHERE case_number = $1 AND guild_id = $2), so case detail fetches always return 400. The fallback to list data in case-table.tsx masks this — scheduledActions are never loaded.

🟡 Warning

  1. No tests for src/api/routes/moderation.js — 329 lines of new backend code with zero test coverage. Per AGENTS.md: "Any new code must include tests — PRs that drop coverage below 80% will fail CI."
  2. case_number uniquenessGET /cases/:caseNumber queries WHERE case_number = $1 AND guild_id = $2 but there's no visible UNIQUE(guild_id, case_number) constraint. Consider adding one or using LIMIT 1.

🔵 Nitpick

  1. Misleading search placeholdercase-table.tsx:97 says "Search user..." but sends value as exact targetId match. Users typing usernames will get zero results.
  2. expandedId stale closurecase-table.tsx:151 toggleExpand callback recreates on every expand/collapse due to expandedId dep, causing full table re-renders.

Previous review issues (addressed ✅)

The following issues from the earlier review have been resolved in commit 47c3655:

  • Cross-guild data exposure → backend now scopes by guild_id
  • Wrong identifier (caseNumber vs id) → route uses case_number correctly
  • Missing React key on Fragment → uses <Fragment key={c.id}>
  • Silent error swallowing → falls back to list data
  • Dead code filesmoderation-filters.tsx and moderation-table.tsx removed
  • Sequential query in user history → parallelized into Promise.all
  • Suppressed ESLint deps → dependency array fixed
🤖 AI Fix Prompt (copy-paste into Claude Code)

Fix the following issues on branch feat/moderation-panel in the volvox-bot repo:

  1. CRITICAL — web/src/app/api/moderation/cases/[id]/route.ts:40-42: Add upstream.searchParams.set("guildId", guildId); between the buildUpstreamUrl call and the proxyToBotApi call. The guildId variable is already validated at line 23 — it just needs to be forwarded to the upstream URL like the stats and user-history proxies do.

  2. src/api/routes/moderation.js: Create tests/api/routes/moderation.test.js with tests for all 4 endpoints. Follow the patterns in tests/api/routes/guilds.test.js and tests/api/routes/config.test.js. Mock getPool() from ../../src/db.js. Test: pagination, filters, missing guildId → 400, invalid caseNumber → 400, case not found → 404, stats aggregation shape, user history pagination + summary.

  3. web/src/components/dashboard/case-table.tsx:97: Change the Input placeholder from "Search user..." to "User ID..." to match the actual behavior (exact targetId match).

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 26, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 26, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

1 warning, 1 nitpick — previous critical issues (cross-guild data exposure, wrong case identifier, missing guildId forwarding) have all been addressed.

🟡 Warning

  1. No tests for src/api/routes/moderation.js — 329 lines, 4 endpoints, zero test coverage. Every other route file has tests (auth, config, guilds, health, webhooks). Per AGENTS.md: "Any new code must include tests."

  2. AGENTS.md Key Files table not updated — New files (src/api/routes/moderation.js, web/src/app/dashboard/moderation/page.tsx, web/src/components/dashboard/moderation-types.ts) should be listed per the project's own documentation rules.

🔵 Nitpick

  1. Misleading search placeholder (case-table.tsx:97) — Says "Search user..." but sends value as exact targetId (Discord user ID) match. Users typing usernames get zero results. Suggest changing to "User ID...".

Previously resolved ✅

All critical issues from earlier reviews have been fixed:

  • Cross-guild data exposure → backend scopes by guild_id
  • Wrong identifier (id vs case_number) → uses case_number correctly
  • Missing guildId forwarding in case detail proxy → added in b5fcd8f
  • Missing React key on Fragment → uses <Fragment key={c.id}>
  • Silent error swallowing → falls back to list data
  • Dead code files removed
  • Sequential query parallelized
  • Missing guildId prop on user history CaseTable → added
  • ESLint deps fixed on user history effect
🤖 AI Fix Prompt

Fix the following issues on branch feat/moderation-panel:

  1. Create tests/api/routes/moderation.test.js with tests for all 4 endpoints. Follow the patterns in tests/api/routes/guilds.test.js. Mock getPool() from ../../src/db.js and logger from ../../src/logger.js. Test: missing guildId → 400, valid requests → 200 with correct shape, invalid caseNumber → 400, case not found → 404, pagination params, action/targetId filters.

  2. Update AGENTS.md Key Files table — add entries for:

    • src/api/routes/moderation.js — Moderation REST API endpoints (cases list/detail/stats, user history)
    • web/src/app/dashboard/moderation/page.tsx — Moderation dashboard page with stats, case table, and user history lookup
    • web/src/components/dashboard/moderation-types.ts — TypeScript types and action metadata for moderation UI
  3. web/src/components/dashboard/case-table.tsx:97 — Change Input placeholder from "Search user..." to "User ID...".

@BillChirico BillChirico merged commit 969e944 into main Feb 26, 2026
6 checks passed
@BillChirico BillChirico deleted the feat/moderation-panel branch February 26, 2026 04:10
@@ -0,0 +1,48 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical — Broken import: radix-ui is not installed as a dependency. The package.json only has scoped @radix-ui/* packages. This import will fail at build/runtime.

Suggested change
import { Slot } from "radix-ui"
import { Slot } from "@radix-ui/react-slot"


import * as React from "react"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { Select as SelectPrimitive } from "radix-ui"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical — Broken import + missing dependency: radix-ui is not installed, and @radix-ui/react-select is also missing from package.json. This component will fail at build time.

Fix:

  1. Install: pnpm add @radix-ui/react-select
  2. Fix import:
Suggested change
import { Select as SelectPrimitive } from "radix-ui"
import * as SelectPrimitive from "@radix-ui/react-select"

@@ -0,0 +1,329 @@
/**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning — No tests for new API routes: Per AGENTS.md: "Any new code must include tests — PRs that drop coverage below 80% will fail CI." Every other route file has a corresponding test file under tests/api/routes/: auth.test.js, config.test.js, guilds.test.js, health.test.js, webhooks.test.js. This file has 329 lines across 4 endpoints with zero test coverage.

Create tests/api/routes/moderation.test.js covering at minimum:

  • GET /cases — pagination, filters, missing guildId → 400
  • GET /cases/:caseNumber — valid case, not found → 404, invalid caseNumber → 400, guild scoping
  • GET /stats — aggregation shape, missing guildId → 400
  • GET /user/:userId/history — pagination, summary shape, missing params → 400

log_message_id,
created_at
FROM mod_cases
WHERE case_number = $1 AND guild_id = $2`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning — case_number not guaranteed unique per guild: Per AGENTS.md (pitfall #11), case_number is assigned via COALESCE(MAX(case_number), 0) + 1 inside createCase(). Without a UNIQUE(guild_id, case_number) constraint, concurrent case creation could theoretically produce duplicates. This query would then return multiple rows.

Consider adding LIMIT 1 as a safety measure, or adding a UNIQUE(guild_id, case_number) constraint in a migration.

} finally {
setExpandLoading(false);
}
}, [expandedId, guildId]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning — expandedId in dependency array causes unnecessary re-renders: expandedId is in the dependency array, so toggleExpand is recreated on every expand/collapse. Since every row's onClick references this callback, every expand/collapse triggers a full table re-render.

Consider using a functional setter pattern or moving to useRef for expandedId to stabilize this callback:

const toggleExpand = useCallback(async (c: ModCase) => {
    setExpandedId((prev) => {
      if (prev === c.id) {
        setExpandedCase(null);
        return null;
      }
      // ... fetch logic
      return c.id;
    });
  }, [guildId]); // expandedId no longer needed

Or accept the re-renders as negligible for typical table sizes — but if so, document that decision in a comment.

if (!prev) return prev;
return { ...prev, cases: [...prev.cases].reverse() };
});
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: only react to sortDesc toggle
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nitpick — eslint-disable comment is now unnecessary: The dependency array at line 268 already includes all deps (guildId, lookupUserId, userHistoryPage, fetchUserHistory). But the sortDesc effect at line 256 still has the eslint-disable. While that one is intentionally suppressing the casesData dep, the comment wording ("intentional: only react to sortDesc toggle") could be clearer about why casesData is excluded — specifically that the functional updater (prev =>) makes it unnecessary.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

2 critical, 2 warnings, 1 nitpick found.

🔴 Critical

  1. badge.tsx:3 — Broken import from "radix-ui": The radix-ui unified package is not installed. Only scoped @radix-ui/* packages exist in package.json. Import should be from "@radix-ui/react-slot".

  2. select.tsx:5 — Broken import + missing dependency: Same "radix-ui" issue, plus @radix-ui/react-select is not in package.json at all. Both the import path and the dependency need fixing.

🟡 Warning

  1. moderation.js — No tests for 329 lines of new backend code: Every other route file has tests (auth, config, guilds, health, webhooks). Per AGENTS.md: "Any new code must include tests — PRs that drop coverage below 80% will fail CI."

  2. moderation.js:134case_number not guaranteed unique per guild: No UNIQUE(guild_id, case_number) constraint exists. Concurrent case creation could produce duplicate case numbers. Add LIMIT 1 or a unique constraint.

🔵 Nitpick

  1. case-table.tsx:175expandedId in useCallback dep array causes full table re-render on every expand/collapse.

Previously addressed ✅

Critical issues from earlier reviews have been resolved:

  • Cross-guild data exposure → backend scopes by guild_id
  • Wrong identifier (id vs case_number) → uses case_number correctly ✅
  • Missing guildId forwarding in case detail proxy → added ✅
  • Missing React key on Fragment → uses <Fragment key={c.id}>
  • Silent error swallowing → falls back to list data ✅
  • Dead code files removed ✅
  • Sequential query parallelized ✅
  • Missing guildId prop on user history CaseTable → added ✅
  • ESLint deps fixed on user history effect ✅
  • Search placeholder clarified to "User ID..." ✅
🤖 AI Fix Prompt (copy-paste into Claude Code to fix all issues)

Fix the following issues on branch feat/moderation-panel:

  1. CRITICAL — web/src/components/ui/badge.tsx:3: Change import { Slot } from "radix-ui" to import { Slot } from "@radix-ui/react-slot". The unified radix-ui package is not installed — only scoped @radix-ui/* packages are.

  2. CRITICAL — web/src/components/ui/select.tsx:5: Change import { Select as SelectPrimitive } from "radix-ui" to import * as SelectPrimitive from "@radix-ui/react-select". Also run pnpm add @radix-ui/react-select in the web/ directory since this dependency is missing from package.json.

  3. src/api/routes/moderation.js: Create tests/api/routes/moderation.test.js with tests for all 4 endpoints. Follow the patterns in tests/api/routes/guilds.test.js. Mock getPool() from ../../src/db.js and logger from ../../src/logger.js. Test: missing guildId → 400, valid requests → 200 with correct shape, invalid caseNumber → 400, case not found → 404, pagination params, action/targetId filters.

  4. src/api/routes/moderation.js:134: Add LIMIT 1 to the WHERE case_number = $1 AND guild_id = $2 query as a safety measure, since there's no UNIQUE constraint on (guild_id, case_number).

  5. Update AGENTS.md Key Files table: Add entries for src/api/routes/moderation.js and web/src/app/dashboard/moderation/page.tsx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Web dashboard: moderation panel — warnings, logs, and actions

1 participant