feat: web dashboard moderation panel (#33)#95
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📒 Files selected for processing (32)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
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. |
|
Request timed out after 900000ms (requestId=26ac3de3-ec61-4bfc-a087-08abec17c646) |
|
| 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
Last reviewed commit: 6c40b43
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Claude finished @BillChirico's task in 2m 47s —— View job Code Review — PR #95: Moderation Panel
2 critical, 2 warnings, 1 nitpick — requesting changes. 🔴 Critical
Both will cause build/runtime failures for the entire moderation panel (and any page using these components). 🟡 Warning
🔵 Nitpick
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 |
There was a problem hiding this comment.
Review Summary
2 critical issues, 4 warnings, 1 nitpick found across 17 files.
🔴 Critical
-
Cross-guild data exposure in
GET /cases/:id(src/api/routes/moderation.js:101-127) — The detail endpoint queriesWHERE id = $1without aguild_idfilter. 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 toguild_id. -
Wrong identifier in case detail fetch (
web/src/components/dashboard/case-table.tsx:161) — The frontend sendscase_number(per-guild sequential) as the URL path parameter, but the backendGET /cases/:idqueries against the databaseidcolumn. These are different values, so expanded case details will show the wrong case or return 404.
🟡 Warning
- Dead code:
moderation-filters.tsx— Not imported anywhere. TheCaseTablecomponent has its own inlineFilterBar. - Dead code:
moderation-table.tsx— Not imported anywhere. The page usesCaseTablefromcase-table.tsxinstead. - Silent error swallowing (
case-table.tsx:162-167) — Failed detail fetches are caught and ignored with no fallback or error indication to the user. - Sequential query in
/user/:userId/history(moderation.js:296-302) — The summary query could be parallelized into the existingPromise.all. - Suppressed ESLint deps (
page.tsx:264-267) — MissingguildId,lookupUserId,fetchUserHistoryfrom the effect dependency array, hidden by eslint-disable comment.
🔵 Nitpick
- 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 routes —
tests/api/routes/moderation.test.jsdoes 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 Fragment —
case-table.tsx:249-250uses 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
There was a problem hiding this comment.
Review Summary
3 critical, 5 warnings found. Requesting changes.
🔴 Critical
-
Cross-guild data exposure (
src/api/routes/moderation.js:101-128) —GET /cases/:idqueriesWHERE id = $1without aguild_idfilter. An authenticated user from guild A can read case details from guild B by enumerating database IDs. All other endpoints correctly scope toguild_id. -
Wrong identifier in case detail fetch (
web/src/components/dashboard/case-table.tsx:161) — The frontend sendscase_number(per-guild sequential) as the URL path parameter, but the backendGET /cases/:idqueries against the databaseidcolumn. These are different values, so expanded case details will show the wrong case or return 404. -
Missing required
guildIdprop (web/src/app/dashboard/moderation/page.tsx:435-448) — The user historyCaseTableinstance is missing theguildIdprop. This will cause a TypeScript compile error or runtime crash when expanding a case in the user history section.
🟡 Warning
-
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. -
Silent error swallowing (
case-table.tsx:162-170) — Failed detail fetches are caught and ignored with no fallback or error indication. -
Sequential DB query (
moderation.js:294-306) — ThesummaryResultquery in/user/:userId/historycould be parallelized into the existingPromise.all. -
Suppressed ESLint deps (
page.tsx:264-267) — MissingguildId,lookupUserId,fetchUserHistoryfrom the effect dependency array, hidden by eslint-disable. Stale closure risk if guild changes while a lookup is active. -
Dead code —
moderation-filters.tsxandmoderation-table.tsxare not imported or used anywhere. The page usesCaseTablefromcase-table.tsxinstead.
Missing per AGENTS.md
- No tests for new API routes —
tests/api/routes/moderation.test.jsdoes 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`.
There was a problem hiding this comment.
Review Summary
1 critical, 2 warnings, 2 nitpicks across the moderation panel PR.
🔴 Critical
cases/[id]/route.ts:42—guildIdnot forwarded to backend proxy: The Next.js proxy validates auth withguildIdbut never adds it to the upstream URL (upstream.searchParams.set("guildId", guildId)is missing). The backend requiresguildIdin the query (WHERE case_number = $1 AND guild_id = $2), so case detail fetches always return 400. The fallback to list data incase-table.tsxmasks this — scheduledActions are never loaded.
🟡 Warning
- 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." case_numberuniqueness —GET /cases/:caseNumberqueriesWHERE case_number = $1 AND guild_id = $2but there's no visibleUNIQUE(guild_id, case_number)constraint. Consider adding one or usingLIMIT 1.
🔵 Nitpick
- Misleading search placeholder —
case-table.tsx:97says "Search user..." but sends value as exacttargetIdmatch. Users typing usernames will get zero results. expandedIdstale closure —case-table.tsx:151toggleExpandcallback recreates on every expand/collapse due toexpandedIddep, 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 byguild_idWrong identifier (caseNumber vs id)→ route usescase_numbercorrectlyMissing React key on Fragment→ uses<Fragment key={c.id}>Silent error swallowing→ falls back to list dataDead code files→moderation-filters.tsxandmoderation-table.tsxremovedSequential query in user history→ parallelized intoPromise.allSuppressed 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:
-
CRITICAL —
web/src/app/api/moderation/cases/[id]/route.ts:40-42: Addupstream.searchParams.set("guildId", guildId);between thebuildUpstreamUrlcall and theproxyToBotApicall. TheguildIdvariable is already validated at line 23 — it just needs to be forwarded to the upstream URL like the stats and user-history proxies do. -
src/api/routes/moderation.js: Createtests/api/routes/moderation.test.jswith tests for all 4 endpoints. Follow the patterns intests/api/routes/guilds.test.jsandtests/api/routes/config.test.js. MockgetPool()from../../src/db.js. Test: pagination, filters, missing guildId → 400, invalid caseNumber → 400, case not found → 404, stats aggregation shape, user history pagination + summary. -
web/src/components/dashboard/case-table.tsx:97: Change the Input placeholder from"Search user..."to"User ID..."to match the actual behavior (exacttargetIdmatch).
There was a problem hiding this comment.
Review Summary
1 warning, 1 nitpick — previous critical issues (cross-guild data exposure, wrong case identifier, missing guildId forwarding) have all been addressed.
🟡 Warning
-
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." -
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
- Misleading search placeholder (
case-table.tsx:97) — Says "Search user..." but sends value as exacttargetId(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_numbercorrectly - 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:
-
Create
tests/api/routes/moderation.test.jswith tests for all 4 endpoints. Follow the patterns intests/api/routes/guilds.test.js. MockgetPool()from../../src/db.jsand 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. -
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 lookupweb/src/components/dashboard/moderation-types.ts— TypeScript types and action metadata for moderation UI
-
web/src/components/dashboard/case-table.tsx:97— Change Input placeholder from"Search user..."to"User ID...".
| @@ -0,0 +1,48 @@ | |||
| import * as React from "react" | |||
| import { cva, type VariantProps } from "class-variance-authority" | |||
| import { Slot } from "radix-ui" | |||
There was a problem hiding this comment.
🔴 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.
| 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" |
There was a problem hiding this comment.
🔴 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:
- Install:
pnpm add @radix-ui/react-select - Fix import:
| import { Select as SelectPrimitive } from "radix-ui" | |
| import * as SelectPrimitive from "@radix-ui/react-select" |
| @@ -0,0 +1,329 @@ | |||
| /** | |||
There was a problem hiding this comment.
🟡 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, missingguildId→ 400GET /cases/:caseNumber— valid case, not found → 404, invalid caseNumber → 400, guild scopingGET /stats— aggregation shape, missingguildId→ 400GET /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`, |
There was a problem hiding this comment.
🟡 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]); |
There was a problem hiding this comment.
🟡 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 neededOr 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 |
There was a problem hiding this comment.
🔵 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.
There was a problem hiding this comment.
Review Summary
2 critical, 2 warnings, 1 nitpick found.
🔴 Critical
-
badge.tsx:3— Broken import from"radix-ui": Theradix-uiunified package is not installed. Only scoped@radix-ui/*packages exist inpackage.json. Import should befrom "@radix-ui/react-slot". -
select.tsx:5— Broken import + missing dependency: Same"radix-ui"issue, plus@radix-ui/react-selectis not inpackage.jsonat all. Both the import path and the dependency need fixing.
🟡 Warning
-
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." -
moderation.js:134—case_numbernot guaranteed unique per guild: NoUNIQUE(guild_id, case_number)constraint exists. Concurrent case creation could produce duplicate case numbers. AddLIMIT 1or a unique constraint.
🔵 Nitpick
case-table.tsx:175—expandedIdinuseCallbackdep 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_numbercorrectly ✅ - 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:
-
CRITICAL —
web/src/components/ui/badge.tsx:3: Changeimport { Slot } from "radix-ui"toimport { Slot } from "@radix-ui/react-slot". The unifiedradix-uipackage is not installed — only scoped@radix-ui/*packages are. -
CRITICAL —
web/src/components/ui/select.tsx:5: Changeimport { Select as SelectPrimitive } from "radix-ui"toimport * as SelectPrimitive from "@radix-ui/react-select". Also runpnpm add @radix-ui/react-selectin theweb/directory since this dependency is missing frompackage.json. -
src/api/routes/moderation.js: Createtests/api/routes/moderation.test.jswith tests for all 4 endpoints. Follow the patterns intests/api/routes/guilds.test.js. MockgetPool()from../../src/db.jsand 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. -
src/api/routes/moderation.js:134: AddLIMIT 1to theWHERE case_number = $1 AND guild_id = $2query as a safety measure, since there's no UNIQUE constraint on (guild_id, case_number). -
Update AGENTS.md Key Files table: Add entries for
src/api/routes/moderation.jsandweb/src/app/dashboard/moderation/page.tsx.
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 filtersGET /cases/:id— single case + scheduled actionsGET /stats— total/24h/7d counts, byAction breakdown, top targetsFrontend (14 new files, 1,749 lines)
Testing
71 test files, 1473 tests passing, 0 failures