Skip to content

Add dashboard permission access controls#360

Merged
BillChirico merged 29 commits intomainfrom
mohsin/volvox-bot-perms
Apr 3, 2026
Merged

Add dashboard permission access controls#360
BillChirico merged 29 commits intomainfrom
mohsin/volvox-bot-perms

Conversation

@MohsinCoding
Copy link
Copy Markdown
Contributor

Summary

This PR tightens and clarifies dashboard access behavior across the bot API and web app.

Changes

  • restore migration ordering compatibility by preserving the historical 014 migration and moving perf indexes to 015
  • fix Windows ESM command loading by importing command files via file:// URLs
  • add explicit guild access evaluation so configured moderator/admin roles are reflected in dashboard access
  • limit moderator dashboard access to the Moderation, Members, and Tickets sections
  • enforce those moderator/admin permissions on both the web proxy layer and bot API routes
  • improve Discord/bot API rate-limit handling so long retry windows do not stall dashboard requests

Copilot AI review requested due to automatic review settings March 25, 2026 00:26
@railway-app
Copy link
Copy Markdown

railway-app bot commented Mar 25, 2026

This PR was not deployed automatically as @MohsinCoding does not have access to the Railway project.

In order to get automatic PR deploys, please add @MohsinCoding to your workspace on Railway.

@github-project-automation github-project-automation bot moved this to Backlog in Volvox.Bot Mar 25, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Guild access levels introduced (viewer, moderator, admin, bot-owner) and new endpoint to fetch per-guild access.
    • New Config Editor UI for managing bot configuration.
    • Centralized guild directory/provider for dashboard components.
  • Bug Fixes & Improvements

    • Many member/ticket endpoints now allow moderator access where appropriate.
    • Sidebar and server selector now reflect moderator/admin/owner access and messaging.
    • Internal/trusted API requests are exempted from rate limiting.
    • API-secret requests may include a Discord user identity to attribute actions (e.g., XP adjustments).

Walkthrough

Adds per-guild access-level resolution and moderator-level authorization, trusted internal request handling via API secret, a guild-directory React context, a DB migration to backfill audit_logs, editor character-limit enforcement, and broad test updates covering the new authorization, rate‑limit, and UI flows.

Changes

Cohort / File(s) Summary
Database Migration
migrations/014_audit_logs_user_tag_backfill.cjs
Adds conditional migration to add/drop user_tag column and ensure idx_audit_logs_guild_user index without failing on repeated runs.
Trusted/Internal Request & Auth
src/api/middleware/trustedInternalRequest.js, src/api/middleware/auth.js, src/api/middleware/rateLimit.js, src/api/middleware/redisRateLimit.js
Adds isTrustedInternalRequest(req) for API-secret validation; rate-limit middlewares early-exit for trusted requests; API-secret auth now reads and validates x-discord-user-id and sets req.user.userId when valid.
Guild Access API & Server Routes
src/api/routes/guilds.js, src/api/routes/members.js, src/api/routes/tickets.js
Adds API-secret-only GET /guilds/access, extends requireGuildPermission with requiredAccess gate, updates OAuth guild listing to derive access levels, and changes several endpoints to require moderator-level access.
Bot-API Proxy & Access Library
web/src/lib/bot-api-proxy.ts, web/src/app/api/guilds/route.ts
Introduces GuildAccessLevel, moderator-permission detection, authorizeGuildAccess/authorizeGuildModerator, and applyAccessLevels which batches calls to bot API /guilds/access and augments mutual-guild responses.
Web Route Authorization Updates
web/src/app/api/guilds/..., web/src/app/api/moderation/..., web/src/app/api/.../members/xp/route.ts
Replaces many authorizeGuildAdmin usages with authorizeGuildModerator; XP route now reads session token and forwards x-discord-user-id to bot API, returning 401 if no user id available.
Guild Directory Context & Integration
web/src/components/layout/guild-directory-context.tsx, web/src/components/layout/dashboard-shell.tsx, web/src/components/layout/server-selector.tsx, web/src/components/layout/sidebar.tsx
Adds GuildDirectoryProvider + useGuildDirectory, wraps dashboard, updates ServerSelector to consume the provider, and makes Sidebar render nav based on selected guild access.
Types & Hooks
web/src/types/discord.ts, web/src/hooks/use-guild-role.ts
Adds optional access to MutualGuild, updates getGuildDashboardRole to prefer explicit access, remaps permission-bit handling so MANAGE_GUILD → moderator and ADMINISTRATOR → admin only.
Fetch Rate-Limit & Discord Helpers
web/src/lib/discord.server.ts
Adds FetchWithRateLimitOptions, parseRetryAfterMs, and retry-budget-aware logic; adjusts callers (fetchUserGuilds, fetchBotGuilds) to use tighter rate-limit options.
Editor Character-Limit Enforcement
web/src/components/ui/discord-markdown-editor.tsx, web/src/components/ui/embed-builder.tsx
Implements clamping of toolbar/variable edits to maxLength and enforces total embed character limits before applying patches.
Config Editor Component
web/src/components/dashboard/config-editor.tsx
Adds client ConfigEditor component rendering ConfigLayoutShell with AI/onboarding categories.
Command Loading & Tests Config
src/utils/loadCommands.js, vitest.config.js
Switches dynamic imports to use file:// URLs via pathToFileURL; increases Vitest timeouts and adds hookTimeout.
Tests — Middleware, Routes, UI, Libraries
tests/..., web/tests/... (many files)
Widespread test additions/updates: trusted-request and auth tests, guild-access endpoint coverage, moderator access flows, XP attribution tests, guild-directory and sidebar UI tests, rate-limit retry budget tests, editor/embed character-limit tests, and command-import adjustments. (See changed test files in summary for specifics.)
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add dashboard permission access controls' accurately summarizes the main change—introducing explicit permission-based access controls for dashboard features based on guild roles (admin vs moderator).
Description check ✅ Passed The description is well-structured, related to the changeset, and covers the key objectives: migration compatibility, Windows ESM fix, guild access evaluation, moderator dashboard restrictions, permission enforcement, and rate-limit improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mohsin/volvox-bot-perms
  • 🛠️ Publish Changes: Commit on current branch
  • 🛠️ Publish Changes: Create PR

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.

'/:id/members/export',
membersRateLimit,
requireGuildAdmin,
requireGuildModerator,

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

* $ref: "#/components/responses/ServiceUnavailable"
*/
router.get('/:id/members', membersRateLimit, requireGuildAdmin, validateGuild, async (req, res) => {
router.get('/:id/members', membersRateLimit, requireGuildModerator, validateGuild, async (req, res) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

'/:id/tickets/stats',
ticketRateLimit,
requireGuildAdmin,
requireGuildModerator,

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

'/:id/tickets/:ticketId',
ticketRateLimit,
requireGuildAdmin,
requireGuildModerator,

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

* $ref: "#/components/responses/ServiceUnavailable"
*/
router.get('/:id/tickets', ticketRateLimit, requireGuildAdmin, validateGuild, async (req, res) => {
router.get('/:id/tickets', ticketRateLimit, requireGuildModerator, validateGuild, async (req, res) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This PR introduces a two-tier dashboard permission model (moderator vs. admin) enforced at both the Next.js proxy layer and the Express API, replacing the previous admin-only gate on Members, Tickets, and Moderation routes. It also centralizes guild fetching into a GuildDirectoryProvider context, adds a bot-config-aware /guilds/access endpoint for role-based access evaluation, fixes Windows ESM command loading, and caps Discord rate-limit retry windows to prevent request stalls.

Confidence Score: 4/5

Safe to merge with minor improvements; no P0/P1 issues found.

All three remaining comments are P2. The most notable is isTrustedInternalRequest using plain === instead of crypto.timingSafeEqual for the shared secret — inconsistent with isValidSecret but not exploitable without a timing oracle over HTTP. The partial-chunk access-map drop in applyAccessLevels is a graceful degradation, and the sidebar flicker is cosmetic. Access control itself is correctly layered across both the web proxy and the bot API routes.

src/api/middleware/trustedInternalRequest.js (timing-safe comparison), web/src/app/api/guilds/route.ts (chunk failure handling)

Important Files Changed

Filename Overview
src/api/middleware/trustedInternalRequest.js New module that bypasses rate limiting for trusted internal (dashboard) requests; uses plain === comparison for the API secret instead of the constant-time crypto.timingSafeEqual used elsewhere.
src/api/routes/guilds.js Adds getGuildAccessLevel for bot-config-aware permission resolution, updates requireGuildPermission to use it, and introduces the new /access batch endpoint; logic is correct and well-tested.
web/src/app/api/guilds/route.ts New applyAccessLevels function fetches bot-evaluated access in 100-guild chunks; a mid-chunk failure drops access data already accumulated in earlier chunks before returning the original guild list.
web/src/lib/bot-api-proxy.ts Cleanly refactored to add authorizeGuildModerator alongside authorizeGuildAdmin; introduces resolveGuildAccess that correctly falls back to OAuth-derived permissions when the bot API is unavailable.
web/src/lib/discord.server.ts Adds maxRetryDelayMs and totalRetryBudgetMs caps to fetchWithRateLimit to prevent long Discord rate-limit windows from stalling requests; also parses x-ratelimit-reset-after header.
web/src/components/layout/guild-directory-context.tsx New React context that centralizes guild fetching for the dashboard shell, reducing duplicate fetch calls from ServerSelector and Sidebar; properly handles abort on unmount.
web/src/components/layout/sidebar.tsx Filters primary nav to Moderation/Members/Tickets for moderator-access guilds; null activeGuildAccess (during initial load) falls through to full nav, causing a brief unrestricted flicker for moderators.
src/api/middleware/auth.js Extends API-secret auth to accept an optional x-discord-user-id header (validated as a Discord snowflake) so the dashboard can forward the acting user's identity for audit attribution.
src/utils/loadCommands.js Switches from raw file path to file:// URL via pathToFileURL for ESM dynamic imports, fixing Windows compatibility.
migrations/014_audit_logs_user_tag_backfill.cjs Repair migration that backfills the user_tag column and idx_audit_logs_guild_user index for databases that ran an older version of 013_audit_log.cjs; correctly uses IF NOT EXISTS.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant NextJS as Next.js Web
    participant BotAPI as Bot API (Express)
    participant Discord

    Browser->>NextJS: GET /api/guilds
    NextJS->>Discord: fetchMutualGuilds(accessToken)
    Discord-->>NextJS: user guild list
    NextJS->>BotAPI: GET /guilds/access?userId=X&guildIds=...
    Note over NextJS,BotAPI: x-api-secret header (bypasses rate limit)
    BotAPI->>BotAPI: getGuildAccessLevel(guild, userId)
    BotAPI-->>NextJS: [{id, access}]
    NextJS-->>Browser: guilds with resolved access level

    Browser->>NextJS: GET /api/guilds/:id/members
    NextJS->>BotAPI: authorizeGuildModerator(token, guildId)
    Note over NextJS,BotAPI: resolveGuildAccess → /guilds/access
    BotAPI-->>NextJS: access level
    NextJS->>BotAPI: proxy GET /:id/members
    BotAPI-->>NextJS: member list
    NextJS-->>Browser: 200 OK
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/api/middleware/trustedInternalRequest.js
Line: 18-19

Comment:
**Plain string comparison for the shared secret**

`isTrustedInternalRequest` uses `providedSecret === expectedSecret` while `isValidSecret` in `auth.js` uses `crypto.timingSafeEqual` for the same `BOT_API_SECRET`. A timing-side-channel on this bypass path could leak the secret and allow an attacker to craft a valid API-secret request.

```suggestion
  return (
    typeof expectedSecret === 'string' &&
    expectedSecret.length > 0 &&
    typeof providedSecret === 'string' &&
    providedSecret.length === expectedSecret.length &&
    crypto.timingSafeEqual(Buffer.from(providedSecret), Buffer.from(expectedSecret))
  );
```

`crypto` would need to be imported at the top of the file with `import crypto from 'node:crypto';`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/app/api/guilds/route.ts
Line: 49-56

Comment:
**Partial chunk failure silently discards successfully resolved access data**

When iterating over chunks of guild IDs, if the first chunk (e.g., guilds 1–100) succeeds and populates `accessMap`, but the second chunk request fails, the function returns the original `guilds` parameter before calling `.map(...)`. This drops the access data already accumulated in `accessMap`, leaving all guilds without augmented access levels rather than applying partial results.

Consider continuing with the accumulated `accessMap` instead of returning early on a per-chunk failure, or at minimum document that this is an intentional all-or-nothing fallback.

```suggestion
      if (!response.ok) {
        logger.warn('[api/guilds] Failed to fetch bot access levels', {
          status: response.status,
          statusText: response.statusText,
          guildCount: guildIdChunk.length,
        });
        // Break out of the loop but still apply whatever we have in accessMap so far
        break;
      }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/components/layout/sidebar.tsx
Line: 111-113

Comment:
**`activeGuildAccess === null` shows the full nav instead of a restricted view**

When `guildId` is set but the guild hasn't loaded into `guilds` yet (e.g., initial render or refresh), `activeGuildAccess` is `null`. The expression `activeGuildAccess === 'moderator' ? moderatorPrimaryNav : primaryNav` then falls through to `primaryNav`, so a moderator briefly sees the unrestricted nav before the guild list resolves. This is a cosmetic flicker rather than a security bypass (the API enforces access), but it may be confusing.

Consider defaulting to `moderatorPrimaryNav` when `activeGuildAccess` is `null` and the user has no resolved access yet, or guard with a loading state.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (13): Last reviewed commit: "Merge branch 'main' into mohsin/volvox-b..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens dashboard access control by introducing explicit access levels (viewer/moderator/admin/bot-owner) and enforcing them consistently across the bot API, the Next.js proxy routes, and the dashboard UI. It also includes supporting fixes for migration ordering, Windows ESM command loading, and rate-limit retry behavior.

Changes:

  • Add bot-evaluated guild access levels and enforce moderator/admin authorization in both the bot API and the Next.js proxy layer.
  • Restrict moderator dashboard navigation to Moderation/Members/Tickets and add tests around role/access behavior.
  • Improve rate-limit handling (retry caps/budgets), skip rate limiting for trusted internal (x-api-secret) requests, and fix Windows command module imports via file:// URLs.

Reviewed changes

Copilot reviewed 38 out of 38 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
web/tests/lib/discord.test.ts Adds coverage for rate-limit retry caps/budgets and fail-fast behavior.
web/tests/lib/bot-api-proxy-branches.test.ts Adds branch tests for moderator permission detection and moderator authorization.
web/tests/hooks/use-guild-role.test.ts Adds test ensuring explicit API access overrides permission-bit fallback.
web/tests/components/layout/sidebar.test.tsx Adds test ensuring moderator-only access hides non-allowed sidebar pages.
web/tests/components/layout/server-selector.test.tsx Updates/manageability copy expectations and adds moderator-access manageable test.
web/tests/api/guilds.test.ts Adds test for augmenting /api/guilds with bot-evaluated access when configured.
web/src/types/discord.ts Extends MutualGuild with optional access field.
web/src/lib/discord.server.ts Introduces retry delay cap + total retry budget for Discord/bot API fetches.
web/src/lib/bot-api-proxy.ts Adds moderator access model and authorization helpers backed by bot API access evaluation.
web/src/hooks/use-guild-role.ts Uses explicit access field to derive dashboard role; updates manageable logic.
web/src/components/layout/sidebar.tsx Hides non-moderation navigation for guilds with moderator-only access.
web/src/components/layout/server-selector.tsx Updates empty-state copy for manageable server selection.
web/src/app/api/moderation/user/[userId]/history/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/moderation/stats/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/moderation/cases/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/moderation/cases/[id]/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/guilds/route.ts Augments Discord mutual guilds with bot-evaluated access levels when configured.
web/src/app/api/guilds/[guildId]/tickets/stats/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/guilds/[guildId]/tickets/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/guilds/[guildId]/members/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/guilds/[guildId]/members/export/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/guilds/[guildId]/members/[userId]/route.ts Switches auth gate from admin-only to moderator-or-higher.
web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts Switches auth gate from admin-only to moderator-or-higher.
tests/commands.test.js Fixes Windows ESM dynamic imports by using pathToFileURL(...).href.
tests/api/routes/guilds.coverage.test.js Adds coverage for new /guilds/access endpoint and updated permission behavior.
tests/api/middleware/redisRateLimit.test.js Adds coverage for skipping rate limits on trusted internal (x-api-secret) requests.
tests/api/middleware/rateLimit.test.js Adds coverage for skipping in-memory rate limits on trusted internal requests.
src/utils/loadCommands.js Fixes Windows ESM command loading using pathToFileURL imports.
src/api/routes/tickets.js Broadens ticket routes from admin-only to moderator-or-higher.
src/api/routes/members.js Broadens member routes from admin-only to moderator-or-higher.
src/api/routes/guilds.js Adds bot-side access resolution + new /guilds/access endpoint and integrates into auth middleware.
src/api/middleware/trustedInternalRequest.js Adds helper for identifying trusted internal requests via shared API secret.
src/api/middleware/redisRateLimit.js Skips Redis-backed rate limiting for trusted internal requests.
src/api/middleware/rateLimit.js Skips in-memory rate limiting for trusted internal requests.
migrations/015_perf_indexes.cjs Renumbers perf index migration comment to align with restored 014 slot.
migrations/014_audit_logs_user_tag_backfill.cjs Adds repair migration preserving historical 014 slot and backfilling audit_logs drift.

Comment on lines 28 to 34
export function getGuildDashboardRole(guild: MutualGuild): GuildDashboardRole {
if (guild.access === 'bot-owner') return 'admin';
if (guild.access === 'admin') return 'admin';
if (guild.access === 'moderator') return 'moderator';
if (guild.access === 'viewer') return 'viewer';
if (guild.owner) return 'owner';

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

In getGuildDashboardRole, (1) checking guild.access before guild.owner means an owner can be reported as admin/viewer when access is present; if you want the owner label to remain authoritative, handle guild.owner first. (2) This helper currently treats MANAGE_GUILD as admin (later in the function), but the server-side checks in this PR treat Manage Guild as moderator (e.g. isModerator() and hasModeratorPermission()). Aligning these avoids showing admin-only UI when access isn’t provided.

Copilot uses AI. Check for mistakes.
Comment on lines 71 to 74
logger.warn(
`[discord] Rate limited on ${url}, retrying in ${waitMs}ms (attempt ${attempt + 1}/${MAX_RETRIES})`,
`[discord] Rate limited on ${url}, retrying in ${waitMs}ms ` +
`(attempt ${attempt + 1}/${maxRetries}, remaining budget ${remainingBudgetMs}ms)`,
);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Same as above: the retry log prints (attempt ${attempt + 1}/${maxRetries}), but maxRetries is the retry count while attempt + 1 is the attempt count. This can exceed the denominator on the last retry (e.g. 4/3). Align the logged numerator/denominator with the actual meaning (attempts vs retries).

Copilot uses AI. Check for mistakes.
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
<Server className="mx-auto mb-1 h-4 w-4" />
You need mod or admin permissions to manage a server.
You need admin permissions to manage a server.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The empty-state copy says “You need admin permissions…”, but the component (and its comment just above) defines manageable servers as mod/admin/owner. Since moderators are explicitly supported, this message is misleading—consider changing it to mention moderator access (or reword to clarify it’s about full admin-only management).

Suggested change
You need admin permissions to manage a server.
You need moderator, admin, or owner permissions to manage a server.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +146
const realFetch = globalThis.fetch;
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => [{ id: "1", access: "moderator" }],
status: 200,
statusText: "OK",
} as Response);

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

This test replaces globalThis.fetch and only restores it at the end of the test body. If an assertion throws (or the test returns early), the restore won’t run and later tests may observe the mocked fetch. Prefer vi.spyOn(globalThis, 'fetch') with mockRestore() in afterEach, or wrap the override in try/finally so restoration is guaranteed.

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +152
useEffect(() => {
const controller = new AbortController();

(async () => {
try {
const response = await fetch('/api/guilds', { signal: controller.signal });
if (!response.ok) return;
const data: unknown = await response.json();
if (!Array.isArray(data)) return;
setGuilds(
data.filter(
(guild): guild is MutualGuild =>
typeof guild === 'object' &&
guild !== null &&
typeof (guild as { id?: unknown }).id === 'string' &&
typeof (guild as { name?: unknown }).name === 'string' &&
typeof (guild as { permissions?: unknown }).permissions === 'string' &&
typeof (guild as { owner?: unknown }).owner === 'boolean',
),
);
} catch {
// Keep full nav if the guild list cannot be loaded.
}
})();

return () => controller.abort();
}, []);

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Sidebar fetches /api/guilds on mount to determine the active guild’s access, but ServerSelector already fetches the same endpoint in the same layout (DashboardShell). This creates duplicate requests (and duplicated runtime shape checks) on every dashboard load. Consider centralizing guild list/access into a shared hook/context (or persisting the selected guild’s access alongside the selected guild ID) so both components can reuse a single fetch/result.

Suggested change
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
const response = await fetch('/api/guilds', { signal: controller.signal });
if (!response.ok) return;
const data: unknown = await response.json();
if (!Array.isArray(data)) return;
setGuilds(
data.filter(
(guild): guild is MutualGuild =>
typeof guild === 'object' &&
guild !== null &&
typeof (guild as { id?: unknown }).id === 'string' &&
typeof (guild as { name?: unknown }).name === 'string' &&
typeof (guild as { permissions?: unknown }).permissions === 'string' &&
typeof (guild as { owner?: unknown }).owner === 'boolean',
),
);
} catch {
// Keep full nav if the guild list cannot be loaded.
}
})();
return () => controller.abort();
}, []);

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +159
/**
* Verify that the incoming request is from the owner or an administrator of the specified guild.
*
* @param request - The incoming NextRequest containing the user's session/token.
* @param guildId - The Discord guild ID to authorize against.
* @param logPrefix - Prefix used when logging contextual error messages.
* @returns `null` if the requester is authorized; a `NextResponse` containing an error JSON otherwise.
* Possible responses:
* - 401 Unauthorized when the access token is missing or expired.
* - 502 Bad Gateway when mutual guilds cannot be verified.
* - 403 Forbidden when the user is neither the guild owner nor has administrator permission.
*/
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The JSDoc for authorizeGuildAdmin still describes the check as “owner or administrator permission”, but the implementation now also authorizes bot-owner (and may derive admin/moderator via the bot API access endpoint). Updating the wording (and the 403 bullet) would keep the docs aligned with the new access model.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +67
if (attempt === maxRetries || waitMs > maxRetryDelayMs || waitMs > remainingBudgetMs) {
logger.warn(
`[discord] Rate limited on ${url}, not retrying after ${waitMs}ms ` +
`(attempt ${attempt + 1}/${maxRetries}, remaining budget ${Math.max(remainingBudgetMs, 0)}ms)`,
);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

maxRetries is treated as the number of retries (loop runs attempt <= maxRetries), but the log prints (attempt ${attempt + 1}/${maxRetries}), which will produce confusing output like 4/3 on the final attempt when maxRetries=3. Consider logging either (attempt ${attempt + 1}/${maxRetries + 1}) (total attempts) or retry ${attempt}/${maxRetries} (retry count) to match the control flow.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +34
try {
const url = new URL(`${botApiBaseUrl}/guilds/access`);
url.searchParams.set('userId', userId);
url.searchParams.set(
'guildIds',
guilds
.filter((guild) => guild.botPresent)
.map((guild) => guild.id)
.join(','),
);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

applyAccessLevels() will still call the bot API even when no guilds have botPresent (because guilds.length !== 0), sending guildIds= as an empty string. Consider computing the botPresent IDs first and returning early when that list is empty to avoid an unnecessary internal request.

Suggested change
try {
const url = new URL(`${botApiBaseUrl}/guilds/access`);
url.searchParams.set('userId', userId);
url.searchParams.set(
'guildIds',
guilds
.filter((guild) => guild.botPresent)
.map((guild) => guild.id)
.join(','),
);
const botGuildIds = guilds
.filter((guild) => guild.botPresent)
.map((guild) => guild.id);
if (botGuildIds.length === 0) {
return guilds;
}
try {
const url = new URL(`${botApiBaseUrl}/guilds/access`);
url.searchParams.set('userId', userId);
url.searchParams.set('guildIds', botGuildIds.join(','));

Copilot uses AI. Check for mistakes.
acc.push({
if (!botGuild) continue;

const access = await getGuildAccessLevel(botGuild, req.user.userId);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

In GET /guilds, getGuildAccessLevel() may call guild.members.fetch(userId) when the member isn’t cached. Doing this inside the loop over userGuilds can trigger up to O(N) Discord REST calls per request (potentially ~200), which risks rate-limits and slow responses. Since fetchUserGuilds() already returns ug.permissions (and ug.owner), consider short-circuiting: derive access from those bits first (Admin/ManageGuild), and only call getGuildAccessLevel() for guilds where OAuth permissions don’t grant mod/admin but configured roles might.

Suggested change
const access = await getGuildAccessLevel(botGuild, req.user.userId);
// Derive access from OAuth guild permissions first to avoid extra Discord REST calls.
// If OAuth grants ADMINISTRATOR or MANAGE_GUILD (or the user is guild owner),
// we can short-circuit without calling getGuildAccessLevel(), which may fetch members.
let access;
const isOwner = Boolean(ug.owner);
let permsBigInt = 0n;
if (ug.permissions != null) {
try {
// Discord returns permissions as a stringified integer.
permsBigInt = BigInt(ug.permissions);
} catch {
permsBigInt = 0n;
}
}
const ADMINISTRATOR = 0x8n;
const MANAGE_GUILD = 0x20n;
const hasAdmin = isOwner || (permsBigInt & ADMINISTRATOR) === ADMINISTRATOR;
const hasManageGuild =
!hasAdmin && (permsBigInt & MANAGE_GUILD) === MANAGE_GUILD;
if (hasAdmin) {
access = 'admin';
} else if (hasManageGuild) {
access = 'moderator';
} else {
access = await getGuildAccessLevel(botGuild, req.user.userId);
}

Copilot uses AI. Check for mistakes.
Comment on lines +502 to +517
const guildIds = [...new Set(guildIdsRaw.split(',').map((id) => id.trim()).filter(Boolean))];
if (guildIds.length === 0) {
return res.json([]);
}

const { client } = req.app.locals;

const accessEntries = await Promise.all(
guildIds.map(async (guildId) => {
const guild = client.guilds.cache.get(guildId);
if (!guild) return null;

const access = await getGuildAccessLevel(guild, userId);
return { id: guildId, access };
}),
);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

GET /guilds/access accepts an arbitrary comma-separated guildIds list and then runs getGuildAccessLevel() for each ID concurrently via Promise.all. With a large list, this can fan out into many guild.members.fetch() calls at once, amplifying Discord rate-limit pressure and memory usage. Consider enforcing a reasonable max number of guildIds per request and/or adding concurrency limiting + short TTL caching for {guildId,userId} access results.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
web/src/app/api/guilds/[guildId]/members/route.ts (1)

15-31: ⚠️ Potential issue | 🟡 Minor

Update the authorization wording in the function docs.

Line 17 says “requester is a guild admin,” but Line 30 now checks moderator authorization.

Proposed fix
- * Validates presence of `guildId` and that the requester is a guild admin, forwards `limit`, `after`, `search`, `sort`, and `order` query parameters to the upstream `/guilds/{guildId}/members` path, and proxies the response from the bot API.
+ * Validates presence of `guildId` and that the requester is a guild moderator, forwards `limit`, `after`, `search`, `sort`, and `order` query parameters to the upstream `/guilds/{guildId}/members` path, and proxies the response from the bot API.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/guilds/`[guildId]/members/route.ts around lines 15 - 31, The
function comment incorrectly states "requester is a guild admin" while the
actual authorization call is authorizeGuildModerator in the GET handler; update
the docblock to reflect that the requester must be a guild moderator (or use
consistent terminology "moderator" across the doc) and mention
authorizeGuildModerator/LOG_PREFIX as the enforcing mechanism so the description
matches the implemented check.
web/src/app/api/guilds/[guildId]/members/export/route.ts (1)

12-28: ⚠️ Potential issue | 🟡 Minor

JSDoc still documents admin-only access.

Line 14 says “guild-admin authorization,” but Line 27 now authorizes moderators as well.

Proposed fix
- * Validates the route parameter, enforces guild-admin authorization, forwards the upstream export request with a timeout, and returns the upstream CSV body with appropriate `Content-Type` and `Content-Disposition`. On failure returns a JSON error response with an appropriate HTTP status (e.g., 400 for missing guildId, the upstream status for upstream errors, 504 for timeouts, or 500 for internal failures).
+ * Validates the route parameter, enforces guild-moderator authorization, forwards the upstream export request with a timeout, and returns the upstream CSV body with appropriate `Content-Type` and `Content-Disposition`. On failure returns a JSON error response with an appropriate HTTP status (e.g., 400 for missing guildId, the upstream status for upstream errors, 504 for timeouts, or 500 for internal failures).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/guilds/`[guildId]/members/export/route.ts around lines 12 -
28, The JSDoc incorrectly states "guild-admin authorization" but the code uses
authorizeGuildModerator (and LOG_PREFIX) to allow moderators; update the
function comment above export GET to accurately describe that the route enforces
guild-moderator (not admin-only) authorization, and adjust any wording about who
can access the export and required permissions to match the
authorizeGuildModerator behavior used in the GET handler.
web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts (1)

15-31: ⚠️ Potential issue | 🟡 Minor

Doc comment is outdated after the moderator auth change.

Line 17 still documents guild-admin authorization, but Line 30 now enforces moderator authorization.

Proposed fix
- * Validates route parameters, enforces guild-admin authorization, forwards the original query parameters, and returns the upstream bot API response.
+ * Validates route parameters, enforces guild-moderator authorization, forwards the original query parameters, and returns the upstream bot API response.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/guilds/`[guildId]/members/[userId]/cases/route.ts around
lines 15 - 31, The top doc comment for the GET handler is outdated: it says
"enforces guild-admin authorization" but the code calls authorizeGuildModerator
(with LOG_PREFIX) and enforces moderator authorization; update the comment block
above the GET function to describe that the route validates params, enforces
guild-moderator authorization via authorizeGuildModerator, forwards query params
to the bot API, and returns the upstream NextResponse or error (e.g., 400 for
missing guildId/userId or authorization error) so the documentation matches the
actual behavior.
web/src/app/api/moderation/cases/route.ts (1)

15-27: ⚠️ Potential issue | 🟡 Minor

Update JSDoc to match the new authorization level.

Line 18 still states “admin authorization,” but Line 26 now enforces moderator authorization.

Proposed fix
 /**
  * GET /api/moderation/cases
  * Proxies to bot API GET /api/v1/moderation/cases
- * Requires guildId query param and admin authorization.
+ * Requires guildId query param and moderator authorization.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/moderation/cases/route.ts` around lines 15 - 27, The JSDoc
above the exported GET function is out of date—update the comment to reflect
moderator (not admin) authorization: change the description line that currently
says “Requires guildId query param and admin authorization.” to reference
moderator authorization, and mention that authorizeGuildModerator is used (see
GET and authorizeGuildModerator with LOG_PREFIX) so the docstring matches the
enforced behavior.
web/src/app/api/moderation/stats/route.ts (1)

14-25: ⚠️ Potential issue | 🟡 Minor

Stale JSDoc comment.

The comment on line 17 states "Requires guildId query param and admin authorization" but the implementation now uses authorizeGuildModerator. Update the documentation to reflect the actual authorization requirement.

📝 Proposed fix
 /**
  * GET /api/moderation/stats
  * Proxies to bot API GET /api/v1/moderation/stats
- * Requires guildId query param and admin authorization.
+ * Requires guildId query param and moderator authorization.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/moderation/stats/route.ts` around lines 14 - 25, Update the
stale JSDoc above the GET handler to reflect the current authorization check:
replace "admin authorization" with the accurate requirement (e.g., "guild
moderator authorization via authorizeGuildModerator"). Ensure the comment
references the guildId query param and the use of authorizeGuildModerator and
LOG_PREFIX so readers understand the actual authorization logic used by the
GET(request: NextRequest) function.
web/src/app/api/guilds/[guildId]/members/[userId]/route.ts (1)

14-34: ⚠️ Potential issue | 🟡 Minor

Stale JSDoc comment.

The comment on line 17 states "enforces guild admin authorization" but the implementation now uses authorizeGuildModerator. Update the documentation to reflect the actual authorization level.

📝 Proposed fix
 /**
  * Proxy a GET request for a guild member's details to the bot API.
  *
- * Validates required path parameters, enforces guild admin authorization, builds the upstream URL,
+ * Validates required path parameters, enforces guild moderator authorization, builds the upstream URL,
  * and forwards the request to the bot API. Returns a 400 response if `guildId` or `userId` is missing,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/guilds/`[guildId]/members/[userId]/route.ts around lines 14 -
34, Update the stale JSDoc on the GET handler to reflect the actual
authorization check: replace "enforces guild admin authorization" with "enforces
guild moderator authorization" (or otherwise mention use of
authorizeGuildModerator) and ensure the summary and param/return descriptions
remain accurate for the GET function that calls authorizeGuildModerator
(referencing authorizeGuildModerator, GET, and LOG_PREFIX to locate the code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@migrations/014_audit_logs_user_tag_backfill.cjs`:
- Around line 30-36: Add a brief inline comment inside the exports.down function
explaining why the index created in the corresponding up migration
(idx_audit_logs_guild_user) is intentionally not dropped here; reference the
existing migration that also creates that index (013_audit_log.cjs) and state
that dropping the index here would cause inconsistency with that migration, so
only the user_tag column is removed in exports.down.

In `@src/api/routes/guilds.js`:
- Around line 259-264: The current blanket catch around
guild.members.fetch(userId) hides transient Discord failures; change the catch
to only swallow the "unknown member" error (Discord API error code 10007 or
error.message includes "Unknown Member") by setting member = null for that case,
and rethrow any other errors so they bubble up and result in a 5xx from /access;
apply the same change to the other fetch sites mentioned (the GET /guilds
filter, the GET /access fan-out handling, and the permission middleware) so only
the explicit "unknown member" case is treated as missing and all other Discord
errors are propagated.

In `@web/src/app/api/guilds/route.ts`:
- Around line 57-72: The filter currently only checks that entry.access is a
string, which allows unknown values through; update the runtime validation in
the accessEntries filter used to build accessMap to ensure entry.access is one
of the allowed literals ('admin','moderator','viewer','bot-owner') (e.g., check
against a Set of VALID_ACCESS values) before mapping to [entry.id,
entry.access], so the resulting accessMap.get(guild.id) can never return an
unexpected string and MutualGuild['access'] remains one of the known enum
values.

In `@web/src/components/layout/server-selector.tsx`:
- Line 272: Update the empty-state copy to match the actual access rules
enforced by isGuildManageable(): replace the current admin-only message with one
that mentions moderator/admin access (e.g., "You need moderator or admin
permissions to manage a server.") so the UI text aligns with the behavior of
isGuildManageable() in server-selector.tsx.

In `@web/src/components/layout/sidebar.tsx`:
- Around line 108-113: The current logic uses activeGuildAccess (derived from
guilds) and defaults to showing primaryNav when access is null, so moderators
see the full nav on first paint or on fetch errors; change the fallback to a
restrictive default until access is known: update the expressions that set
visiblePrimaryNav and visibleSecondaryNav so they only show primaryNav when
activeGuildAccess === 'moderator' and otherwise hide primary (e.g.,
visiblePrimaryNav = activeGuildAccess === 'moderator' ? moderatorPrimaryNav :
[]) while keeping visibleSecondaryNav = activeGuildAccess === 'moderator' ? [] :
secondaryNav, and ensure references to guilds and activeGuildAccess remain
unchanged elsewhere.

In `@web/src/hooks/use-guild-role.ts`:
- Around line 29-33: The current role resolution checks explicit access levels
before honoring guild.owner, causing { owner: true, access: 'viewer' } to return
'viewer'; update the logic in use-guild-role to first short-circuit if
guild.owner is truthy (return 'owner') and also handle access === 'owner' before
falling back to the 'viewer' branch, then keep the remaining checks for
'bot-owner', 'admin', and 'moderator' as-is so ownership always takes precedence
over augmented access.

In `@web/src/lib/bot-api-proxy.ts`:
- Around line 102-110: The current entries.find predicate accepts any string for
entry.access so typos/new values override fallbackAccess and can cause hard
denies in accessSatisfiesRequirement; update the predicate and/or the return to
whitelist supported GuildAccessLevel values (use the GuildAccessLevel type/enum
or a Set of allowed values) and only accept entry.access when it matches one of
those supported values, otherwise fall back to fallbackAccess; e.g., locate the
entries.find predicate and the return that uses entry?.access and replace the
unconditional use of entry.access with a validated value
(supported.includes(entry.access) ? entry.access : fallbackAccess) so unknown
strings never override the fallback.
- Around line 58-63: resolveGuildAccess creates independent timeouts for
getMutualGuilds and the subsequent /guilds/access call, allowing the overall
auth path to exceed the intended REQUEST_TIMEOUT_MS; fix by creating a single
AbortController/AbortSignal with a single timeout budget (use AbortController +
setTimeout or compute a shared deadline) at the start of resolveGuildAccess and
pass that same signal into getMutualGuilds and the downstream request (the call
that hits "/guilds/access" / proxyToBotApi), instead of calling
AbortSignal.timeout(...) twice; update calls to
getMutualGuilds(token.accessToken, sharedSignal) and the proxyToBotApi/fetch for
"/guilds/access" to accept and use the same sharedSignal so the whole auth check
shares one deadline.

In `@web/src/lib/discord.server.ts`:
- Around line 29-35: In parseRetryAfterMs, don’t prefer the raw 'retry-after'
header before validating it; instead attempt to parse 'retry-after' and if that
yields an invalid value then try parsing 'x-ratelimit-reset-after' and only fall
back to 1000ms if both parse to non-positive/NaN; update parseRetryAfterMs to
read both headers (response.headers.get('retry-after') and
response.headers.get('x-ratelimit-reset-after')), parse each into a finite
positive number (multiply by 1000 for ms) and return the first valid one,
otherwise return 1000.

In `@web/tests/api/guilds.test.ts`:
- Around line 39-45: The tests currently clear mocks and set NEXTAUTH_SECRET in
beforeEach but delete BOT_API_URL/BOT_API_SECRET and override globalThis.fetch
(and call mockGetBotApiBaseUrl.mockReturnValue(null)) without restoring them,
which can leak into other suites; add an afterEach teardown that restores
process.env.BOT_API_URL and process.env.BOT_API_SECRET to their original values
(capture originals at the top of the suite), restores globalThis.fetch to its
original value, and resets mockGetBotApiBaseUrl (and any other mocks) so that
functions like beforeEach, mockGetBotApiBaseUrl, and globalThis.fetch are
returned to their pre-test state.

In `@web/tests/lib/bot-api-proxy-branches.test.ts`:
- Around line 145-157: The test only stubs the global fetch response once via
mockResolvedValueOnce so authorizeGuildModerator(...) gets the mocked
[{id:'guild-1', access:'moderator'}] but authorizeGuildAdmin(...) falls back to
error behavior; update the test to mock the /guilds/access response for both
authorization calls—either add a second (globalThis.fetch as ReturnType<typeof
vi.fn>).mockResolvedValueOnce(...) with the same moderator access payload or
replace mockResolvedValueOnce with mockResolvedValue so both
authorizeGuildModerator and authorizeGuildAdmin receive the intended [{ id:
'guild-1', access: 'moderator' }] response while keeping mockGetToken and
mockGetMutualGuilds as-is.

In `@web/tests/lib/discord.test.ts`:
- Around line 473-493: The test "fails fast when bot API retry-after is too
large" uses a retry-after of "728"s which exceeds the generic fetchWithRateLimit
cap and doesn't validate the bot-specific fail-fast; update the mocked response
headers in this test (the fetchSpy mock used by fetchBotGuilds) to return a
sub-5s retry-after (e.g., "1") so the bot-specific cap is exercised and this
becomes a true regression test for fetchBotGuilds' bot API path.

---

Outside diff comments:
In `@web/src/app/api/guilds/`[guildId]/members/[userId]/cases/route.ts:
- Around line 15-31: The top doc comment for the GET handler is outdated: it
says "enforces guild-admin authorization" but the code calls
authorizeGuildModerator (with LOG_PREFIX) and enforces moderator authorization;
update the comment block above the GET function to describe that the route
validates params, enforces guild-moderator authorization via
authorizeGuildModerator, forwards query params to the bot API, and returns the
upstream NextResponse or error (e.g., 400 for missing guildId/userId or
authorization error) so the documentation matches the actual behavior.

In `@web/src/app/api/guilds/`[guildId]/members/[userId]/route.ts:
- Around line 14-34: Update the stale JSDoc on the GET handler to reflect the
actual authorization check: replace "enforces guild admin authorization" with
"enforces guild moderator authorization" (or otherwise mention use of
authorizeGuildModerator) and ensure the summary and param/return descriptions
remain accurate for the GET function that calls authorizeGuildModerator
(referencing authorizeGuildModerator, GET, and LOG_PREFIX to locate the code).

In `@web/src/app/api/guilds/`[guildId]/members/export/route.ts:
- Around line 12-28: The JSDoc incorrectly states "guild-admin authorization"
but the code uses authorizeGuildModerator (and LOG_PREFIX) to allow moderators;
update the function comment above export GET to accurately describe that the
route enforces guild-moderator (not admin-only) authorization, and adjust any
wording about who can access the export and required permissions to match the
authorizeGuildModerator behavior used in the GET handler.

In `@web/src/app/api/guilds/`[guildId]/members/route.ts:
- Around line 15-31: The function comment incorrectly states "requester is a
guild admin" while the actual authorization call is authorizeGuildModerator in
the GET handler; update the docblock to reflect that the requester must be a
guild moderator (or use consistent terminology "moderator" across the doc) and
mention authorizeGuildModerator/LOG_PREFIX as the enforcing mechanism so the
description matches the implemented check.

In `@web/src/app/api/moderation/cases/route.ts`:
- Around line 15-27: The JSDoc above the exported GET function is out of
date—update the comment to reflect moderator (not admin) authorization: change
the description line that currently says “Requires guildId query param and admin
authorization.” to reference moderator authorization, and mention that
authorizeGuildModerator is used (see GET and authorizeGuildModerator with
LOG_PREFIX) so the docstring matches the enforced behavior.

In `@web/src/app/api/moderation/stats/route.ts`:
- Around line 14-25: Update the stale JSDoc above the GET handler to reflect the
current authorization check: replace "admin authorization" with the accurate
requirement (e.g., "guild moderator authorization via authorizeGuildModerator").
Ensure the comment references the guildId query param and the use of
authorizeGuildModerator and LOG_PREFIX so readers understand the actual
authorization logic used by the GET(request: NextRequest) function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ff78055-4f9f-4a85-82bf-46657b944fc8

📥 Commits

Reviewing files that changed from the base of the PR and between 77f95b6 and e6f55ce.

📒 Files selected for processing (38)
  • migrations/014_audit_logs_user_tag_backfill.cjs
  • migrations/015_perf_indexes.cjs
  • src/api/middleware/rateLimit.js
  • src/api/middleware/redisRateLimit.js
  • src/api/middleware/trustedInternalRequest.js
  • src/api/routes/guilds.js
  • src/api/routes/members.js
  • src/api/routes/tickets.js
  • src/utils/loadCommands.js
  • tests/api/middleware/rateLimit.test.js
  • tests/api/middleware/redisRateLimit.test.js
  • tests/api/routes/guilds.coverage.test.js
  • tests/commands.test.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/route.ts
  • 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/components/layout/server-selector.tsx
  • web/src/components/layout/sidebar.tsx
  • web/src/hooks/use-guild-role.ts
  • web/src/lib/bot-api-proxy.ts
  • web/src/lib/discord.server.ts
  • web/src/types/discord.ts
  • web/tests/api/guilds.test.ts
  • web/tests/components/layout/server-selector.test.tsx
  • web/tests/components/layout/sidebar.test.tsx
  • web/tests/hooks/use-guild-role.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (13)
migrations/*.cjs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Database migrations must use .cjs extension (CommonJS) because node-pg-migrate does not support ESM; use sequential numbering (001_, 002_, etc.) and always use parameterized queries—never string interpolation

Files:

  • migrations/015_perf_indexes.cjs
  • migrations/014_audit_logs_user_tag_backfill.cjs
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • web/src/app/api/moderation/cases/route.ts
  • tests/commands.test.js
  • web/src/components/layout/server-selector.tsx
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/tests/hooks/use-guild-role.test.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • src/utils/loadCommands.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • src/api/middleware/redisRateLimit.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/types/discord.ts
  • web/src/hooks/use-guild-role.ts
  • src/api/middleware/trustedInternalRequest.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • src/api/routes/tickets.js
  • web/tests/components/layout/server-selector.test.tsx
  • tests/api/middleware/rateLimit.test.js
  • src/api/routes/members.js
  • tests/api/middleware/redisRateLimit.test.js
  • src/api/middleware/rateLimit.js
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • tests/api/routes/guilds.coverage.test.js
  • web/tests/components/layout/sidebar.test.tsx
  • web/tests/lib/discord.test.ts
  • web/src/components/layout/sidebar.tsx
  • web/tests/api/guilds.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • src/api/routes/guilds.js
  • web/src/lib/bot-api-proxy.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use console.* methods in web dashboard code; use appropriate logging mechanisms for React applications

Files:

  • web/src/app/api/moderation/cases/route.ts
  • web/src/components/layout/server-selector.tsx
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/types/discord.ts
  • web/src/hooks/use-guild-role.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • web/src/components/layout/sidebar.tsx
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
**/*.{js,mjs,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only

Files:

  • web/src/app/api/moderation/cases/route.ts
  • tests/commands.test.js
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/tests/hooks/use-guild-role.test.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • src/utils/loadCommands.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • src/api/middleware/redisRateLimit.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/types/discord.ts
  • web/src/hooks/use-guild-role.ts
  • src/api/middleware/trustedInternalRequest.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • src/api/routes/tickets.js
  • tests/api/middleware/rateLimit.test.js
  • src/api/routes/members.js
  • tests/api/middleware/redisRateLimit.test.js
  • src/api/middleware/rateLimit.js
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • tests/api/routes/guilds.coverage.test.js
  • web/tests/lib/discord.test.ts
  • web/tests/api/guilds.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • src/api/routes/guilds.js
  • web/src/lib/bot-api-proxy.ts
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts}: Use src/logger.js; do not use console.*
Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls
Use parameterized SQL only
Community features should be gated behind config..enabled. Moderation commands are the exception

Files:

  • web/src/app/api/moderation/cases/route.ts
  • tests/commands.test.js
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/tests/hooks/use-guild-role.test.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • src/utils/loadCommands.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • src/api/middleware/redisRateLimit.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/types/discord.ts
  • web/src/hooks/use-guild-role.ts
  • src/api/middleware/trustedInternalRequest.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • src/api/routes/tickets.js
  • tests/api/middleware/rateLimit.test.js
  • src/api/routes/members.js
  • tests/api/middleware/redisRateLimit.test.js
  • src/api/middleware/rateLimit.js
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • tests/api/routes/guilds.coverage.test.js
  • web/tests/lib/discord.test.ts
  • web/tests/api/guilds.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • src/api/routes/guilds.js
  • web/src/lib/bot-api-proxy.ts
web/**/*.{css,scss,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

web/**/*.{css,scss,ts,tsx}: Check both light and dark themes if colors or theming changed
Check responsive behavior if layout changed. Verify on mobile, tablet, and desktop

Files:

  • web/src/app/api/moderation/cases/route.ts
  • web/src/components/layout/server-selector.tsx
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/tests/hooks/use-guild-role.test.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/types/discord.ts
  • web/src/hooks/use-guild-role.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • web/tests/components/layout/server-selector.test.tsx
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/components/layout/sidebar.test.tsx
  • web/tests/lib/discord.test.ts
  • web/src/components/layout/sidebar.tsx
  • web/tests/api/guilds.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
web/**/*.{ts,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Follow the design system when making changes to the UI/UX. If you don't follow the design system, your changes will be rejected

Files:

  • web/src/app/api/moderation/cases/route.ts
  • web/src/components/layout/server-selector.tsx
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/tests/hooks/use-guild-role.test.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/types/discord.ts
  • web/src/hooks/use-guild-role.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • web/tests/components/layout/server-selector.test.tsx
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/components/layout/sidebar.test.tsx
  • web/tests/lib/discord.test.ts
  • web/src/components/layout/sidebar.tsx
  • web/tests/api/guilds.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
{web/**/*.{ts,tsx},**/DESIGN.md}

📄 CodeRabbit inference engine (AGENTS.md)

Always update the design system when making changes to the UI/UX

Files:

  • web/src/app/api/moderation/cases/route.ts
  • web/src/components/layout/server-selector.tsx
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/tests/hooks/use-guild-role.test.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/types/discord.ts
  • web/src/hooks/use-guild-role.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • web/tests/components/layout/server-selector.test.tsx
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/components/layout/sidebar.test.tsx
  • web/tests/lib/discord.test.ts
  • web/src/components/layout/sidebar.tsx
  • web/tests/api/guilds.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • tests/commands.test.js
  • src/utils/loadCommands.js
  • src/api/middleware/redisRateLimit.js
  • src/api/middleware/trustedInternalRequest.js
  • src/api/routes/tickets.js
  • tests/api/middleware/rateLimit.test.js
  • src/api/routes/members.js
  • tests/api/middleware/redisRateLimit.test.js
  • src/api/middleware/rateLimit.js
  • tests/api/routes/guilds.coverage.test.js
  • src/api/routes/guilds.js
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/commands.test.js
  • tests/api/middleware/rateLimit.test.js
  • tests/api/middleware/redisRateLimit.test.js
  • tests/api/routes/guilds.coverage.test.js
web/tests/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

web/tests/**/*.test.{ts,tsx}: Write web dashboard tests using Vitest 4 with the jsdom environment and React Testing Library, matching the web/src/ structure
Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Files:

  • web/tests/hooks/use-guild-role.test.ts
  • web/tests/components/layout/server-selector.test.tsx
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/components/layout/sidebar.test.tsx
  • web/tests/lib/discord.test.ts
  • web/tests/api/guilds.test.ts
src/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.js: Never use console.* methods; use the Winston logger instead via import logger from '../logger.js' (adjust path as needed), then call logger.info(), logger.warn(), logger.error(), or logger.debug()
Always use safeReply(), safeSend(), or safeEditReply() instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Files:

  • src/utils/loadCommands.js
  • src/api/middleware/redisRateLimit.js
  • src/api/middleware/trustedInternalRequest.js
  • src/api/routes/tickets.js
  • src/api/routes/members.js
  • src/api/middleware/rateLimit.js
  • src/api/routes/guilds.js
src/api/routes/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create API route handlers in src/api/routes/ and mount them in src/api/server.js; add auth middleware if the endpoint requires authentication

Files:

  • src/api/routes/tickets.js
  • src/api/routes/members.js
  • src/api/routes/guilds.js
🧠 Learnings (20)
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to migrations/*.cjs : Rename database migration files to maintain sequential ordering and avoid conflicts with production migrations that have already been executed

Applied to files:

  • migrations/015_perf_indexes.cjs
  • migrations/014_audit_logs_user_tag_backfill.cjs
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to migrations/*.cjs : Database migrations must use `.cjs` extension (CommonJS) because node-pg-migrate does not support ESM; use sequential numbering (001_, 002_, etc.) and always use parameterized queries—never string interpolation

Applied to files:

  • migrations/015_perf_indexes.cjs
  • migrations/014_audit_logs_user_tag_backfill.cjs
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources

Applied to files:

  • web/src/app/api/moderation/cases/route.ts
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/tests/hooks/use-guild-role.test.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/hooks/use-guild-role.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • src/api/routes/tickets.js
  • web/tests/components/layout/server-selector.test.tsx
  • src/api/routes/members.js
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • tests/api/routes/guilds.coverage.test.js
  • web/tests/components/layout/sidebar.test.tsx
  • web/src/app/api/guilds/route.ts
  • src/api/routes/guilds.js
  • web/src/lib/bot-api-proxy.ts
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id

Applied to files:

  • web/src/app/api/moderation/cases/route.ts
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • src/api/routes/tickets.js
  • web/tests/components/layout/server-selector.test.tsx
  • src/api/routes/members.js
  • tests/api/routes/guilds.coverage.test.js
  • web/src/app/api/guilds/route.ts
  • src/api/routes/guilds.js
  • web/src/lib/bot-api-proxy.ts
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/app/api/**/route.ts : Include guildId in signed WebSocket ticket payload when issuing tickets from dashboard endpoints

Applied to files:

  • web/src/app/api/moderation/cases/route.ts
  • web/src/app/api/moderation/user/[userId]/history/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/moderation/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/moderation/cases/[id]/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/hooks/use-guild-role.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • src/api/routes/tickets.js
  • src/api/routes/members.js
  • web/tests/api/guilds.test.ts
  • web/src/app/api/guilds/route.ts
  • src/api/routes/guilds.js
  • web/src/lib/bot-api-proxy.ts
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Write bot tests using Vitest 4 with the `node` environment, matching the `src/` structure in the `tests/` directory

Applied to files:

  • tests/commands.test.js
  • tests/api/middleware/rateLimit.test.js
  • tests/api/middleware/redisRateLimit.test.js
  • tests/api/routes/guilds.coverage.test.js
  • web/tests/components/layout/sidebar.test.tsx
  • web/tests/api/guilds.test.ts
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Write web dashboard tests using Vitest 4 with the `jsdom` environment and React Testing Library, matching the `web/src/` structure

Applied to files:

  • tests/commands.test.js
  • tests/api/middleware/redisRateLimit.test.js
  • web/tests/components/layout/sidebar.test.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/commands/**/*.js : Create slash command definitions in `src/commands/`, exporting a slash command builder and an `execute` function

Applied to files:

  • tests/commands.test.js
  • src/utils/loadCommands.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • web/tests/hooks/use-guild-role.test.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/members/export/route.ts
  • web/src/app/api/guilds/[guildId]/members/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/route.ts
  • web/src/types/discord.ts
  • web/src/hooks/use-guild-role.ts
  • src/api/routes/tickets.js
  • web/tests/components/layout/server-selector.test.tsx
  • src/api/routes/members.js
  • tests/api/routes/guilds.coverage.test.js
  • src/api/routes/guilds.js
  • web/src/lib/bot-api-proxy.ts
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation

Applied to files:

  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • src/api/routes/tickets.js
  • web/tests/api/guilds.test.ts
  • web/src/lib/bot-api-proxy.ts
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/ws/**/*.js : Enforce tenant scoping on WebSocket handlers: reject mismatched guildId in filters and require entry.guild_id === ws.guildId for broadcast matching

Applied to files:

  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
📚 Learning: 2026-03-22T01:55:35.217Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-22T01:55:35.217Z
Learning: Applies to **/*.{js,ts} : Use src/logger.js; do not use console.*

Applied to files:

  • src/utils/loadCommands.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/utils/cache.js : Use `src/utils/cache.js` for generic caching with Redis primary + in-memory fallback; use `src/utils/discordCache.js` for Discord API response caching (channels, roles, members); use `src/utils/reputationCache.js` for leaderboard and user reputation data; all caches auto-invalidate when config changes

Applied to files:

  • src/api/middleware/redisRateLimit.js
  • src/api/routes/members.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/api/routes/**/*.js : Create API route handlers in `src/api/routes/` and mount them in `src/api/server.js`; add auth middleware if the endpoint requires authentication

Applied to files:

  • src/api/routes/tickets.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Applied to files:

  • web/tests/lib/bot-api-proxy-branches.test.ts
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Applied to files:

  • tests/api/routes/guilds.coverage.test.js
📚 Learning: 2026-03-22T01:55:35.217Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-22T01:55:35.217Z
Learning: Applies to **/*.{js,ts} : Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls

Applied to files:

  • tests/api/routes/guilds.coverage.test.js
  • web/src/lib/discord.server.ts
  • src/api/routes/guilds.js
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{ts,tsx} : Web dashboard config editor should use category workspace navigation with reusable SettingsFeatureCard pattern (header + master toggle + Basic/Advanced blocks)

Applied to files:

  • web/src/components/layout/sidebar.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Always use `safeReply()`, `safeSend()`, or `safeEditReply()` instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Never use `console.*` methods; use the Winston logger instead via `import logger from '../logger.js'` (adjust path as needed), then call `logger.info()`, `logger.warn()`, `logger.error()`, or `logger.debug()`

Applied to files:

  • src/api/routes/guilds.js
🪛 GitHub Check: CodeQL
src/api/routes/tickets.js

[failure] 77-77: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 205-205: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 337-337: Missing rate limiting
This route handler performs authorization, but is not rate-limited.

src/api/routes/members.js

[failure] 89-89: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 299-299: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 299-475: Missing rate limiting
This route handler performs a database access, but is not rate-limited.
This route handler performs a database access, but is not rate-limited.
This route handler performs a database access, but is not rate-limited.


[failure] 608-608: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 806-806: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 937-937: Missing rate limiting
This route handler performs authorization, but is not rate-limited.

🪛 GitHub Check: SonarCloud Code Analysis
web/tests/components/layout/sidebar.test.tsx

[warning] 19-19: Prefer globalThis over global.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0iZCtBoMfWd5jBtCJN&open=AZ0iZCtBoMfWd5jBtCJN&pullRequest=360

web/src/app/api/guilds/route.ts

[warning] 97-99: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0iZCoaoMfWd5jBtCJK&open=AZ0iZCoaoMfWd5jBtCJK&pullRequest=360

web/src/lib/bot-api-proxy.ts

[warning] 72-72: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0iZCsLoMfWd5jBtCJL&open=AZ0iZCsLoMfWd5jBtCJL&pullRequest=360

🔇 Additional comments (15)
src/utils/loadCommands.js (1)

3-3: Cross-platform ESM module loading fix looks good.

Using pathToFileURL(...).href for dynamic import is the right fix for Windows path compatibility while
keeping ESM semantics intact.

Also applies to: 35-35

tests/commands.test.js (1)

3-3: Test loader update is aligned with runtime loader behavior.

Switching test imports to file:// URLs keeps command loading consistent across platforms and avoids
path-resolution issues on Windows.

Also applies to: 24-24

migrations/015_perf_indexes.cjs (1)

1-2: LGTM!

Comment correctly updated to reflect the new migration slot (015). The renumbering aligns with preserving the historical 014 slot for the audit_logs backfill migration.

migrations/014_audit_logs_user_tag_backfill.cjs (1)

1-28: LGTM! Well-designed repair migration with proper idempotency guards.

The use of IF EXISTS / IF NOT EXISTS guards ensures safe execution regardless of the database's prior migration state. The documentation clearly explains the schema drift scenario being addressed.

web/src/app/api/moderation/user/[userId]/history/route.ts (1)

4-29: Moderator authorization swap is correct and clean.

The import and auth guard update are consistent, and the control flow remains correct.

web/src/app/api/moderation/cases/[id]/route.ts (1)

4-29: Authorization change looks good here.

Switching this read endpoint to authorizeGuildModerator is implemented correctly.

web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts (1)

4-24: This moderator-gated ticket read path is implemented correctly.

The updated guard is clean and preserves early-return auth handling.

web/src/app/api/guilds/[guildId]/tickets/stats/route.ts (1)

4-24: Permission guard update is correct.

authorizeGuildModerator is applied consistently with the existing early-return auth pattern.

src/api/middleware/redisRateLimit.js (1)

11-11: LGTM!

The trusted internal request bypass is correctly placed before any Redis operations, making it efficient. This aligns with the in-memory rate limiter implementation and ensures dashboard-to-bot API calls aren't unnecessarily rate-limited.

Also applies to: 28-30

web/src/app/api/guilds/[guildId]/tickets/route.ts (1)

4-4: LGTM!

The authorization downgrade from admin to moderator for the tickets endpoint aligns with the PR objective to grant moderators access to the Tickets section. The authorization flow remains correct.

Also applies to: 23-24

web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts (1)

4-4: LGTM!

The authorization change to moderator level for XP adjustments is appropriate since this is a moderation action. The thorough input validation and payload sanitization before proxying upstream are well-implemented.

Also applies to: 38-39

src/api/middleware/rateLimit.js (1)

6-7: LGTM!

The trusted internal request bypass is consistent with the Redis rate limiter implementation. Placing it before any rate limit accounting is efficient and ensures both rate limiters behave uniformly for internal requests.

Also applies to: 61-64

src/api/routes/members.js (2)

17-17: LGTM! Static analysis false positive.

The authorization change to requireGuildModerator aligns with the PR objectives. The CodeQL "Missing rate limiting" warnings are false positives — membersRateLimit middleware is correctly applied before authorization in the middleware chain on all routes (e.g., line 88 shows the order: membersRateLimit, then requireGuildModerator, then validateGuild).

Also applies to: 86-90


299-299: LGTM!

The remaining authorization changes from requireGuildAdmin to requireGuildModerator are consistent and appropriate for the member management routes. All routes maintain the correct middleware order with rate limiting applied first.

Also applies to: 605-609, 803-807, 934-938

src/api/routes/tickets.js (1)

12-12: LGTM! Static analysis false positive.

The authorization changes to requireGuildModerator for all ticket routes align with the PR objective to grant moderators access to the Tickets section. The CodeQL "Missing rate limiting" warnings are false positives — ticketRateLimit middleware is correctly applied before authorization in the middleware chain (e.g., lines 76, 204, 337 all show rate limiting applied first).

Also applies to: 74-78, 202-206, 337-337

Comment on lines +108 to +113
const [guilds, setGuilds] = useState<MutualGuild[]>([]);
const isNavItemActive = (href: string) =>
pathname === href || (href !== '/dashboard' && pathname.startsWith(`${href}/`));
const hasActiveSecondaryItem = secondaryNav.some((item) => isNavItemActive(item.href));
const activeSecondaryHref = secondaryNav.find((item) => isNavItemActive(item.href))?.href ?? null;
const activeGuildAccess = guilds.find((guild) => guild.id === guildId)?.access ?? null;
const visiblePrimaryNav = activeGuildAccess === 'moderator' ? moderatorPrimaryNav : primaryNav;
const visibleSecondaryNav = activeGuildAccess === 'moderator' ? [] : secondaryNav;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't default moderators to the full nav while access is unresolved.

guilds starts empty and the error path leaves it empty, so activeGuildAccess stays null until /api/guilds succeeds. That makes Lines 112-113 render the full nav on first paint and on any 401/timeout/parse failure, which undercuts the moderator-only dashboard restriction this change is trying to add.

Also applies to: 125-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/sidebar.tsx` around lines 108 - 113, The current
logic uses activeGuildAccess (derived from guilds) and defaults to showing
primaryNav when access is null, so moderators see the full nav on first paint or
on fetch errors; change the fallback to a restrictive default until access is
known: update the expressions that set visiblePrimaryNav and visibleSecondaryNav
so they only show primaryNav when activeGuildAccess === 'moderator' and
otherwise hide primary (e.g., visiblePrimaryNav = activeGuildAccess ===
'moderator' ? moderatorPrimaryNav : []) while keeping visibleSecondaryNav =
activeGuildAccess === 'moderator' ? [] : secondaryNav, and ensure references to
guilds and activeGuildAccess remain unchanged elsewhere.

@github-project-automation github-project-automation bot moved this from Backlog to In Review in Volvox.Bot Mar 25, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/routes/guilds.js`:
- Around line 532-582: In the router.get('/access') handler, change the response
for missing API secret authentication by replacing the status code returned when
req.authMethod !== 'api-secret' from 403 to 401 (i.e., in the early return that
currently sends res.status(403).json(...)); keep the error message but use 401
Unauthorized to match the test expectation for missing authentication in this
endpoint.

In `@web/src/app/api/guilds/route.ts`:
- Around line 97-99: The nested ternary used to derive userId (using token.id
and token.sub) is hard to read; replace it with a small, explicit conditional or
helper to compute userId (e.g., a getUserId function or an if/else block) so the
logic is clearer, then pass that userId into applyAccessLevels(guilds, userId,
signal) as before; update the code around the userId declaration and the call to
applyAccessLevels to use the new clearer variable assignment.

In `@web/src/lib/bot-api-proxy.ts`:
- Around line 86-87: The nested ternary assigning userId from token is hard to
read; replace it by extracting the logic into a small clear block or helper:
compute userId by first checking typeof token.id === 'string' then falling back
to typeof token.sub === 'string' else ''. Update the assignment in
web/src/lib/bot-api-proxy.ts (variable userId) to use an if/else or a helper
function (e.g., getUserIdFromToken) to make the intent explicit and remove the
nested ternary.

In `@web/tests/hooks/use-guild-role.test.ts`:
- Around line 31-33: The test description for the case using
getGuildDashboardRole(createGuild({ permissions: '32' })) is inconsistent with
its assertion; either update the test title or the expected value. Change the
it(...) string from "returns admin for manage guild permissions" to "returns
moderator for manage guild permissions" to match the
expect(...).toBe('moderator') assertion (or, if the intended behavior is to
return 'admin', change the expected value to 'admin' instead); reference
getGuildDashboardRole and createGuild in the test when making the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 01acf502-1a2f-4c10-8088-538f7f03755c

📥 Commits

Reviewing files that changed from the base of the PR and between e6f55ce and 9af0a96.

📒 Files selected for processing (10)
  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
  • web/src/app/api/guilds/route.ts
  • web/src/hooks/use-guild-role.ts
  • web/src/lib/bot-api-proxy.ts
  • web/src/lib/discord.server.ts
  • web/tests/api/guilds.test.ts
  • web/tests/hooks/use-guild-role.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Docker Build Validation
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use console.* methods in web dashboard code; use appropriate logging mechanisms for React applications

Files:

  • web/src/hooks/use-guild-role.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
**/*.{js,mjs,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only

Files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts}: Use src/logger.js; do not use console.*
Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls
Use parameterized SQL only
Community features should be gated behind config..enabled. Moderation commands are the exception

Files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
web/**/*.{css,scss,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

web/**/*.{css,scss,ts,tsx}: Check both light and dark themes if colors or theming changed
Check responsive behavior if layout changed. Verify on mobile, tablet, and desktop

Files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
web/**/*.{ts,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Follow the design system when making changes to the UI/UX. If you don't follow the design system, your changes will be rejected

Files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
{web/**/*.{ts,tsx},**/DESIGN.md}

📄 CodeRabbit inference engine (AGENTS.md)

Always update the design system when making changes to the UI/UX

Files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/discord.server.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
web/tests/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

web/tests/**/*.test.{ts,tsx}: Write web dashboard tests using Vitest 4 with the jsdom environment and React Testing Library, matching the web/src/ structure
Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Files:

  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/tests/lib/discord.test.ts
  • web/tests/hooks/use-guild-role.test.ts
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
src/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.js: Never use console.* methods; use the Winston logger instead via import logger from '../logger.js' (adjust path as needed), then call logger.info(), logger.warn(), logger.error(), or logger.debug()
Always use safeReply(), safeSend(), or safeEditReply() instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Files:

  • src/api/routes/guilds.js
src/api/routes/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create API route handlers in src/api/routes/ and mount them in src/api/server.js; add auth middleware if the endpoint requires authentication

Files:

  • src/api/routes/guilds.js
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/api/routes/guilds.coverage.test.js
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources

Applied to files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/reactionrole.js : Enforce invoker role hierarchy check in /reactionrole add command to prevent non-owner users from configuring roles at or above their highest role

Applied to files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/hooks/use-guild-role.test.ts
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id

Applied to files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/bot-api-proxy.ts
  • web/tests/hooks/use-guild-role.test.ts
  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/app/api/**/route.ts : Include guildId in signed WebSocket ticket payload when issuing tickets from dashboard endpoints

Applied to files:

  • web/src/hooks/use-guild-role.ts
  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/bot-api-proxy.ts
  • src/api/routes/guilds.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation

Applied to files:

  • web/tests/api/guilds.test.ts
  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/src/app/api/guilds/route.ts
  • web/src/lib/bot-api-proxy.ts
  • src/api/routes/guilds.js
📚 Learning: 2026-03-11T17:18:17.626Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T17:18:17.626Z
Learning: Applies to src/**/{startup,command-register,reload}*.{js,ts} : Remove process.env.GUILD_ID runtime reads from bot startup and reload command registration

Applied to files:

  • web/tests/api/guilds.test.ts
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Write bot tests using Vitest 4 with the `node` environment, matching the `src/` structure in the `tests/` directory

Applied to files:

  • web/tests/api/guilds.test.ts
  • tests/api/routes/guilds.coverage.test.js
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/ws/**/*.js : Enforce tenant scoping on WebSocket handlers: reject mismatched guildId in filters and require entry.guild_id === ws.guildId for broadcast matching

Applied to files:

  • web/tests/lib/bot-api-proxy-branches.test.ts
  • web/src/app/api/guilds/route.ts
  • src/api/routes/guilds.js
📚 Learning: 2026-03-22T01:55:35.229Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-22T01:55:35.229Z
Learning: Applies to **/*.{js,ts} : Community features should be gated behind config.<feature>.enabled. Moderation commands are the exception

Applied to files:

  • web/src/app/api/guilds/route.ts
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Always use `safeReply()`, `safeSend()`, or `safeEditReply()` instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-03-22T01:55:35.229Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-22T01:55:35.229Z
Learning: Applies to **/*.{js,ts} : Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls

Applied to files:

  • src/api/routes/guilds.js
  • tests/api/routes/guilds.coverage.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Never use `console.*` methods; use the Winston logger instead via `import logger from '../logger.js'` (adjust path as needed), then call `logger.info()`, `logger.warn()`, `logger.error()`, or `logger.debug()`

Applied to files:

  • src/api/routes/guilds.js
🪛 GitHub Check: SonarCloud Code Analysis
web/src/app/api/guilds/route.ts

[warning] 98-98: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nLibevx0stJtcbBHj&open=AZ0nLibevx0stJtcbBHj&pullRequest=360

web/src/lib/bot-api-proxy.ts

[warning] 87-87: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0iZCsLoMfWd5jBtCJL&open=AZ0iZCsLoMfWd5jBtCJL&pullRequest=360

🔇 Additional comments (30)
src/api/routes/guilds.js (5)

239-243: accessSatisfiesRequirement logic is correct but could be clearer.

The function correctly implements the access hierarchy where bot-owner satisfies all requirements. The implementation is accurate for the moderator/admin access model.


245-251: BigInt permission check looks correct.

The hasPermissionFlag function properly handles BigInt conversion with a try-catch for invalid inputs, returning false on failure. This is a safe approach.


264-278: Potential race condition in mapWithConcurrency worker index increment.

The index++ operation is not atomic in JavaScript's single-threaded event loop, but since workers are async functions that await between iterations, there's no actual race condition here—each worker grabs an index before yielding. The implementation is correct for Node.js's cooperative concurrency model.


287-320: getGuildAccessLevel implementation is solid.

The function correctly:

  • Checks bot owner status first
  • Tries cache before fetching
  • Only swallows "Unknown Member" errors (per previous review feedback)
  • Propagates other errors for proper 5xx handling
  • Falls back to viewer for non-members

488-505: GET /guilds filtering correctly uses the new access model.

The code properly derives access level from OAuth data first, then falls back to getGuildAccessLevel for bot-based role evaluation. Guilds with viewer access are correctly filtered out.

web/tests/hooks/use-guild-role.test.ts (1)

35-39: New test cases cover important edge cases.

The tests correctly verify:

  1. Explicit access field from API takes precedence over permission-derived access
  2. Guild ownership (owner: true) overrides a downgraded access value

This aligns with the past review feedback that was addressed.

Also applies to: 74-76

web/src/hooks/use-guild-role.ts (3)

29-33: Ownership precedence logic is now correct.

The past review feedback was addressed: guild.owner is now checked alongside guild.access === 'owner' at the top of the function, ensuring a real server owner is never locked out by a stale access: 'viewer' value.


42-48: MANAGE_GUILD now correctly maps to 'moderator'.

This change aligns with the PR objective to distinguish moderator access (MANAGE_GUILD) from admin access (ADMINISTRATOR), allowing the dashboard to limit moderator users to specific sections.


66-68: isGuildManageable uses explicit role comparison.

The explicit equality checks (role === 'owner' || role === 'admin' || role === 'moderator') are clearer than a negative check against 'viewer', and correctly handle any future role additions.

web/tests/lib/bot-api-proxy-branches.test.ts (3)

74-80: hasModeratorPermission tests correctly cover permission bitfields.

Tests verify:

  • '32' (MANAGE_GUILD) → true
  • '2' (KICK_MEMBERS) → true
  • '4' (BAN_MEMBERS) → true
  • '0' (no permissions) → false
  • 'garbage' (invalid input) → false

145-162: Moderator authorization test now correctly mocks both fetch calls.

The past review feedback was addressed: the test now chains two mockResolvedValueOnce calls so both authorizeGuildModerator and authorizeGuildAdmin receive the mocked moderator access response, ensuring the admin assertion actually tests the 403 path rather than an error fallback.


164-173: Unknown access fallback test is well-designed.

This test verifies that when the bot API returns an unrecognized access string ('super-admin'), the authorization falls back to OAuth-derived permissions (permissions: '8' = ADMINISTRATOR), correctly allowing admin access.

web/tests/lib/discord.test.ts (4)

128-138: Test correctly verifies no retry when delay cap exceeded.

The test uses retry-after: '728' (which exceeds the default max delay cap) and asserts only 1 fetch call is made, confirming the rate limiter correctly gives up early.


140-166: Budget exhaustion test has proper rateLimit configuration.

The test configures a 2-second total budget and a 1.5-second retry-after, correctly expecting only 2 fetch calls (initial + one retry) before the budget is exhausted.


267-290: Fallback header test covers important edge case.

When retry-after is malformed ('nope'), the test verifies the code falls back to x-ratelimit-reset-after and successfully retries.


497-517: Bot API fail-fast test now uses appropriate retry-after value.

The past review feedback was addressed: the test now uses retry-after: '1' which is within the generic cap but exercises the bot-specific fail-fast path (maxRetryDelayMs: 250, totalRetryBudgetMs: 500).

tests/api/routes/guilds.coverage.test.js (2)

121-146: Test properly restores mock state after member cache manipulation.

The test correctly saves originalCache and originalFetch, manipulates them for the test scenarios, and restores them in Lines 144-145. This prevents test pollution.


107-153: ⚠️ Potential issue | 🟡 Minor

Fix test assertion: expect 403, not 401 for missing API secret auth.

The test at lines 148-152 expects HTTP 401, but the /access endpoint implementation returns HTTP 403 when authMethod !== 'api-secret'. Update the assertion from expect(res.status).toBe(401) to expect(res.status).toBe(403).

⛔ Skipped due to learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id
web/tests/api/guilds.test.ts (3)

50-66: Proper teardown now restores all globals.

The past review feedback was addressed: afterEach now correctly restores globalThis.fetch, BOT_API_URL, BOT_API_SECRET, and NEXTAUTH_SECRET to their original values, preventing test pollution.


149-193: Access augmentation test verifies end-to-end behavior.

The test correctly:

  1. Sets up bot API credentials
  2. Mocks fetch to return access entries
  3. Verifies the response includes the augmented access field
  4. Asserts the correct endpoint URL and headers were used

195-234: Unknown access fallback test ensures resilience.

The test verifies that when the bot API returns an invalid access value ('super-admin'), the original guild access value ('viewer') is preserved rather than being overwritten with the unknown value.

web/src/app/api/guilds/route.ts (2)

57-70: Access validation correctly restricts to known enum values.

The past review feedback was addressed: Line 67 now validates that entry.access is in VALID_ACCESS_LEVELS before including it in the map, preventing schema drift from silently breaking the moderator nav restriction.


14-80: applyAccessLevels implementation is robust.

The function correctly:

  • Short-circuits when bot API is not configured or no guilds exist
  • Only queries for guilds where botPresent is true
  • Validates response shape (array check + typed filter)
  • Falls back gracefully on any error
  • Uses the request signal for timeout propagation
web/src/lib/discord.server.ts (2)

29-42: LGTM! The retry-after parsing issue has been addressed.

The parseSeconds helper correctly validates each header independently before falling through to the next, ensuring a malformed retry-after won't mask a valid x-ratelimit-reset-after.


49-98: LGTM! Well-structured retry budget tracking.

The configurable rate limit options with per-request overrides allow fine-grained control over retry behavior. The early-exit conditions at lines 69-74 properly prevent excessive waits when delay cap or budget would be exceeded.

web/src/lib/bot-api-proxy.ts (5)

156-167: LGTM! The shared timeout budget has been implemented correctly.

A single AbortController now governs the entire authorization path, ensuring the total auth check respects REQUEST_TIMEOUT_MS. The finally block properly cleans up the timeout.


117-126: LGTM! Access level validation now whitelists supported values.

The predicate at line 123 properly validates against GUILD_ACCESS_LEVELS, preventing unknown or typo'd access values from overriding the fallback.


43-55: LGTM! Moderator permission detection looks correct.

The permission bitfield checks align with Discord's permission model. The OR-based check appropriately grants moderator access to users with any of MANAGE_GUILD, KICK_MEMBERS, BAN_MEMBERS, or MODERATE_MEMBERS.


57-70: LGTM! Access hierarchy is well-structured.

The access satisfaction logic correctly places bot-owner and admin above moderator. Guild owners are appropriately granted admin-level access in the fallback path, aligning with Discord's permission model expectations. Based on learnings: "Guild owners should be explicitly exempted from role hierarchy restrictions."


188-202: LGTM! Clean authorization API for route handlers.

The exported functions provide clear semantics for admin vs moderator authorization. The delegation pattern keeps the implementation DRY while exposing an intuitive interface.

Comment on lines +97 to +99
const userId =
typeof token.id === 'string' ? token.id : typeof token.sub === 'string' ? token.sub : '';
const guildsWithAccess = userId ? await applyAccessLevels(guilds, userId, signal) : guilds;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extract nested ternary for readability.

The nested ternary on Line 98 was flagged by SonarCloud. While functionally correct, it's harder to read at a glance.

♻️ Suggested refactor
-    const userId =
-      typeof token.id === 'string' ? token.id : typeof token.sub === 'string' ? token.sub : '';
+    let userId = '';
+    if (typeof token.id === 'string') {
+      userId = token.id;
+    } else if (typeof token.sub === 'string') {
+      userId = token.sub;
+    }
     const guildsWithAccess = userId ? await applyAccessLevels(guilds, userId, signal) : guilds;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const userId =
typeof token.id === 'string' ? token.id : typeof token.sub === 'string' ? token.sub : '';
const guildsWithAccess = userId ? await applyAccessLevels(guilds, userId, signal) : guilds;
let userId = '';
if (typeof token.id === 'string') {
userId = token.id;
} else if (typeof token.sub === 'string') {
userId = token.sub;
}
const guildsWithAccess = userId ? await applyAccessLevels(guilds, userId, signal) : guilds;
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 98-98: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nLibevx0stJtcbBHj&open=AZ0nLibevx0stJtcbBHj&pullRequest=360

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/guilds/route.ts` around lines 97 - 99, The nested ternary
used to derive userId (using token.id and token.sub) is hard to read; replace it
with a small, explicit conditional or helper to compute userId (e.g., a
getUserId function or an if/else block) so the logic is clearer, then pass that
userId into applyAccessLevels(guilds, userId, signal) as before; update the code
around the userId declaration and the call to applyAccessLevels to use the new
clearer variable assignment.

# Conflicts:
#	migrations/015_perf_indexes.cjs
Copilot AI review requested due to automatic review settings March 25, 2026 22:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/api/routes/guilds.js:357

  • When guild is present in the client cache, the middleware always calls getGuildAccessLevel() (which may do guild.members.fetch) and never uses the OAuth permission-bit path (permissionCheck, which is backed by the cached /users/@me/guilds call). This can increase Discord API load on hot endpoints. Consider checking permissionCheck first (fast path) and only falling back to getGuildAccessLevel() when permission bits aren’t sufficient but configured roles might still grant access.
      try {
        const guild = req.app.locals.client?.guilds?.cache?.get(req.params.id);
        if (guild) {
          const access = await getGuildAccessLevel(guild, req.user.userId);
          if (!accessSatisfiesRequirement(access, requiredAccess)) {
            return res.status(403).json({ error: errorMessage });
          }
          return next();
        }

        if (!(await permissionCheck(req.user, req.params.id))) {
          return res.status(403).json({ error: errorMessage });
        }
        return next();
      } catch (err) {

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 26, 2026

⚠️ No Changeset found

Latest commit: b93c0ad

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/routes/members.js`:
- Around line 928-932: Update the comment that currently reads "Admin XP
adjustment" above the XP route to match the actual authorization used by the
route handler: change it to "Moderator XP adjustment" so it aligns with the
middleware and proxy (the router.post route for '/:id/members/:userId/xp' which
uses requireGuildModerator and the corresponding authorizeGuildModerator in the
web proxy). Ensure the comment text is exactly updated and no other code is
changed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 28d1c7c6-8c93-46c3-b06c-04cb9df32bcb

📥 Commits

Reviewing files that changed from the base of the PR and between 9af0a96 and d48d8b7.

📒 Files selected for processing (2)
  • src/api/routes/guilds.js
  • src/api/routes/members.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Greptile Review
  • GitHub Check: E2E Tests (1/2)
  • GitHub Check: E2E Tests (2/2)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
src/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.js: Never use console.* methods; use the Winston logger instead via import logger from '../logger.js' (adjust path as needed), then call logger.info(), logger.warn(), logger.error(), or logger.debug()
Always use safeReply(), safeSend(), or safeEditReply() instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
src/api/routes/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create API route handlers in src/api/routes/ and mount them in src/api/server.js; add auth middleware if the endpoint requires authentication

Files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: ESM only - do not use CommonJS modules
Use src/logger.js; do not use console.*

Files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
src/**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{js,ts}: Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls
Community features should be gated behind config..enabled. Moderation commands are the exception.

Files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use parameterized SQL only; do not use string concatenation for SQL queries

Files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources

Applied to files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id

Applied to files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/app/api/**/route.ts : Include guildId in signed WebSocket ticket payload when issuing tickets from dashboard endpoints

Applied to files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/api/routes/**/*.js : Create API route handlers in `src/api/routes/` and mount them in `src/api/server.js`; add auth middleware if the endpoint requires authentication

Applied to files:

  • src/api/routes/members.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • src/api/routes/members.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/utils/cache.js : Use `src/utils/cache.js` for generic caching with Redis primary + in-memory fallback; use `src/utils/discordCache.js` for Discord API response caching (channels, roles, members); use `src/utils/reputationCache.js` for leaderboard and user reputation data; all caches auto-invalidate when config changes

Applied to files:

  • src/api/routes/members.js
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/ws/**/*.js : Enforce tenant scoping on WebSocket handlers: reject mismatched guildId in filters and require entry.guild_id === ws.guildId for broadcast matching

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Always use `safeReply()`, `safeSend()`, or `safeEditReply()` instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-03-26T00:04:14.673Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.673Z
Learning: Applies to src/**/*.{js,ts} : Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Never use `console.*` methods; use the Winston logger instead via `import logger from '../logger.js'` (adjust path as needed), then call `logger.info()`, `logger.warn()`, `logger.error()`, or `logger.debug()`

Applied to files:

  • src/api/routes/guilds.js
🪛 GitHub Check: CodeQL
src/api/routes/members.js

[failure] 77-77: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 289-289: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 289-468: Missing rate limiting
This route handler performs a database access, but is not rate-limited.
This route handler performs a database access, but is not rate-limited.
This route handler performs a database access, but is not rate-limited.


[failure] 601-601: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 800-800: Missing rate limiting
This route handler performs authorization, but is not rate-limited.


[failure] 931-931: Missing rate limiting
This route handler performs authorization, but is not rate-limited.

🔇 Additional comments (12)
src/api/routes/guilds.js (7)

533-537: Return 401 instead of 403 for missing API secret authentication.

The endpoint returns 403 when req.authMethod !== 'api-secret', but this should be 401 (Unauthorized) since the request lacks the required authentication method. Reserve 403 for cases where authentication succeeded but authorization failed. The test at tests/api/routes/guilds.coverage.test.js also expects 401.

🔧 Proposed fix
   if (req.authMethod !== 'api-secret') {
     return res
-      .status(403)
+      .status(401)
       .json({ error: 'Guild access endpoint requires API secret authentication' });
   }

239-258: LGTM! Well-structured access level helpers.

The helper functions are clean and correctly implement the permission hierarchy:

  • accessSatisfiesRequirement properly treats bot-owner as having all permissions
  • hasPermissionFlag safely handles BigInt conversion with a try/catch
  • getOAuthDerivedAccessLevel derives access from OAuth permissions with appropriate fallback

260-278: LGTM! Correct concurrent mapping implementation.

The mapWithConcurrency helper correctly implements a bounded-concurrency worker pattern, and isUnknownMemberError properly identifies Discord's "Unknown Member" error (code 10007) for the member fetch fallback logic.


287-320: LGTM! Robust access level resolution.

The function correctly implements the permission hierarchy and properly handles Discord member fetch errors by only treating "Unknown Member" as a non-member (returning viewer), while re-throwing transient errors to surface them as 502s upstream.


336-387: LGTM! Well-layered permission middleware.

The updated middleware properly:

  1. Bypasses checks for API-secret and bot owners
  2. Uses getGuildAccessLevel for cached guilds with config-aware role evaluation
  3. Falls back to OAuth permission check when guild isn't cached
  4. Returns 502 (not 403) on transient Discord errors

488-505: LGTM! Proper access-level filtering with fallback.

The filtering correctly derives access from OAuth permissions first (getOAuthDerivedAccessLevel), falling back to getGuildAccessLevel only when needed. This minimizes Discord API calls while still honoring configured moderator/admin roles.


560-582: LGTM! Bounded-concurrency access resolution.

The /access endpoint correctly:

  • Normalizes and deduplicates guild IDs
  • Uses mapWithConcurrency to limit parallel Discord lookups
  • Filters out null entries (guilds not in cache)
  • Returns 502 on transient Discord errors
src/api/routes/members.js (5)

15-15: LGTM! Authorization import aligns with PR objectives.

The switch from requireGuildAdmin to requireGuildModerator correctly enables moderator-level access to the Members dashboard section, consistent with the web proxy layer changes.


74-78: LGTM! Rate limiting is present.

The static analysis hint about "Missing rate limiting" is a false positive—membersRateLimit is correctly applied at line 76 before requireGuildModerator at line 77.


289-289: LGTM! Authorization and rate limiting are correctly chained.

The static analysis warnings are false positives. The middleware chain correctly applies:

  1. membersRateLimit — rate limiting
  2. requireGuildModerator — authorization
  3. validateGuild — guild validation

598-602: LGTM! Member detail endpoint correctly gated.

Rate limiting (membersRateLimit) and authorization (requireGuildModerator) are both applied, matching the PR's intent to allow moderator access to member details.


797-801: LGTM! Case history endpoint correctly gated.

Moderators can now view member moderation history, which aligns with their role in handling moderation tasks. Rate limiting is properly applied.

@BillChirico BillChirico added scope: api REST/API routes and contracts scope: backend Bot/runtime/backend logic scope: dashboard Web dashboard scope: security Security vulnerability or hardening size:XL type: feature New feature labels Mar 27, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
web/src/components/layout/guild-directory-context.tsx (2)

51-54: 🧹 Nitpick | 🔵 Trivial

Use TypeError for invalid data shape.

When the response data shape doesn't match expectations, TypeError is more semantically appropriate than generic Error.

✏️ Suggested fix
       const data: unknown = await response.json();
       if (!Array.isArray(data)) {
-        throw new Error('Invalid guild response');
+        throw new TypeError('Invalid guild response');
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/guild-directory-context.tsx` around lines 51 - 54,
Replace the generic Error thrown when the fetched response shape is wrong with a
TypeError to reflect an invalid data type; in the block where you parse
response.json() into the local variable data and check Array.isArray(data),
throw new TypeError('Invalid guild response') (or equivalent) instead of
Error('Invalid guild response') so the error type correctly represents a
type/shape violation.

27-27: 🧹 Nitpick | 🔵 Trivial

Mark the props of the component as read-only.

Per React best practices and static analysis hints, the props should be marked as read-only.

✏️ Suggested fix
-export function GuildDirectoryProvider({ children }: { children: React.ReactNode }) {
+export function GuildDirectoryProvider({ children }: Readonly<{ children: React.ReactNode }>) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/guild-directory-context.tsx` at line 27, The
GuildDirectoryProvider component's props should be marked read-only; update the
function signature for GuildDirectoryProvider to use a readonly prop type (e.g.,
Readonly<{ children: React.ReactNode }> or { readonly children: React.ReactNode
}) so the incoming props cannot be mutated; locate the GuildDirectoryProvider
declaration and change its parameter type accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/api/middleware/auth.test.js`:
- Around line 139-150: Add two unit tests for requireAuth middleware covering
edge cases when x-api-secret is valid but x-discord-user-id is missing or blank:
(1) stub BOT_API_SECRET and set req.headers['x-api-secret'] to the secret but do
NOT set req.headers['x-discord-user-id'], then call middleware and assert next()
was called, req.authMethod === 'api-secret' and req.user is undefined; (2) same
setup but set req.headers['x-discord-user-id'] = '   ' (whitespace), call
middleware and assert next() was called, req.authMethod === 'api-secret' and
req.user is undefined to ensure trimming/empty-check logic in requireAuth is
exercised.

In `@tests/api/routes/guilds.coverage.test.js`:
- Line 258: The assertion in the test that checks the POST /:id/actions response
currently expects 'admin access' but the endpoint actually returns 'API secret';
update the assertion in tests/api/routes/guilds.coverage.test.js (the
expect(res.body.error).toContain(...) call) to look for 'API secret' instead so
it matches the error text produced by the route handler (see the error string in
the guilds route) and aligns with the other test that expects 'API secret'.

In `@web/src/app/api/guilds/`[guildId]/members/[userId]/xp/route.ts:
- Around line 106-119: The code currently falls back from token.id to token.sub
when building the x-discord-user-id header; remove that fallback and require
token.id only: use getToken(...) and if token?.id is not a string return a 401
response instead of calling proxyToBotApi, and when present send only token.id
in the 'x-discord-user-id' header; update the route handler around requesterId
(and its usage in the headers) to enforce this, and add a regression test that
POST to this route returns 401 when getToken() yields no id (mock getToken to
return undefined/id missing) to ensure we never proxy with token.sub.

---

Duplicate comments:
In `@web/src/components/layout/guild-directory-context.tsx`:
- Around line 51-54: Replace the generic Error thrown when the fetched response
shape is wrong with a TypeError to reflect an invalid data type; in the block
where you parse response.json() into the local variable data and check
Array.isArray(data), throw new TypeError('Invalid guild response') (or
equivalent) instead of Error('Invalid guild response') so the error type
correctly represents a type/shape violation.
- Line 27: The GuildDirectoryProvider component's props should be marked
read-only; update the function signature for GuildDirectoryProvider to use a
readonly prop type (e.g., Readonly<{ children: React.ReactNode }> or { readonly
children: React.ReactNode }) so the incoming props cannot be mutated; locate the
GuildDirectoryProvider declaration and change its parameter type accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3eef2bbf-fd88-4850-8cfc-c91301ccf179

📥 Commits

Reviewing files that changed from the base of the PR and between 05ce563 and cb77c42.

📒 Files selected for processing (8)
  • src/api/middleware/auth.js
  • src/api/routes/guilds.js
  • tests/api/middleware/auth.test.js
  • tests/api/routes/guilds.coverage.test.js
  • tests/api/routes/members.test.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/components/layout/guild-directory-context.tsx
  • web/tests/api/xp-route.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
  • GitHub Check: E2E Tests (1/2)
  • GitHub Check: E2E Tests (2/2)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • tests/api/middleware/auth.test.js
  • src/api/middleware/auth.js
  • tests/api/routes/members.test.js
  • tests/api/routes/guilds.coverage.test.js
  • src/api/routes/guilds.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • tests/api/middleware/auth.test.js
  • src/api/middleware/auth.js
  • tests/api/routes/members.test.js
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.coverage.test.js
  • web/src/components/layout/guild-directory-context.tsx
  • src/api/routes/guilds.js
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/api/middleware/auth.test.js
  • tests/api/routes/members.test.js
  • tests/api/routes/guilds.coverage.test.js
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: ESM only - do not use CommonJS modules
Use src/logger.js; do not use console.*

Files:

  • tests/api/middleware/auth.test.js
  • src/api/middleware/auth.js
  • tests/api/routes/members.test.js
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.coverage.test.js
  • web/src/components/layout/guild-directory-context.tsx
  • src/api/routes/guilds.js
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use parameterized SQL only; do not use string concatenation for SQL queries

Files:

  • tests/api/middleware/auth.test.js
  • src/api/middleware/auth.js
  • tests/api/routes/members.test.js
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.coverage.test.js
  • src/api/routes/guilds.js
src/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.js: Never use console.* methods; use the Winston logger instead via import logger from '../logger.js' (adjust path as needed), then call logger.info(), logger.warn(), logger.error(), or logger.debug()
Always use safeReply(), safeSend(), or safeEditReply() instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Files:

  • src/api/middleware/auth.js
  • src/api/routes/guilds.js
src/**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{js,ts}: Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls
Community features should be gated behind config..enabled. Moderation commands are the exception.

Files:

  • src/api/middleware/auth.js
  • src/api/routes/guilds.js
web/tests/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

web/tests/**/*.test.{ts,tsx}: Write web dashboard tests using Vitest 4 with the jsdom environment and React Testing Library, matching the web/src/ structure
Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Files:

  • web/tests/api/xp-route.test.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use console.* methods in web dashboard code; use appropriate logging mechanisms for React applications

Files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/components/layout/guild-directory-context.tsx
src/api/routes/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create API route handlers in src/api/routes/ and mount them in src/api/server.js; add auth middleware if the endpoint requires authentication

Files:

  • src/api/routes/guilds.js
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation

Applied to files:

  • src/api/middleware/auth.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources

Applied to files:

  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.coverage.test.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/app/api/**/route.ts : Include guildId in signed WebSocket ticket payload when issuing tickets from dashboard endpoints

Applied to files:

  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • src/api/routes/guilds.js
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id

Applied to files:

  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.coverage.test.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.coverage.test.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-26T00:04:14.673Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.673Z
Learning: Applies to src/**/*.{js,ts} : Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls

Applied to files:

  • tests/api/routes/guilds.coverage.test.js
  • src/api/routes/guilds.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Write bot tests using Vitest 4 with the `node` environment, matching the `src/` structure in the `tests/` directory

Applied to files:

  • tests/api/routes/guilds.coverage.test.js
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/ws/**/*.js : Enforce tenant scoping on WebSocket handlers: reject mismatched guildId in filters and require entry.guild_id === ws.guildId for broadcast matching

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Always use `safeReply()`, `safeSend()`, or `safeEditReply()` instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Never use `console.*` methods; use the Winston logger instead via `import logger from '../logger.js'` (adjust path as needed), then call `logger.info()`, `logger.warn()`, `logger.error()`, or `logger.debug()`

Applied to files:

  • src/api/routes/guilds.js
🪛 GitHub Check: SonarCloud Code Analysis
web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts

[warning] 110-112: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ1HBGyjxuRvoptrX6hR&open=AZ1HBGyjxuRvoptrX6hR&pullRequest=360

web/src/components/layout/guild-directory-context.tsx

[warning] 53-53: new Error() is too unspecific for a type check. Use new TypeError() instead.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ1GxbwDkZ0EE30SsY1Q&open=AZ1GxbwDkZ0EE30SsY1Q&pullRequest=360


[warning] 44-44: Prefer globalThis over window.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ1GxbwDkZ0EE30SsY1P&open=AZ1GxbwDkZ0EE30SsY1P&pullRequest=360


[warning] 27-27: Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ1GxbwDkZ0EE30SsY1O&open=AZ1GxbwDkZ0EE30SsY1O&pullRequest=360

🔇 Additional comments (10)
src/api/middleware/auth.js (1)

51-57: LGTM!

The trusted user identity propagation is correctly implemented:

  • Header is only read after API-secret validation succeeds
  • Value is properly trimmed and only assigned if non-empty
  • The req.user shape { userId } aligns with OAuth JWT auth path
tests/api/routes/members.test.js (1)

511-535: LGTM!

The test properly validates moderator attribution logging for XP adjustments. The use of expect.objectContaining provides appropriate flexibility while ensuring the key assertion.

tests/api/routes/guilds.coverage.test.js (1)

107-164: LGTM!

The new /access endpoint tests provide good coverage:

  • Moderator access resolution via mocked isModerator
  • Graceful handling of Unknown Member errors (→ viewer) vs transient Discord failures (→ 502)
  • Auth enforcement (401 for non-api-secret)
  • Batch size cap validation

The save/restore pattern for mockGuild.members.cache and mockGuild.members.fetch in lines 122-145 is clean.

web/src/components/layout/guild-directory-context.tsx (1)

33-67: LGTM on the fetch/abort logic.

The refreshGuilds implementation correctly:

  • Aborts previous in-flight requests before starting a new one
  • Uses AbortController signal for cancellation
  • Handles abort errors gracefully (returns early, doesn't set error state)
  • Only clears loading state if this request is still the current one (line 63 check)
src/api/routes/guilds.js (5)

240-244: LGTM!

The access hierarchy logic is correct: bot-owner supersedes all, admin is required for admin-gated routes, and both admin and moderator satisfy moderator requirements.


265-279: LGTM!

The mapWithConcurrency implementation is correct. The shared index variable is safe in Node.js's single-threaded event loop since JS-level increments are atomic between await points.


288-321: LGTM!

The getGuildAccessLevel function correctly:

  • Prioritizes bot owner check
  • Uses cache-first with API fallback for member lookup
  • Only swallows "Unknown Member" errors, re-throws transient failures (addresses past review)
  • Falls through to role-based checks via isAdmin/isModerator

340-377: LGTM!

The updated requireGuildPermission middleware correctly gates OAuth requests using the new requiredAccess parameter and accessSatisfiesRequirement check, while preserving the API-secret and bot-owner bypass paths.


539-594: LGTM!

The /access endpoint correctly:

  • Enforces API-secret authentication with 401 (addresses past review)
  • Validates and deduplicates guildIds
  • Caps batch size at 100 (addresses past review)
  • Uses concurrency-limited resolution
  • Returns 502 for Discord API failures
web/tests/api/xp-route.test.ts (1)

180-188: Good regression coverage for requester attribution.

This locks in the x-discord-user-id forwarding contract the bot API now depends on for moderator-side re-authorization.


expect(res.status).toBe(403);
expect(res.body.error).toContain('API secret');
expect(res.body.error).toContain('admin access');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the actual error message returned by POST /:id/actions for OAuth (non-api-secret) auth
rg -n "Actions endpoint requires" src/api/routes/guilds.js -A 1 -B 1

# Also check both test files for the 403 assertion on POST /:id/actions
echo "--- guilds.coverage.test.js assertion ---"
rg -n "expect.*error.*toContain" tests/api/routes/guilds.coverage.test.js | grep -i "403\|actions\|admin\|api.secret" | head -5

echo "--- guilds.test.js assertion ---"
rg -n "expect.*error.*toContain.*API secret" tests/api/routes/guilds.test.js | head -5

Repository: VolvoxLLC/volvox-bot

Length of output: 416


Fix inconsistent error message assertion in guilds.coverage.test.js.

The test at line 258 expects 'admin access' but should expect 'API secret' to match the actual error message returned by the endpoint (line 1832 in src/api/routes/guilds.js). This test covers the same POST /:id/actions scenario as tests/api/routes/guilds.test.js at line 1340, which correctly expects 'API secret'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/api/routes/guilds.coverage.test.js` at line 258, The assertion in the
test that checks the POST /:id/actions response currently expects 'admin access'
but the endpoint actually returns 'API secret'; update the assertion in
tests/api/routes/guilds.coverage.test.js (the
expect(res.body.error).toContain(...) call) to look for 'API secret' instead so
it matches the error text produced by the route handler (see the error string in
the guilds route) and aligns with the other test that expects 'API secret'.

Copilot AI review requested due to automatic review settings April 2, 2026 02:17
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/tests/components/dashboard/config-editor-autosave.test.tsx (1)

141-166: ⚠️ Potential issue | 🟡 Minor

Add the inverse assertion so these tests actually prove route gating.

ConfigEditor mounts both <AiAutomationCategory /> and <OnboardingGrowthCategory />. With only positive assertions here, both tests still pass if both sections render on every route. Please also assert that the non-selected category is hidden on each pathname so the pathname-based filtering is actually covered.

Also applies to: 168-194

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/tests/components/dashboard/config-editor-autosave.test.tsx` around lines
141 - 166, The test only asserts presence of the selected category but not
absence of the other, so add inverse assertions to verify route gating: in the
'renders AI settings on the AI category route' test (using ConfigEditor) assert
that the non-selected category component (e.g., text unique to
OnboardingGrowthCategory like "Onboarding" or whatever its visible label is) is
not present (use queryByText and expect(...).toBeNull() or
expect(...).not.toBeInTheDocument()); and make the analogous change in the other
test (the one covering the Onboarding/Growth route) to assert
AiAutomationCategory-specific text (e.g., "AI Chat") is not present. Reference
the ConfigEditor render and the AiAutomationCategory / OnboardingGrowthCategory
visible labels when adding these negative assertions so the tests actually
verify pathname-based filtering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/api/routes/guilds.test.js`:
- Around line 369-384: The test mutates shared mocks mockGuild.members.cache and
mockGuild.members.fetch and restores them only after assertions, which can leak
state if an assertion fails; wrap the mutation and assertions in a try/finally
block (capture originalCache and originalFetch, assign the new values, run the
request/assertions inside try, and restore originalCache/originalFetch in the
finally) for this test and the other similar test that mutates the same mocks to
ensure cleanup always runs.

In `@tests/modules/cli-process.test.js`:
- Around line 650-657: The test currently converts sendP into a resolved error
value which weakens the assertion; instead attach a noop catch to
cli.send('slow') to suppress an early unhandled-rejection, keep sendP as the
original Promise, advance timers as before, and then assert on sendP itself that
it rejects with an instance of CLIProcessError and that the rejection message
contains 'timed out after 3000ms' (use the existing sendP, cli.send, and
CLIProcessError identifiers to locate and update the assertions).

In `@web/src/components/layout/guild-directory-context.tsx`:
- Around line 71-79: The abort cleanup leaves abortControllerRef.current
pointing at the aborted controller so refreshGuilds()'s finally can still call
setLoading(false) after unmount; update the useEffect cleanup to both abort and
clear the ref (set abortControllerRef.current = undefined/null) so the equality
check in refreshGuilds (abortControllerRef.current === controller) will fail
after unmount; ensure the change is applied around the useEffect that calls
refreshGuilds and references abortControllerRef, so no other logic needs
changing.
- Around line 23-32: The runtime guard is missing validation for the optional
MutualGuild.access field, so invalid values like { access: 'bogus' } pass;
update isMutualGuild to, when an access property exists, ensure typeof access
=== 'string' and that its value matches the exact closed set of literals defined
for MutualGuild.access (use the same union literal values used in the
MutualGuild type/enum), returning false for any other types or unexpected
strings; keep the rest of the existing checks (id, name, permissions, owner,
botPresent) intact.

In `@web/tests/api/xp-route.test.ts`:
- Around line 56-60: The test uses mockGetToken.mockResolvedValue({ id:
'moderator-1' }) which is not a valid Discord snowflake and fails the new auth
check; change the mocked token id(s) in xp-route.test.ts (both occurrences where
mockGetToken.mockResolvedValue is called) to a numeric string matching
/^\d{17,20}$/ (e.g., a 17–20 digit snowflake) so the header x-discord-user-id
will pass src/api/middleware/auth.js validation and exercise the real
success-path behavior.

---

Outside diff comments:
In `@web/tests/components/dashboard/config-editor-autosave.test.tsx`:
- Around line 141-166: The test only asserts presence of the selected category
but not absence of the other, so add inverse assertions to verify route gating:
in the 'renders AI settings on the AI category route' test (using ConfigEditor)
assert that the non-selected category component (e.g., text unique to
OnboardingGrowthCategory like "Onboarding" or whatever its visible label is) is
not present (use queryByText and expect(...).toBeNull() or
expect(...).not.toBeInTheDocument()); and make the analogous change in the other
test (the one covering the Onboarding/Growth route) to assert
AiAutomationCategory-specific text (e.g., "AI Chat") is not present. Reference
the ConfigEditor render and the AiAutomationCategory / OnboardingGrowthCategory
visible labels when adding these negative assertions so the tests actually
verify pathname-based filtering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ffde336b-aed7-4363-aa56-24994ba6005a

📥 Commits

Reviewing files that changed from the base of the PR and between cb77c42 and aaca827.

📒 Files selected for processing (14)
  • src/api/middleware/auth.js
  • tests/api/middleware/auth.test.js
  • tests/api/routes/guilds.test.js
  • tests/api/routes/members.test.js
  • tests/modules/ai.test.js
  • tests/modules/cli-process.test.js
  • vitest.config.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/layout/guild-directory-context.tsx
  • web/tests/api/xp-route.test.ts
  • web/tests/components/dashboard/analytics-dashboard.test.tsx
  • web/tests/components/dashboard/config-diff-modal.test.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • vitest.config.js
  • tests/modules/ai.test.js
  • tests/modules/cli-process.test.js
  • src/api/middleware/auth.js
  • tests/api/middleware/auth.test.js
  • tests/api/routes/members.test.js
  • tests/api/routes/guilds.test.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • vitest.config.js
  • web/tests/components/dashboard/config-diff-modal.test.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/tests/components/dashboard/analytics-dashboard.test.tsx
  • tests/modules/ai.test.js
  • tests/modules/cli-process.test.js
  • src/api/middleware/auth.js
  • tests/api/middleware/auth.test.js
  • tests/api/routes/members.test.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • tests/api/routes/guilds.test.js
  • web/src/components/layout/guild-directory-context.tsx
  • web/tests/api/xp-route.test.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: ESM only - do not use CommonJS modules
Use src/logger.js; do not use console.*

Files:

  • vitest.config.js
  • web/tests/components/dashboard/config-diff-modal.test.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/tests/components/dashboard/analytics-dashboard.test.tsx
  • tests/modules/ai.test.js
  • tests/modules/cli-process.test.js
  • src/api/middleware/auth.js
  • tests/api/middleware/auth.test.js
  • tests/api/routes/members.test.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • tests/api/routes/guilds.test.js
  • web/src/components/layout/guild-directory-context.tsx
  • web/tests/api/xp-route.test.ts
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use parameterized SQL only; do not use string concatenation for SQL queries

Files:

  • vitest.config.js
  • tests/modules/ai.test.js
  • tests/modules/cli-process.test.js
  • src/api/middleware/auth.js
  • tests/api/middleware/auth.test.js
  • tests/api/routes/members.test.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.test.js
  • web/tests/api/xp-route.test.ts
web/tests/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

web/tests/**/*.test.{ts,tsx}: Write web dashboard tests using Vitest 4 with the jsdom environment and React Testing Library, matching the web/src/ structure
Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Files:

  • web/tests/components/dashboard/config-diff-modal.test.tsx
  • web/tests/components/dashboard/analytics-dashboard.test.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • web/tests/api/xp-route.test.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use console.* methods in web dashboard code; use appropriate logging mechanisms for React applications

Files:

  • web/src/components/dashboard/config-editor.tsx
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/components/layout/guild-directory-context.tsx
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/modules/ai.test.js
  • tests/modules/cli-process.test.js
  • tests/api/middleware/auth.test.js
  • tests/api/routes/members.test.js
  • tests/api/routes/guilds.test.js
src/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.js: Never use console.* methods; use the Winston logger instead via import logger from '../logger.js' (adjust path as needed), then call logger.info(), logger.warn(), logger.error(), or logger.debug()
Always use safeReply(), safeSend(), or safeEditReply() instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Files:

  • src/api/middleware/auth.js
src/**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{js,ts}: Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls
Community features should be gated behind config..enabled. Moderation commands are the exception.

Files:

  • src/api/middleware/auth.js
🧠 Learnings (16)
📓 Common learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Applied to files:

  • vitest.config.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Applied to files:

  • web/tests/components/dashboard/config-diff-modal.test.tsx
  • web/tests/components/dashboard/analytics-dashboard.test.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{ts,tsx} : Web dashboard config editor should use category workspace navigation with reusable SettingsFeatureCard pattern (header + master toggle + Basic/Advanced blocks)

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to web/src/components/layout/dashboard-shell.tsx : Dashboard page titles should sync with route changes using DashboardTitleSync component mounted in dashboard-shell.tsx and canonical title string 'Volvox.Bot - AI Powered Discord Bot'

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-26T00:04:14.693Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.693Z
Learning: Applies to {web/src/lib/page-titles.ts,web/src/**/+page.ts,web/src/**/+page.svelte} : New dashboard routes need title wiring in web/src/lib/page-titles.ts: use createPageMetadata() for SSR and keep DashboardTitleSync aligned for client navigation.

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/pages/dashboard/**/*.{ts,tsx} : Use shared title helpers from web/src/lib/page-titles.ts for setting browser titles in dashboard pages

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Write web dashboard tests using Vitest 4 with the `jsdom` environment and React Testing Library, matching the `web/src/` structure

Applied to files:

  • web/tests/components/dashboard/analytics-dashboard.test.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation

Applied to files:

  • src/api/middleware/auth.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id

Applied to files:

  • tests/api/routes/members.test.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.test.js
  • web/tests/api/xp-route.test.ts
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.test.js
  • web/tests/api/xp-route.test.ts
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/app/api/**/route.ts : Include guildId in signed WebSocket ticket payload when issuing tickets from dashboard endpoints

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/tests/api/xp-route.test.ts
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • tests/api/routes/guilds.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/src/app/dashboard/**/*.tsx : For dashboard routes, add a matcher entry to `dashboardTitleMatchers` in `web/src/lib/page-titles.ts`: use exact equality for leaf routes (`pathname === '/dashboard/my-route'`) and subtree checks (`pathname.startsWith('/dashboard/my-route/')`); export `metadata` using `createPageMetadata(title)` for SSR entry points

Applied to files:

  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Write bot tests using Vitest 4 with the `node` environment, matching the `src/` structure in the `tests/` directory

Applied to files:

  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • tests/api/routes/guilds.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/src/**/*.{ts,tsx} : Never use `console.*` methods in web dashboard code; use appropriate logging mechanisms for React applications

Applied to files:

  • web/src/components/layout/guild-directory-context.tsx
🪛 GitHub Check: SonarCloud Code Analysis
tests/api/routes/guilds.test.js

[warning] 167-167: Move function 'createOAuthToken' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ1L68Yok3Kjcr38DY1d&open=AZ1L68Yok3Kjcr38DY1d&pullRequest=360

🔇 Additional comments (8)
tests/modules/ai.test.js (2)

52-54: LGTM!

The assertion correctly uses expect.objectContaining to verify the timestamp field added by addToHistory. This aligns with the implementation at src/modules/ai.js:304 where Date.now() is attached to in-memory messages.


88-96: LGTM!

The assertion correctly distinguishes between DB-hydrated messages (lines 89-90, no timestamp) and the in-memory concurrent message (lines 91-95, with timestamp). This matches the implementation behavior where:

  • hydrateHistory maps DB rows to { role, content } only
  • addToHistory includes timestamp: Date.now()
tests/api/routes/guilds.test.js (2)

103-113: Good permission/access mocking foundation for the new auth model

The permissionState-driven permissions.has() stub and Unknown Member (code: 10007) fetch behavior are well aligned with the route’s access-resolution branches, and the helper keeps test setup explicit.

Also applies to: 129-134, 188-191


260-342: Strong coverage for admin vs moderator authorization paths

These additions clearly exercise admin-only vs moderator-allowed endpoints and mixed-guild access outcomes, which materially reduces regression risk for the permission changes in this PR.

Also applies to: 429-457, 1235-1276

src/api/middleware/auth.js (1)

10-10: Good validation boundary for forwarded actor identity.

req.user is only populated after API-secret auth succeeds, and malformed IDs are ignored instead of leaking into downstream authorization.

Also applies to: 53-59

web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts (1)

106-117: Failing closed when the Discord actor is unavailable is the right contract.

Returning 401 before proxying prevents unauthenticated XP writes from reaching the bot API and keeps the forwarded x-discord-user-id header explicit.

tests/api/middleware/auth.test.js (1)

139-192: Good edge-case coverage for trusted actor parsing.

The valid, blank, and invalid header cases lock in the new trim() and snowflake-validation behavior well.

web/src/components/dashboard/config-editor.tsx (1)

11-16: ConfigEditor is correctly scoped to test-only usage—no action needed.

The component is documented as a backward-compatible entry point for older tests and is only imported by web/tests/components/dashboard/config-editor-autosave.test.tsx. It is not re-exported, not imported by any runtime routes or pages, and not accessible from the main app. No changes required.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 51 out of 51 changed files in this pull request and generated 2 comments.

BillChirico and others added 5 commits April 2, 2026 04:26
Signed-off-by: Bill Chirico <bill@chirico.dev>
…inline comment guidelines

- Clarified the project context in the review prompt for the Discord bot Volvox.Bot.
- Added detailed instructions for posting inline comments, including formatting requirements and examples for AI fix prompts.
- Ensured that the review process emphasizes only reporting issues without providing praise or compliments.
* feat(dashboard): add Discord markdown parser and WYSIWYG editor component

- Add discord-markdown.ts utility with parser, wrapSelection, insertAtCursor, wrapLine
- Add DiscordMarkdownEditor component with:
  - Toolbar (Bold, Italic, Underline, Strikethrough, Code, Code Block, Spoiler, Quote, H1-H3, Lists)
  - Variable inserter dropdown for template variables
  - Split view: raw editor + live Discord-style preview
  - Character counter with configurable limit
  - Keyboard shortcuts (Ctrl+B, Ctrl+I, Ctrl+U)
  - Follows existing Radix UI + Tailwind + Lucide patterns

* test: add DiscordMarkdownEditor and discord-markdown parser tests

- 29 tests covering parser, utility functions, and component behavior
- Tests: toolbar rendering, variable insertion, character counter,
  markdown preview, disabled state, XSS prevention

* fix: address markdown editor review feedback

* fix: restore dashboard config test compatibility

* fix: address discord markdown review feedback

* fix: resolve markdown parser edge cases

* fix(ci): stabilize markdown editor branch checks

* fix(sonar): remove regex hotspot parsing

* fix(sonar): remove remaining regex markdown hotspots

* fix: address remaining review feedback on markdown editor

- Use stable index-based keys for badge rows to prevent remount on edit
- Add timestamp to initConversationHistory startup hydration entries
- Replace placeholder text in Welcome Messages section with proper UI copy
- Fix block quote parser to require space after > (matching Discord behavior)
- Render plain newlines as line breaks instead of paragraph blocks
- Update ReputationSection JSDoc to remove stale 'announcements' reference
- Add fallback to reputation.levelThresholds for backward compatibility

* fix: address final review feedback on markdown editor

- Fix hydration mismatch in preview rendering (isMounted gate)
- Remove * as unordered list marker (Discord only supports -)
- Remove unused act import from autosave tests
- Add TODO for Welcome Messages section placeholder
- Unify ReputationSection to use updateDraftConfig consistently
- Remove stale reputation.levelThresholds fallback
- Add sweepDmLimits test for stale-entry eviction
- Strengthen ai.test.js hydration timestamp assertions with exact values
# Conflicts:
#	tests/modules/ai.test.js
#	web/src/components/dashboard/config-editor.tsx
#	web/tests/components/dashboard/config-editor-autosave.test.tsx
* feat(dashboard): add visual Discord embed builder component (Closes #371)

- Color picker with Discord preset colors + hex input
- Title, description fields with character limits (256, 4096)
- Thumbnail selector: none / user avatar / server icon / custom URL
- Field management: add/remove/reorder with inline toggle
- Footer text (2048) + icon URL, full-width image URL
- Timestamp toggle, format selector (text/embed/text+embed)
- Live Discord-style embed preview with accent color bar
- Variable template support with {{var}} badge rendering
- Lightweight Discord markdown preview (bold, italic, code)
- Character count indicators with color-coded warnings
- Aria-labels on switches for accessibility
- 29 unit tests covering all features

* fix: address review comments on embed-builder

* fix: address embed builder review feedback

* fix: remove regex hotspots from embed preview

* fix: address final embed builder review feedback

* test: cover auth route fallback branches

* test: cover auth route fallback cache recovery
# Conflicts:
#	tests/modules/ai.test.js
#	web/src/components/dashboard/config-editor.tsx
#	web/tests/components/dashboard/config-editor-autosave.test.tsx
Copilot AI review requested due to automatic review settings April 3, 2026 02:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 79 out of 79 changed files in this pull request and generated 5 comments.

Comment on lines +26 to +35
const botGuildIds = guilds.filter((guild) => guild.botPresent).map((guild) => guild.id);
if (botGuildIds.length === 0) {
return guilds;
}

try {
const url = new URL(`${botApiBaseUrl}/guilds/access`);
url.searchParams.set('userId', userId);
url.searchParams.set('guildIds', botGuildIds.join(','));

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

applyAccessLevels() sends all bot-present guild IDs in a single guildIds query param, but the bot API /guilds/access endpoint caps lookups at 100 guilds (returns 400 when exceeded). If a user shares >100 bot-present guilds, access augmentation will fail and the dashboard will fall back to OAuth-only permissions.

Consider batching botGuildIds into chunks of 100 (or truncating with a log) and merging the results before applying them to the guild list.

Copilot uses AI. Check for mistakes.
Comment on lines 105 to +114
export function Sidebar({ className, onNavClick }: SidebarProps) {
const pathname = usePathname();
const guildId = useGuildSelection();
const { guilds } = useGuildDirectory();
const isNavItemActive = (href: string) =>
pathname === href || (href !== '/dashboard' && pathname.startsWith(`${href}/`));
const hasActiveSecondaryItem = secondaryNav.some((item) => isNavItemActive(item.href));
const activeSecondaryHref = secondaryNav.find((item) => isNavItemActive(item.href))?.href ?? null;
const activeGuildAccess = guilds.find((guild) => guild.id === guildId)?.access ?? null;
const visiblePrimaryNav = activeGuildAccess === 'moderator' ? moderatorPrimaryNav : primaryNav;
const visibleSecondaryNav = activeGuildAccess === 'moderator' ? [] : secondaryNav;
const hasActiveSecondaryItem = visibleSecondaryNav.some((item) => isNavItemActive(item.href));
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Sidebar computes activeGuildAccess from guilds but doesn’t account for the directory still loading/empty. On first render guilds is empty, so activeGuildAccess is null and the full navigation (including admin-only pages) is rendered even for moderator-only users until the fetch completes.

To fully enforce the intended moderator navigation restrictions, gate visiblePrimaryNav/visibleSecondaryNav on the directory loading state (e.g., render a skeleton/placeholder, or default to the most-restrictive nav until access is known).

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +110
const token = await getToken({ req: request });
const requesterId = typeof token?.id === 'string' ? token.id : null;
if (!requesterId) {
return NextResponse.json({ error: 'Unable to determine Discord user id' }, { status: 401 });
}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This route requires token.id to be present to forward x-discord-user-id, but other routes in the codebase fall back to token.sub when id is unavailable. If an older/partial NextAuth JWT is missing id, this will return 401 even though the request is otherwise authenticated.

Use the same id ?? sub extraction pattern as web/src/app/api/guilds/route.ts (and only 401 when neither value is available).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +18
'use client';

import {
AlignLeft,
ChevronDown,
ChevronUp,
Clock,
Columns,
Eye,
ImageIcon,
Plus,
Settings,
Trash2,
Type,
} from 'lucide-react';
import * as React from 'react';
import { generateId } from '@/components/dashboard/config-editor-utils';
import { Button } from '@/components/ui/button';
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The PR title/description focus on dashboard permission access controls, migration ordering, command loading, and rate-limit handling, but this PR also introduces a large new Embed Builder UI (and associated preview/utility exports). If this is intentional, the PR description should call it out (or consider splitting into a separate PR) so reviewers can assess scope and risk appropriately.

Copilot uses AI. Check for mistakes.
Comment on lines 5 to +9
globals: false,
environment: 'node',
include: ['tests/**/*.test.js'],
testTimeout: 10000,
testTimeout: 30000,
hookTimeout: 30000,
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Global Vitest timeouts were increased from 10s to 30s for all tests. This can hide hangs/slow tests and makes timeouts less actionable.

If only a few suites need longer (many already pass explicit 30_000 timeouts), prefer keeping the global defaults tight and setting per-test/per-suite timeouts where required (or using vi.setConfig({ testTimeout }) locally).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/modules/ai.js (1)

354-377: ⚠️ Potential issue | 🟠 Major

Select created_at in initConversationHistory outer query.

timestamp is derived from row.created_at, but the outer SELECT does not return created_at.
Result: startup hydration always falls back to Date.now() and loses persisted chronology.

💡 Proposed fix
-      `SELECT channel_id, role, content
+      `SELECT channel_id, role, content, created_at
        FROM (
          SELECT channel_id, role, content, created_at,
                 ROW_NUMBER() OVER (PARTITION BY channel_id ORDER BY created_at DESC) AS rn
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/ai.js` around lines 354 - 377, The outer SQL in
initConversationHistory omits created_at so rows lack created_at and timestamp
falls back to Date.now(), losing persisted chronology; update the outer SELECT
to include created_at (i.e., SELECT channel_id, role, content, created_at FROM
(... ) sub) so the row.created_at value is returned and hydratedByChannel
entries get accurate timestamp values when constructing the objects in the for
loop that sets timestamp from row.created_at.
.github/workflows/claude-review.yml (1)

49-49: 🧹 Nitpick | 🔵 Trivial

Simplify the PR_NUMBER expression.

The X && X pattern is redundant. Since GitHub expressions use JavaScript-style short-circuit evaluation, the first operand alone suffices for truthiness.

♻️ Simplified expression
-          PR_NUMBER: ${{ github.event.pull_request.number && github.event.pull_request.number || github.event.issue.number }}
+          PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/claude-review.yml at line 49, The PR_NUMBER assignment
uses a redundant `X && X` pattern; replace `github.event.pull_request.number &&
github.event.pull_request.number || github.event.issue.number` with the
simplified short-circuit expression `github.event.pull_request.number ||
github.event.issue.number` so the first truthy value is used; update the
PR_NUMBER environment variable assignment accordingly to remove the duplicate
check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/claude-review.yml:
- Around line 92-98: The gh API call for creating a PR review uses the wrong
flags: replace the output-format flag `--json comments` and the lowercase `-f`
form for the comments payload with the form-JSON `-F` flags so the array is
parsed as JSON; specifically update the command that posts to `gh api repos/${{
github.repository }}/pulls/${{ env.PR_NUMBER }}/reviews` to use `-F
event="REQUEST_CHANGES"`, `-F body="..."`, and `-F comments='[...]'` instead of
`-f` and `--json comments`.

In `@tests/modules/actions/sendDm.test.js`:
- Around line 179-205: The test uses vi.useFakeTimers() but restores timers
inline; to avoid leaking fake timers if the test errors, add an afterEach hook
that calls vi.useRealTimers() instead of relying on the final line in the test.
Update the describe('sweepDmLimits', ...) block to include afterEach(() =>
vi.useRealTimers()); so any test using vi.useFakeTimers() (the current test that
calls vi.useFakeTimers(), recordDmSend, sweepDmLimits, checkDmRateLimit, etc.)
will always restore real timers even on failure.

In `@tests/modules/ai.test.js`:
- Around line 89-103: The timestamp assertions are too loose—replace
expect.any(Number) with exact millisecond values by mocking or seeding the DB
rows' created_at timestamps used for the messages ('db message', 'db reply',
'concurrent message') and asserting those exact numeric values; locate the
assertions in tests/modules/ai.test.js (the expect.objectContaining blocks for
the three messages) and change the test setup to provide deterministic
created_at fields (or stub Date.now() only if the code uses it) and assert the
exact milliseconds for each message instead of using expect.any(Number).

In `@web/src/app/api/auth/`[...nextauth]/route.ts:
- Around line 44-50: The current catch wraps both getHandler() and
handler(request, context), causing runtime request errors to be treated as
handler construction failures; change this by isolating handler construction:
call getHandler() inside its own try/catch, and in that catch reset
cachedHandler = undefined and return fallbackAuthResponse(request, error); after
a successful getHandler() return await handler(request, context) with no
surrounding catch so request-time errors from handler(request, context)
propagate as normal 5xxs.

In `@web/src/components/ui/discord-markdown-editor.tsx`:
- Around line 207-229: The textarea's maxLength is not enforced: add the
maxLength prop to the textarea element and ensure any programmatic edits in
applyAction (function applyAction) and insertVariable (the other handler
referenced around 232-253 and 359-368) truncate the resulting text to the
provided maxLength before calling onChange; when trimming, also adjust
result.selectionStart/result.selectionEnd/result.cursorPos to be within the
truncated length so setSelectionRange uses valid indices. Ensure you reference
textareaRef, the result object returned by action/value transforms, and the
maxLength prop when implementing the truncation and selection adjustment.

In `@web/src/components/ui/embed-builder.tsx`:
- Around line 545-559: The builder currently emits configs that can exceed
Discord's 6000-char embed cap because update() and updateField() blindly call
onChange; fix both functions to enforce CHAR_LIMITS.total by computing the
current total length (summing value.title, value.description, value.footer, and
every field's name+value) and, before calling onChange, either trim the incoming
patch strings to fit the remaining allowed characters or reject the change —
e.g., for update(), compute allowedRemaining = CHAR_LIMITS.total - currentTotal
+ lengthOfAnyReplacedParts and clamp patch.title/description/footer/fields
accordingly; for updateField(), compute total excluding the target field,
determine available space for that field's name/value, trim the patch for that
field to fit, then call onChange with the adjusted fields so no emitted config
exceeds CHAR_LIMITS.total.
- Around line 168-323: This file duplicates Discord markdown parsing logic
(tokenizeMarkdownSegments, renderMarkdownSegment, renderDiscordMarkdown) which
causes drift; replace those local implementations by importing and using the
shared parser/renderer from web/src/lib/discord-markdown.ts (import the exported
function(s) there, e.g., the parser that returns tokenized segments or React
nodes) and remove the local tokenizeMarkdownSegments, renderMarkdownSegment and
renderDiscordMarkdown implementations; keep or adapt
tokenizeVariableSegments/renderVariablePreview to use the token stream or React
nodes produced by the shared parser so variables render as the same badges
(update calls that used renderDiscordMarkdown to call the shared export
instead).

In `@web/tests/api/auth-route.test.ts`:
- Around line 24-26: The helper createRequest currently constructs NextRequest
without an init object so it defaults to GET; update createRequest to accept an
optional method parameter (or an options/init param) and pass it through to new
NextRequest(url, init) so tests can explicitly set method: "POST", then update
the POST test calls (the ones around the createRequest usages noted) to call
createRequest(pathname, "POST") (or provide { method: "POST" }) so the tests
send real POST requests instead of defaulting to GET.

In `@web/tests/components/dashboard/config-editor-autosave.test.tsx`:
- Around line 203-207: Remove the duplicated waitFor assertion and fix the extra
indentation in the test block: eliminate the redundant
expect(screen.getAllByText('Welcome Messages').length).toBeGreaterThan(0) that
repeats the earlier assertion, and unindent the remaining lines so the expect
calls inside the waitFor callback align properly; locate the waitFor usage and
the expect(...) assertions in the test function in
config-editor-autosave.test.tsx to make these edits.
- Around line 174-179: The test contains a redundant assertion and wrong
indentation: remove the duplicated expect(screen.getAllByText('AI
Chat').length).toBeGreaterThan(0) that repeats an earlier check inside the same
waitFor, and fix the indentation of the remaining lines inside the waitFor block
to use 2 spaces (not 6), keeping the
expect(screen.getByText('Settings')).toBeInTheDocument() and
expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0) assertions
unchanged; locate the assertions within the waitFor callback in
web/tests/components/dashboard/config-editor-autosave.test.tsx to apply these
edits.

---

Outside diff comments:
In @.github/workflows/claude-review.yml:
- Line 49: The PR_NUMBER assignment uses a redundant `X && X` pattern; replace
`github.event.pull_request.number && github.event.pull_request.number ||
github.event.issue.number` with the simplified short-circuit expression
`github.event.pull_request.number || github.event.issue.number` so the first
truthy value is used; update the PR_NUMBER environment variable assignment
accordingly to remove the duplicate check.

In `@src/modules/ai.js`:
- Around line 354-377: The outer SQL in initConversationHistory omits created_at
so rows lack created_at and timestamp falls back to Date.now(), losing persisted
chronology; update the outer SELECT to include created_at (i.e., SELECT
channel_id, role, content, created_at FROM (... ) sub) so the row.created_at
value is returned and hydratedByChannel entries get accurate timestamp values
when constructing the objects in the for loop that sets timestamp from
row.created_at.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d32f881a-41b9-4e9a-b9e7-03ab9bd67658

📥 Commits

Reviewing files that changed from the base of the PR and between aaca827 and 1574422.

📒 Files selected for processing (30)
  • .github/workflows/ci.yml
  • .github/workflows/claude-review.yml
  • biome.json
  • src/modules/actions/buildPayload.js
  • src/modules/actions/xpBonus.js
  • src/modules/ai.js
  • src/modules/levelUpActions.js
  • src/modules/triage-parse.js
  • src/modules/triage.js
  • src/modules/welcomeOnboarding.js
  • tests/api/utils/configValidation.test.js
  • tests/modules/actions/addReaction.test.js
  • tests/modules/actions/announce.test.js
  • tests/modules/actions/nickPrefix.test.js
  • tests/modules/actions/sendDm.test.js
  • tests/modules/actions/webhook.test.js
  • tests/modules/actions/xpBonus.test.js
  • tests/modules/ai.test.js
  • web/src/app/api/auth/[...nextauth]/route.ts
  • web/src/components/dashboard/analytics-dashboard-sections.tsx
  • web/src/components/dashboard/analytics-dashboard.tsx
  • web/src/components/dashboard/config-sections/ReputationSection.tsx
  • web/src/components/dashboard/config-sections/index.ts
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/src/components/ui/embed-builder.tsx
  • web/src/lib/discord-markdown.ts
  • web/tests/api/auth-route.test.ts
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/tests/components/ui/embed-builder.test.tsx
💤 Files with no reviewable changes (3)
  • web/src/components/dashboard/config-sections/index.ts
  • web/src/components/dashboard/analytics-dashboard.tsx
  • web/src/components/dashboard/config-sections/ReputationSection.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (11)
**/*.json

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use double quotes in JSON files (standard JSON format); this exception applies only to JSON files, not JavaScript/TypeScript

Files:

  • biome.json
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • src/modules/actions/buildPayload.js
  • tests/modules/actions/xpBonus.test.js
  • src/modules/welcomeOnboarding.js
  • tests/modules/actions/webhook.test.js
  • src/modules/triage.js
  • tests/api/utils/configValidation.test.js
  • tests/modules/actions/announce.test.js
  • tests/modules/actions/addReaction.test.js
  • src/modules/actions/xpBonus.js
  • src/modules/triage-parse.js
  • tests/modules/actions/nickPrefix.test.js
  • src/modules/levelUpActions.js
  • src/modules/ai.js
  • tests/modules/ai.test.js
  • tests/modules/actions/sendDm.test.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • src/modules/actions/buildPayload.js
  • tests/modules/actions/xpBonus.test.js
  • src/modules/welcomeOnboarding.js
  • tests/modules/actions/webhook.test.js
  • src/modules/triage.js
  • tests/api/utils/configValidation.test.js
  • tests/modules/actions/announce.test.js
  • tests/modules/actions/addReaction.test.js
  • src/modules/actions/xpBonus.js
  • src/modules/triage-parse.js
  • tests/modules/actions/nickPrefix.test.js
  • src/modules/levelUpActions.js
  • src/modules/ai.js
  • tests/modules/ai.test.js
  • web/src/components/dashboard/analytics-dashboard-sections.tsx
  • tests/modules/actions/sendDm.test.js
  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/src/app/api/auth/[...nextauth]/route.ts
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • web/tests/api/auth-route.test.ts
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/src/lib/discord-markdown.ts
  • web/tests/components/ui/embed-builder.test.tsx
  • web/src/components/ui/embed-builder.tsx
src/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.js: Never use console.* methods; use the Winston logger instead via import logger from '../logger.js' (adjust path as needed), then call logger.info(), logger.warn(), logger.error(), or logger.debug()
Always use safeReply(), safeSend(), or safeEditReply() instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Files:

  • src/modules/actions/buildPayload.js
  • src/modules/welcomeOnboarding.js
  • src/modules/triage.js
  • src/modules/actions/xpBonus.js
  • src/modules/triage-parse.js
  • src/modules/levelUpActions.js
  • src/modules/ai.js
src/modules/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create feature modules in src/modules/ and add corresponding config sections to config.json

Files:

  • src/modules/actions/buildPayload.js
  • src/modules/welcomeOnboarding.js
  • src/modules/triage.js
  • src/modules/actions/xpBonus.js
  • src/modules/triage-parse.js
  • src/modules/levelUpActions.js
  • src/modules/ai.js
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: ESM only - do not use CommonJS modules
Use src/logger.js; do not use console.*

Files:

  • src/modules/actions/buildPayload.js
  • tests/modules/actions/xpBonus.test.js
  • src/modules/welcomeOnboarding.js
  • tests/modules/actions/webhook.test.js
  • src/modules/triage.js
  • tests/api/utils/configValidation.test.js
  • tests/modules/actions/announce.test.js
  • tests/modules/actions/addReaction.test.js
  • src/modules/actions/xpBonus.js
  • src/modules/triage-parse.js
  • tests/modules/actions/nickPrefix.test.js
  • src/modules/levelUpActions.js
  • src/modules/ai.js
  • tests/modules/ai.test.js
  • web/src/components/dashboard/analytics-dashboard-sections.tsx
  • tests/modules/actions/sendDm.test.js
  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/src/app/api/auth/[...nextauth]/route.ts
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • web/tests/api/auth-route.test.ts
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/src/lib/discord-markdown.ts
  • web/tests/components/ui/embed-builder.test.tsx
  • web/src/components/ui/embed-builder.tsx
src/**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{js,ts}: Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls
Community features should be gated behind config..enabled. Moderation commands are the exception.

Files:

  • src/modules/actions/buildPayload.js
  • src/modules/welcomeOnboarding.js
  • src/modules/triage.js
  • src/modules/actions/xpBonus.js
  • src/modules/triage-parse.js
  • src/modules/levelUpActions.js
  • src/modules/ai.js
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use parameterized SQL only; do not use string concatenation for SQL queries

Files:

  • src/modules/actions/buildPayload.js
  • tests/modules/actions/xpBonus.test.js
  • src/modules/welcomeOnboarding.js
  • tests/modules/actions/webhook.test.js
  • src/modules/triage.js
  • tests/api/utils/configValidation.test.js
  • tests/modules/actions/announce.test.js
  • tests/modules/actions/addReaction.test.js
  • src/modules/actions/xpBonus.js
  • src/modules/triage-parse.js
  • tests/modules/actions/nickPrefix.test.js
  • src/modules/levelUpActions.js
  • src/modules/ai.js
  • tests/modules/ai.test.js
  • tests/modules/actions/sendDm.test.js
  • web/src/app/api/auth/[...nextauth]/route.ts
  • web/tests/api/auth-route.test.ts
  • web/src/lib/discord-markdown.ts
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/modules/actions/xpBonus.test.js
  • tests/modules/actions/webhook.test.js
  • tests/api/utils/configValidation.test.js
  • tests/modules/actions/announce.test.js
  • tests/modules/actions/addReaction.test.js
  • tests/modules/actions/nickPrefix.test.js
  • tests/modules/ai.test.js
  • tests/modules/actions/sendDm.test.js
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use console.* methods in web dashboard code; use appropriate logging mechanisms for React applications

Files:

  • web/src/components/dashboard/analytics-dashboard-sections.tsx
  • web/src/app/api/auth/[...nextauth]/route.ts
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/src/lib/discord-markdown.ts
  • web/src/components/ui/embed-builder.tsx
web/tests/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

web/tests/**/*.test.{ts,tsx}: Write web dashboard tests using Vitest 4 with the jsdom environment and React Testing Library, matching the web/src/ structure
Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Files:

  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • web/tests/api/auth-route.test.ts
  • web/tests/components/ui/embed-builder.test.tsx
🧠 Learnings (16)
📓 Common learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to **/*.js : Use ESM-only syntax: `import`/`export`, never `require()`/`module.exports`

Applied to files:

  • biome.json
📚 Learning: 2026-03-26T00:04:14.693Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.693Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : ESM only - do not use CommonJS modules

Applied to files:

  • biome.json
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/reactionrole.js : Enforce invoker role hierarchy check in /reactionrole add command to prevent non-owner users from configuring roles at or above their highest role

Applied to files:

  • src/modules/welcomeOnboarding.js
📚 Learning: 2026-03-26T00:04:14.693Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.693Z
Learning: Applies to src/**/*.{js,ts} : Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls

Applied to files:

  • tests/modules/actions/announce.test.js
  • src/modules/levelUpActions.js
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/src/lib/discord-markdown.ts
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Always use `safeReply()`, `safeSend()`, or `safeEditReply()` instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Applied to files:

  • tests/modules/actions/announce.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Applied to files:

  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Write web dashboard tests using Vitest 4 with the `jsdom` environment and React Testing Library, matching the `web/src/` structure

Applied to files:

  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • web/tests/api/auth-route.test.ts
  • web/tests/components/ui/embed-builder.test.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Applied to files:

  • web/tests/components/ui/discord-markdown-editor.test.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/api/routes/**/*.js : Create API route handlers in `src/api/routes/` and mount them in `src/api/server.js`; add auth middleware if the endpoint requires authentication

Applied to files:

  • web/src/app/api/auth/[...nextauth]/route.ts
  • web/tests/api/auth-route.test.ts
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to web/src/components/layout/dashboard-shell.tsx : Dashboard page titles should sync with route changes using DashboardTitleSync component mounted in dashboard-shell.tsx and canonical title string 'Volvox.Bot - AI Powered Discord Bot'

Applied to files:

  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{ts,tsx} : Web dashboard config editor should use category workspace navigation with reusable SettingsFeatureCard pattern (header + master toggle + Basic/Advanced blocks)

Applied to files:

  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/src/app/dashboard/**/*.tsx : For dashboard routes, add a matcher entry to `dashboardTitleMatchers` in `web/src/lib/page-titles.ts`: use exact equality for leaf routes (`pathname === '/dashboard/my-route'`) and subtree checks (`pathname.startsWith('/dashboard/my-route/')`); export `metadata` using `createPageMetadata(title)` for SSR entry points

Applied to files:

  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • web/tests/api/auth-route.test.ts
📚 Learning: 2026-03-26T00:04:14.693Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.693Z
Learning: Applies to {web/src/lib/page-titles.ts,web/src/**/+page.ts,web/src/**/+page.svelte} : New dashboard routes need title wiring in web/src/lib/page-titles.ts: use createPageMetadata() for SSR and keep DashboardTitleSync aligned for client navigation.

Applied to files:

  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Config editor save contract: maintain global save/discard, diff-modal confirmation, per-section PATCH batching, and partial-failure behavior

Applied to files:

  • web/tests/components/dashboard/config-editor-autosave.test.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Write bot tests using Vitest 4 with the `node` environment, matching the `src/` structure in the `tests/` directory

Applied to files:

  • web/tests/components/dashboard/config-editor-autosave.test.tsx
  • web/tests/api/auth-route.test.ts
  • web/tests/components/ui/embed-builder.test.tsx
🔇 Additional comments (30)
biome.json (1)

2-2: Schema version update looks good.

Scoped $schema bump only, with valid JSON formatting and no adverse config changes detected.

src/modules/welcomeOnboarding.js (1)

234-234: Optional-chaining guard is a solid simplification.

This preserves behavior while making the role eligibility check more concise and safe for cache/fetch misses.

tests/api/utils/configValidation.test.js (2)

414-417: Formatting-only refactor looks good.

This keeps the same assertion behavior while improving readability.


425-427: Formatting-only refactor looks good.

No behavior change; assertion intent remains clear.

web/src/components/dashboard/analytics-dashboard-sections.tsx (1)

120-120: Unused-prop aliasing is clean and safe.

Keeping compareMode in the public prop contract while aliasing to _compareMode avoids lint noise without behavior changes.

.github/workflows/ci.yml (1)

140-140: Good cache-key correction for monorepo lockfile.

Using hashFiles('pnpm-lock.yaml') is the right fix here; it properly invalidates Playwright cache when workspace dependencies change.

src/modules/triage-parse.js (1)

77-77: LGTM: Safe optional chaining refactor.

The use of optional chaining (parsed?.classification) safely handles both null/undefined parsed values and missing classification properties in a single, concise expression. This is cleaner than the previous two-step check and aligns with modern JavaScript best practices.

src/modules/triage.js (2)

328-328: LGTM: Defensive optional chaining.

The optional chaining (parsed?.responses?.length) safely guards against null/undefined parsed objects and missing responses arrays before checking length. This is more defensive than the previous approach and prevents potential runtime errors.


541-549: LGTM: Clean formatting with trailing commas.

Reformatting the sendResponses invocation across multiple lines with trailing commas improves readability and produces cleaner git diffs when arguments are added or removed. This follows the coding guideline for multi-line function calls.

src/modules/actions/buildPayload.js (1)

32-33: LGTM! Formatting improvement enforces line-width guideline.

The multi-line format keeps the line under the 100-character maximum and improves consistency with the description and footer blocks above. As per coding guidelines, the 100-character line width is maintained.

tests/modules/actions/addReaction.test.js (1)

9-10: LGTM!

The import reordering correctly places the handleAddReaction import after the logger mock setup. This follows the standard Vitest ESM mocking pattern where vi.mock() declarations must precede actual imports for proper hoisting.

tests/modules/actions/announce.test.js (1)

17-20: LGTM!

Import ordering correctly places module imports after all vi.mock() declarations, ensuring proper mock hoisting for ESM modules.

tests/modules/actions/webhook.test.js (2)

9-10: LGTM!

Import reordering follows the correct Vitest ESM mock hoisting pattern.


92-92: LGTM!

The single-line reformatting of assertion calls improves readability while staying within the 100-character line limit.

src/modules/actions/xpBonus.js (1)

68-72: LGTM!

The query formatting change is cosmetic. The parameterized SQL properly prevents injection, and the try/finally block ensures the recursion guard is cleared even on errors.

tests/modules/actions/xpBonus.test.js (1)

14-15: LGTM!

Import reordering follows the correct Vitest ESM mock hoisting pattern, placing module imports after vi.mock() declarations.

src/modules/levelUpActions.js (1)

16-18: LGTM!

Import reordering improves consistency by grouping action imports alphabetically. The action registry registrations remain correctly mapped.

tests/modules/actions/nickPrefix.test.js (1)

9-12: LGTM!

Import reordering follows proper Vitest mock hoisting patterns, and the single-line parameter formatting improves readability while staying within line length limits.

tests/modules/actions/sendDm.test.js (1)

14-22: LGTM!

Import reordering correctly places logger imports before action module imports, following Vitest's ESM mock hoisting pattern.

src/modules/ai.js (1)

224-235: Good hydration update for DB-backed timestamps.

Selecting created_at and mapping it to timestamp in hydrateHistory is a solid fix for preserving message chronology during lazy loads.

tests/modules/ai.test.js (1)

124-126: Nice query assertion update.

Including created_at in the SQL expectation adds a useful guard for hydration query regressions.

.github/workflows/claude-review.yml (3)

17-30: LGTM!

The job condition correctly triggers on @claude mentions across all three event types, with proper null-safety using || '' fallback.


56-164: Prompt structure is comprehensive and well-organized.

The categorization of rules (HARD RULES, DISCORD-SPECIFIC, DOCS, OUTPUT), the inline comment format with AI Fix Prompts, and the concrete examples provide clear guidance for the review agent.


41-44: No issues found. actions/checkout@v6 is the current stable version.

web/tests/components/dashboard/config-editor-autosave.test.tsx (5)

13-18: LGTM!

The module-level mockPathname pattern with the next/navigation mock is a clean approach for testing route-dependent behavior. The mock properly stubs useRouter methods.


69-84: LGTM!

The conditional rendering based on open and the exposed onConfirm handler provide adequate test coverage for the confirmation flow.


232-294: LGTM!

The test correctly verifies the PATCH request body format { path: 'ai.systemPrompt', value: 'Updated prompt' }, which matches the API contract in validateConfigPatchBody and the component's updateSystemPrompt implementation. The three-response mock chain properly simulates the initial load, PATCH response, and subsequent config refresh.


296-323: LGTM!

Clean test for the discard functionality — properly sets up the saved state, simulates user edits, triggers discard, and verifies restoration to the last saved value.


326-408: LGTM!

The unit tests for normalization and config update utilities are well-structured with good coverage of edge cases (clamping, empty strings, nested updates). The immutability checks confirming source objects are not mutated are valuable for catching accidental mutations.

web/src/app/api/auth/[...nextauth]/route.ts (1)

14-16: The current implementation is correct for next-auth v4.24.x.

The /session endpoint should return {} (empty object) when there is no active session, which matches the standard next-auth v4.24.x behavior. Clients built to work with next-auth expect this response format. Changing it to null would break compatibility with the next-auth contract.

			> Likely an incorrect or invalid review comment.

Comment on lines +92 to +98
```bash
gh api repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}/reviews \
-X POST \
-f event="REQUEST_CHANGES" \
-f body="Code review complete. See inline comments for issues and AI fix prompts." \
--json comments -f comments='[...array of comment objects...]'
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incorrect gh api syntax: --json is for output formatting, not input.

The --json comments flag tells gh which fields to include in the JSON output. To pass a JSON array as input, use -F (capital F) which parses the value as JSON. The current syntax will fail or behave unexpectedly, causing Claude to generate broken commands.

🐛 Proposed fix
             ```bash
             gh api repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}/reviews \
               -X POST \
-              -f event="REQUEST_CHANGES" \
-              -f body="Code review complete. See inline comments for issues and AI fix prompts." \
-              --json comments -f comments='[...array of comment objects...]'
+              -F event="REQUEST_CHANGES" \
+              -F body="Code review complete. See inline comments for issues and AI fix prompts." \
+              -F comments='[...array of comment objects...]'
             ```

Using -F (instead of -f) enables JSON type inference, so arrays like [{"path":"...","line":10,"body":"..."}] are correctly parsed as JSON rather than strings.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```bash
gh api repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}/reviews \
-X POST \
-f event="REQUEST_CHANGES" \
-f body="Code review complete. See inline comments for issues and AI fix prompts." \
--json comments -f comments='[...array of comment objects...]'
```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/claude-review.yml around lines 92 - 98, The gh API call
for creating a PR review uses the wrong flags: replace the output-format flag
`--json comments` and the lowercase `-f` form for the comments payload with the
form-JSON `-F` flags so the array is parsed as JSON; specifically update the
command that posts to `gh api repos/${{ github.repository }}/pulls/${{
env.PR_NUMBER }}/reviews` to use `-F event="REQUEST_CHANGES"`, `-F body="..."`,
and `-F comments='[...]'` instead of `-f` and `--json comments`.

Comment on lines +179 to +205
describe('sweepDmLimits', () => {
beforeEach(() => {
resetDmLimits();
});

it('should evict stale entries and keep recent ones', () => {
vi.useFakeTimers();

// Record an entry that will become stale
recordDmSend('g1', 'u-stale');

// Advance past the rate window (60s)
vi.advanceTimersByTime(60_001);

// Record a recent entry
recordDmSend('g1', 'u-recent');

// Sweep should evict the stale entry
sweepDmLimits();

// Stale entry evicted — should be allowed again
expect(checkDmRateLimit('g1', 'u-stale')).toBe(true);
// Recent entry still rate-limited
expect(checkDmRateLimit('g1', 'u-recent')).toBe(false);

vi.useRealTimers();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add afterEach to restore real timers for test isolation.

If the test fails before reaching vi.useRealTimers() on line 204, fake timers will leak into subsequent tests, potentially causing flaky failures. Move timer restoration to an afterEach hook for robustness.

♻️ Proposed fix
 describe('sweepDmLimits', () => {
   beforeEach(() => {
     resetDmLimits();
   });
+
+  afterEach(() => {
+    vi.useRealTimers();
+  });

   it('should evict stale entries and keep recent ones', () => {
     vi.useFakeTimers();

     // Record an entry that will become stale
     recordDmSend('g1', 'u-stale');

     // Advance past the rate window (60s)
     vi.advanceTimersByTime(60_001);

     // Record a recent entry
     recordDmSend('g1', 'u-recent');

     // Sweep should evict the stale entry
     sweepDmLimits();

     // Stale entry evicted — should be allowed again
     expect(checkDmRateLimit('g1', 'u-stale')).toBe(true);
     // Recent entry still rate-limited
     expect(checkDmRateLimit('g1', 'u-recent')).toBe(false);
-
-    vi.useRealTimers();
   });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/modules/actions/sendDm.test.js` around lines 179 - 205, The test uses
vi.useFakeTimers() but restores timers inline; to avoid leaking fake timers if
the test errors, add an afterEach hook that calls vi.useRealTimers() instead of
relying on the final line in the test. Update the describe('sweepDmLimits', ...)
block to include afterEach(() => vi.useRealTimers()); so any test using
vi.useFakeTimers() (the current test that calls vi.useFakeTimers(),
recordDmSend, sweepDmLimits, checkDmRateLimit, etc.) will always restore real
timers even on failure.

Comment on lines +89 to +103
expect.objectContaining({
role: 'user',
content: 'db message',
timestamp: expect.any(Number),
}),
expect.objectContaining({
role: 'assistant',
content: 'db reply',
timestamp: expect.any(Number),
}),
expect.objectContaining({
role: 'user',
content: 'concurrent message',
timestamp: expect.any(Number),
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Timestamp assertions are too permissive for this behavior change.

expect.any(Number) passes even when timestamps come from Date.now() fallback instead of DB created_at.
Please make this deterministic by mocking created_at values and asserting exact milliseconds.

💡 Suggested test hardening
       resolveHydration({
         rows: [
-          { role: 'assistant', content: 'db reply' },
-          { role: 'user', content: 'db message' },
+          {
+            role: 'assistant',
+            content: 'db reply',
+            created_at: '2026-03-01T10:01:00.000Z',
+          },
+          {
+            role: 'user',
+            content: 'db message',
+            created_at: '2026-03-01T10:00:00.000Z',
+          },
         ],
       });

@@
         expect(historyRef).toEqual([
           expect.objectContaining({
             role: 'user',
             content: 'db message',
-            timestamp: expect.any(Number),
+            timestamp: new Date('2026-03-01T10:00:00.000Z').getTime(),
           }),
           expect.objectContaining({
             role: 'assistant',
             content: 'db reply',
-            timestamp: expect.any(Number),
+            timestamp: new Date('2026-03-01T10:01:00.000Z').getTime(),
           }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/modules/ai.test.js` around lines 89 - 103, The timestamp assertions are
too loose—replace expect.any(Number) with exact millisecond values by mocking or
seeding the DB rows' created_at timestamps used for the messages ('db message',
'db reply', 'concurrent message') and asserting those exact numeric values;
locate the assertions in tests/modules/ai.test.js (the expect.objectContaining
blocks for the three messages) and change the test setup to provide
deterministic created_at fields (or stub Date.now() only if the code uses it)
and assert the exact milliseconds for each message instead of using
expect.any(Number).

Comment on lines +168 to +323
function tokenizeVariableSegments(text: string): string[] {
const segments: string[] = [];
let cursor = 0;

while (cursor < text.length) {
const variableStart = text.indexOf('{{', cursor);
if (variableStart === -1) {
segments.push(text.slice(cursor));
break;
}

if (variableStart > cursor) {
segments.push(text.slice(cursor, variableStart));
}

const variableEnd = text.indexOf('}}', variableStart + 2);
if (variableEnd === -1) {
segments.push(text.slice(variableStart));
break;
}

segments.push(text.slice(variableStart, variableEnd + 2));
cursor = variableEnd + 2;
}

return segments;
}

function tokenizeMarkdownSegments(line: string): string[] {
const segments: string[] = [];
let cursor = 0;

while (cursor < line.length) {
if (line.startsWith('{{', cursor)) {
const variableEnd = line.indexOf('}}', cursor + 2);
if (variableEnd !== -1) {
segments.push(line.slice(cursor, variableEnd + 2));
cursor = variableEnd + 2;
continue;
}
}

if (line.startsWith('**', cursor)) {
const boldEnd = line.indexOf('**', cursor + 2);
if (boldEnd !== -1) {
segments.push(line.slice(cursor, boldEnd + 2));
cursor = boldEnd + 2;
continue;
}
}

if (line.startsWith('*', cursor)) {
const italicEnd = line.indexOf('*', cursor + 1);
if (italicEnd !== -1) {
segments.push(line.slice(cursor, italicEnd + 1));
cursor = italicEnd + 1;
continue;
}
}

if (line.startsWith('`', cursor)) {
const codeEnd = line.indexOf('`', cursor + 1);
if (codeEnd !== -1) {
segments.push(line.slice(cursor, codeEnd + 1));
cursor = codeEnd + 1;
continue;
}
}

const nextTokenStarts = [
line.indexOf('{{', cursor + 1),
line.indexOf('**', cursor + 1),
line.indexOf('*', cursor + 1),
line.indexOf('`', cursor + 1),
].filter((index) => index !== -1);
const nextTokenStart = nextTokenStarts.length > 0 ? Math.min(...nextTokenStarts) : line.length;

segments.push(line.slice(cursor, nextTokenStart));
cursor = nextTokenStart;
}

return segments;
}

/** Render template variables as styled badges in a string for preview */
function renderVariablePreview(text: string): React.ReactNode[] {
if (!text) return [];

return tokenizeVariableSegments(text).map((part, index) => {
if (part.startsWith('{{') && part.endsWith('}}')) {
const varName = part.slice(2, -2);
return (
<span
key={`${varName}-${index}`}
className="inline-flex items-center rounded bg-primary/20 px-1.5 py-0.5 text-xs font-medium text-primary"
>
{varName}
</span>
);
}
return <span key={`text-${index}`}>{part}</span>;
});
}

/** Very lightweight Discord markdown → HTML (bold, italic, inline code) */
function renderMarkdownSegment(segment: string, lineIndex: number, segmentIndex: number) {
if (segment.startsWith('{{') && segment.endsWith('}}')) {
const varName = segment.slice(2, -2);
return (
<span
key={`var-${lineIndex}-${segmentIndex}`}
className="inline-flex items-center rounded bg-primary/20 px-1.5 py-0.5 text-xs font-medium text-primary"
>
{varName}
</span>
);
}

if (segment.startsWith('**') && segment.endsWith('**')) {
return <strong key={`b-${lineIndex}-${segmentIndex}`}>{segment.slice(2, -2)}</strong>;
}

if (segment.startsWith('*') && segment.endsWith('*')) {
return <em key={`i-${lineIndex}-${segmentIndex}`}>{segment.slice(1, -1)}</em>;
}

if (segment.startsWith('`') && segment.endsWith('`')) {
return (
<code
key={`c-${lineIndex}-${segmentIndex}`}
className="rounded bg-muted px-1 py-0.5 text-xs font-mono"
>
{segment.slice(1, -1)}
</code>
);
}

return <span key={`t-${lineIndex}-${segmentIndex}`}>{segment}</span>;
}

function renderDiscordMarkdown(text: string): React.ReactNode[] {
const lines = text.split('\n');
const result: React.ReactNode[] = [];

for (let li = 0; li < lines.length; li++) {
if (li > 0) result.push(<br key={`br-${li}`} />);
const line = lines[li];
const segments = tokenizeMarkdownSegments(line);
for (let si = 0; si < segments.length; si++) {
const seg = segments[si];
if (!seg) continue;
result.push(renderMarkdownSegment(seg, li, si));
}
}
return result;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Reuse the shared Discord markdown parser instead of maintaining a second one here.

This file now has its own markdown tokenizer/renderer, but it already supports a smaller syntax subset than web/src/lib/discord-markdown.ts. That means the embed preview will drift from the rest of the dashboard as soon as one side changes. Please extract a shared preview-rendering helper and feed both UIs through the same parser.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/embed-builder.tsx` around lines 168 - 323, This file
duplicates Discord markdown parsing logic (tokenizeMarkdownSegments,
renderMarkdownSegment, renderDiscordMarkdown) which causes drift; replace those
local implementations by importing and using the shared parser/renderer from
web/src/lib/discord-markdown.ts (import the exported function(s) there, e.g.,
the parser that returns tokenized segments or React nodes) and remove the local
tokenizeMarkdownSegments, renderMarkdownSegment and renderDiscordMarkdown
implementations; keep or adapt tokenizeVariableSegments/renderVariablePreview to
use the token stream or React nodes produced by the shared parser so variables
render as the same badges (update calls that used renderDiscordMarkdown to call
the shared export instead).

Comment on lines +24 to +26
function createRequest(pathname: string) {
return new NextRequest(new URL(`http://localhost:3000${pathname}`));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Next.js 16, what method does new NextRequest(url)use when nomethod is provided in the init object?

💡 Result:

In Next.js 16, new NextRequest(url) uses the standard Web API Request constructor behavior, defaulting to the "GET" method when no method is provided in the init object.

Citations:


🏁 Script executed:

# First, check the test file structure and the createRequest function
head -30 web/tests/api/auth-route.test.ts | cat -n

Repository: VolvoxLLC/volvox-bot

Length of output: 985


🏁 Script executed:

# Get the specific lines mentioned in the review (24-26, 149-153, 196-203)
sed -n '24,26p; 149,153p; 196,203p' web/tests/api/auth-route.test.ts | cat -n

Repository: VolvoxLLC/volvox-bot

Length of output: 848


🏁 Script executed:

# Check the route file to see if POST export validates HTTP method
find . -path '*/app/api/auth/*/route.ts' -type f

Repository: VolvoxLLC/volvox-bot

Length of output: 109


🏁 Script executed:

# Check the POST export in the auth route to see if it validates method
cat -n web/src/app/api/auth/\[...nextauth\]/route.ts

Repository: VolvoxLLC/volvox-bot

Length of output: 2434


🏁 Script executed:

# Also verify the test file structure around lines 149-153 and 196-203 with more context
sed -n '145,210p' web/tests/api/auth-route.test.ts | cat -n

Repository: VolvoxLLC/volvox-bot

Length of output: 2997


Specify HTTP method explicitly in POST test cases.

The createRequest() helper defaults to GET because NextRequest defaults to GET when no init object is provided. While the route handler doesn't currently validate the method, POST tests should pass actual POST requests to avoid silently passing if auth logic becomes method-sensitive.

Suggested fix
-function createRequest(pathname: string) {
-  return new NextRequest(new URL(`http://localhost:3000${pathname}`));
+function createRequest(pathname: string, method: string = 'GET') {
+  return new NextRequest(new URL(`http://localhost:3000${pathname}`), { method });
 }
@@
-    const response = await POST(createRequest('/api/auth/csrf'), createContext(['csrf']));
+    const response = await POST(
+      createRequest('/api/auth/csrf', 'POST'),
+      createContext(['csrf']),
+    );
@@
     const response = await POST(
-      createRequest('/api/auth/callback/discord'),
+      createRequest('/api/auth/callback/discord', 'POST'),
       createContext(['callback', 'discord']),
     );

Also applies to: 149-153, 196-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/tests/api/auth-route.test.ts` around lines 24 - 26, The helper
createRequest currently constructs NextRequest without an init object so it
defaults to GET; update createRequest to accept an optional method parameter (or
an options/init param) and pass it through to new NextRequest(url, init) so
tests can explicitly set method: "POST", then update the POST test calls (the
ones around the createRequest usages noted) to call createRequest(pathname,
"POST") (or provide { method: "POST" }) so the tests send real POST requests
instead of defaulting to GET.

Comment on lines +174 to +179
expect(screen.getAllByText('AI Chat').length).toBeGreaterThan(0);
expect(screen.getByText('Settings')).toBeInTheDocument();
expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
},
30_000,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix indentation and remove redundant assertion.

Lines 175-176 have inconsistent indentation (6 spaces instead of 4), violating the 2-space indentation guideline. Additionally, line 174 duplicates the assertion from line 169 inside waitFor — once waitFor passes, the element is guaranteed to exist.

🛠️ Proposed fix
     await waitFor(
       () => {
         expect(screen.getAllByText('AI Chat').length).toBeGreaterThan(0);
       },
       { timeout: 3000 },
     );
 
-    expect(screen.getAllByText('AI Chat').length).toBeGreaterThan(0);
-      expect(screen.getByText('Settings')).toBeInTheDocument();
-      expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
+    expect(screen.getByText('Settings')).toBeInTheDocument();
+    expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
     },
     30_000,
   );

As per coding guidelines: "Use 2-space indentation (spaces, not tabs)".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(screen.getAllByText('AI Chat').length).toBeGreaterThan(0);
expect(screen.getByText('Settings')).toBeInTheDocument();
expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
},
30_000,
);
expect(screen.getByText('Settings')).toBeInTheDocument();
expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
},
30_000,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/tests/components/dashboard/config-editor-autosave.test.tsx` around lines
174 - 179, The test contains a redundant assertion and wrong indentation: remove
the duplicated expect(screen.getAllByText('AI Chat').length).toBeGreaterThan(0)
that repeats an earlier check inside the same waitFor, and fix the indentation
of the remaining lines inside the waitFor block to use 2 spaces (not 6), keeping
the expect(screen.getByText('Settings')).toBeInTheDocument() and
expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0) assertions
unchanged; locate the assertions within the waitFor callback in
web/tests/components/dashboard/config-editor-autosave.test.tsx to apply these
edits.

Comment on lines +203 to +207
expect(screen.getAllByText('Welcome Messages').length).toBeGreaterThan(0);
expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
},
30_000,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same indentation and redundancy issues as above.

Lines 203-204 have extra indentation, and line 203 duplicates the waitFor assertion from line 198.

🛠️ Proposed fix
     await waitFor(
       () => {
         expect(screen.getAllByText('Welcome Messages').length).toBeGreaterThan(0);
       },
       { timeout: 3000 },
     );
 
-      expect(screen.getAllByText('Welcome Messages').length).toBeGreaterThan(0);
-      expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
     },
     30_000,
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(screen.getAllByText('Welcome Messages').length).toBeGreaterThan(0);
expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
},
30_000,
);
expect(screen.getAllByText('Save Changes').length).toBeGreaterThan(0);
},
30_000,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/tests/components/dashboard/config-editor-autosave.test.tsx` around lines
203 - 207, Remove the duplicated waitFor assertion and fix the extra indentation
in the test block: eliminate the redundant expect(screen.getAllByText('Welcome
Messages').length).toBeGreaterThan(0) that repeats the earlier assertion, and
unindent the remaining lines so the expect calls inside the waitFor callback
align properly; locate the waitFor usage and the expect(...) assertions in the
test function in config-editor-autosave.test.tsx to make these edits.

# Conflicts:
#	tests/modules/ai.test.js
#	web/src/components/dashboard/config-editor.tsx
#	web/tests/components/dashboard/config-editor-autosave.test.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
web/tests/api/xp-route.test.ts (1)

59-59: ⚠️ Potential issue | 🟡 Minor

Use a valid Discord snowflake in the token mock to exercise real behavior.

'moderator-1' does not match the Discord snowflake pattern (^\d{17,20}$) that the bot API middleware validates. This test passes locally but doesn't verify the actual cross-service contract—the bot API would reject or ignore this value.

Suggested fix
-    mockGetToken.mockResolvedValue({ id: 'moderator-1' });
+    mockGetToken.mockResolvedValue({ id: '123456789012345678' });

And update the assertion at lines 186-187:

     expect(mockProxyToBotApi.mock.calls[0][4].headers).toMatchObject({
       'Content-Type': 'application/json',
-      'x-discord-user-id': 'moderator-1',
+      'x-discord-user-id': '123456789012345678',
     });

Also applies to: 180-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/tests/api/xp-route.test.ts` at line 59, Replace the invalid token mock
value 'moderator-1' used in mockGetToken.mockResolvedValue with a valid Discord
snowflake string (e.g., a 17–20 digit numeric string like '12345678901234567')
so the bot API middleware's regex validation is exercised; update any related
assertions that expect behaviors tied to the mock id (the assertions around the
test that reference the mocked moderator id—previously at the assertion block
for lines ~186-187 and the similar block at ~180-188) to use the new numeric
snowflake value (referencing mockGetToken and the test's expectation/assert
calls) so the test verifies the real cross-service contract.
web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts (1)

106-111: ⚠️ Potential issue | 🟠 Major

Remove the token.sub fallback; it is not a Discord snowflake and cannot be validated by the bot API.

The fallback to token.sub is intentionally tested (line 190 of web/tests/api/xp-route.test.ts), but the test mocks the bot API and does not validate production behavior. In reality, token.sub (a NextAuth internal identifier like 'nextauth-sub-only') does not match the DISCORD_SNOWFLAKE_PATTERN (/^\d{17,20}$/) enforced by the bot API. When sent as the x-discord-user-id header, validation fails silently at the bot API level: req.user is not set, and the XP adjustment request proceeds without proper user context.

Additionally, refactor the nested ternary for clarity:

Suggested fix: fail closed when token.id is unavailable
   const token = await getToken({ req: request });
-  const requesterId =
-    typeof token?.id === 'string' ? token.id : typeof token?.sub === 'string' ? token.sub : null;
+  const requesterId = typeof token?.id === 'string' ? token.id : null;
   if (!requesterId) {
     return NextResponse.json({ error: 'Unable to determine Discord user id' }, { status: 401 });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/guilds/`[guildId]/members/[userId]/xp/route.ts around lines
106 - 111, The code currently falls back to token.sub when determining
requesterId; remove that fallback and require token.id only (use getToken
result), so if token?.id is not a string the handler returns the 401 via
NextResponse.json immediately; replace the nested ternary that sets requesterId
with a clear if-check that assigns requesterId = token?.id when it's a string
and otherwise returns the 401 error response (update the block around getToken,
requesterId and the existing NextResponse.json call accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/ui/embed-builder.tsx`:
- Around line 566-577: The conditional expressions inside baseConfig's fields
mapping use negated/indirect checks (patch.name !== undefined and patch.value
!== undefined); replace them with positive property-existence checks (for
example use 'name' in patch and 'value' in patch) so the fields.map callback
explicitly applies { name: '' } when the patch contains name and { value: '' }
when it contains value; update the mapping that constructs baseConfig (the
fields.map block referencing patch, index, and value) to use those positive
conditions for clarity.

In `@web/tests/api/guilds.test.ts`:
- Around line 149-277: Tests ("augments guilds with bot-evaluated access levels
when bot api is configured", "ignores unknown access values from the bot api",
"batches guild access lookups to avoid exceeding the 100-guild API cap") use
double quotes; update string literals in these test cases (and optionally the
whole file) to use single quotes to match project style, e.g. change
process.env.BOT_API_SECRET, URLs passed to mockGetBotApiBaseUrl, the mocked
fetch responses, object property string values, and any string constants passed
to GET(createMockRequest()) or used in expect calls; run the repo formatter or
ESLint auto-fix (prettier/eslint --fix) after editing to ensure consistent
single-quote usage across the file.

In `@web/tests/api/xp-route.test.ts`:
- Around line 190-200: The test asserts that when mockGetToken returns only {
sub } the route should succeed and proxy to bot API, but per the route fix it
should reject such requests; update the test in xp-route.test.ts (the 'falls
back to token.sub when token.id is unavailable' case) to expect a 401 response
instead of 200, assert that mockProxyToBotApi is not called (or not called with
a proxied header), and remove or change the header assertion against
mockProxyToBotApi.mock.calls[0][4].headers; keep the use of mockGetToken, POST,
makeRequest, makeParams and mockProxyToBotApi to locate and modify this test.

---

Duplicate comments:
In `@web/src/app/api/guilds/`[guildId]/members/[userId]/xp/route.ts:
- Around line 106-111: The code currently falls back to token.sub when
determining requesterId; remove that fallback and require token.id only (use
getToken result), so if token?.id is not a string the handler returns the 401
via NextResponse.json immediately; replace the nested ternary that sets
requesterId with a clear if-check that assigns requesterId = token?.id when it's
a string and otherwise returns the 401 error response (update the block around
getToken, requesterId and the existing NextResponse.json call accordingly).

In `@web/tests/api/xp-route.test.ts`:
- Line 59: Replace the invalid token mock value 'moderator-1' used in
mockGetToken.mockResolvedValue with a valid Discord snowflake string (e.g., a
17–20 digit numeric string like '12345678901234567') so the bot API middleware's
regex validation is exercised; update any related assertions that expect
behaviors tied to the mock id (the assertions around the test that reference the
mocked moderator id—previously at the assertion block for lines ~186-187 and the
similar block at ~180-188) to use the new numeric snowflake value (referencing
mockGetToken and the test's expectation/assert calls) so the test verifies the
real cross-service contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fa904439-416d-46fa-b0f0-57a56f9aed6e

📥 Commits

Reviewing files that changed from the base of the PR and between 1574422 and 4affbea.

📒 Files selected for processing (8)
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/route.ts
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/src/components/ui/embed-builder.tsx
  • web/tests/api/guilds.test.ts
  • web/tests/api/xp-route.test.ts
  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/tests/components/ui/embed-builder.test.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/tests/components/ui/embed-builder.test.tsx
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/route.ts
  • web/tests/api/guilds.test.ts
  • web/src/components/ui/embed-builder.tsx
web/tests/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

web/tests/**/*.test.{ts,tsx}: Write web dashboard tests using Vitest 4 with the jsdom environment and React Testing Library, matching the web/src/ structure
Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Files:

  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/tests/components/ui/embed-builder.test.tsx
  • web/tests/api/xp-route.test.ts
  • web/tests/api/guilds.test.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: ESM only - do not use CommonJS modules
Use src/logger.js; do not use console.*

Files:

  • web/tests/components/ui/discord-markdown-editor.test.tsx
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/tests/components/ui/embed-builder.test.tsx
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/route.ts
  • web/tests/api/guilds.test.ts
  • web/src/components/ui/embed-builder.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use console.* methods in web dashboard code; use appropriate logging mechanisms for React applications

Files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/components/ui/discord-markdown-editor.tsx
  • web/src/app/api/guilds/route.ts
  • web/src/components/ui/embed-builder.tsx
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use parameterized SQL only; do not use string concatenation for SQL queries

Files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/route.ts
  • web/tests/api/guilds.test.ts
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.693Z
Learning: Applies to src/**/*.{js,ts} : Community features should be gated behind config.<feature>.enabled. Moderation commands are the exception.
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/route.ts
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/route.ts
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/app/api/**/route.ts : Include guildId in signed WebSocket ticket payload when issuing tickets from dashboard endpoints

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/route.ts
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/route.ts
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/api/ws/*.js : Bind audit-log WebSocket auth tickets to guild context by including nonce.expiry.guildId.hmac in ticket validation

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/tests/api/xp-route.test.ts
  • web/src/app/api/guilds/route.ts
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/ws/**/*.js : Enforce tenant scoping on WebSocket handlers: reject mismatched guildId in filters and require entry.guild_id === ws.guildId for broadcast matching

Applied to files:

  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/api/guilds/route.ts
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to **/*.{js,ts,tsx} : Maintain a maximum line width of 100 characters

Applied to files:

  • web/src/components/ui/discord-markdown-editor.tsx
  • web/src/components/ui/embed-builder.tsx
📚 Learning: 2026-03-26T00:04:14.693Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.693Z
Learning: Applies to src/**/*.{js,ts} : Community features should be gated behind config.<feature>.enabled. Moderation commands are the exception.

Applied to files:

  • web/src/app/api/guilds/route.ts
📚 Learning: 2026-03-11T17:18:17.626Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T17:18:17.626Z
Learning: Applies to src/**/{startup,command-register,reload}*.{js,ts} : Remove process.env.GUILD_ID runtime reads from bot startup and reload command registration

Applied to files:

  • web/tests/api/guilds.test.ts
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/pages/dashboard/**/*.{ts,tsx} : Use shared title helpers from web/src/lib/page-titles.ts for setting browser titles in dashboard pages

Applied to files:

  • web/src/components/ui/embed-builder.tsx
📚 Learning: 2026-03-26T00:04:14.693Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.693Z
Learning: Applies to src/**/*.{js,ts} : Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls

Applied to files:

  • web/src/components/ui/embed-builder.tsx
🪛 GitHub Check: SonarCloud Code Analysis
web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts

[warning] 108-108: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ1RaQ14PG_Wd_Vqn3hV&open=AZ1RaQ14PG_Wd_Vqn3hV&pullRequest=360

web/src/app/api/guilds/route.ts

[warning] 105-105: Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nLibevx0stJtcbBHj&open=AZ0nLibevx0stJtcbBHj&pullRequest=360

web/src/components/ui/embed-builder.tsx

[warning] 573-573: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ1RaQyrPG_Wd_Vqn3hU&open=AZ1RaQyrPG_Wd_Vqn3hU&pullRequest=360


[warning] 572-572: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ1RaQyrPG_Wd_Vqn3hT&open=AZ1RaQyrPG_Wd_Vqn3hT&pullRequest=360

🔇 Additional comments (22)
web/src/components/ui/embed-builder.tsx (4)

344-346: LGTM!

Clean helper that correctly handles the edge case where available <= 0 by returning an empty string.


549-556: LGTM! This addresses the prior review concern about emitting configs that exceed Discord's 6,000-character embed cap.

The gate correctly prevents onChange from receiving over-limit configs. Users still get visual feedback via the CharCount indicators.


602-614: LGTM!

Clean delegation to the new commit and commitFieldPatch functions, with correct dependency arrays.


616-642: LGTM!

Field array operations correctly routed through commit() for consistency, even though removeField and moveField can never increase the total character count.

web/tests/components/ui/embed-builder.test.tsx (2)

24-47: LGTM!

Solid test helper that wraps EmbedBuilder in a controlled component, enabling assertions on both the onChange mock and the actual emitted state. This pattern correctly handles React's state updates during user interactions.


367-399: Good regression coverage for character clamping.

The test cases correctly verify the boundary conditions:

  • Line 371-372: 4096 + 1900 = 5996, leaving exactly 4 characters for the title → 'abcd'
  • Line 387-389: 256 + 4096 + 1647 = 5999, leaving 1 character for the field name → 'a'
web/src/components/ui/discord-markdown-editor.tsx (3)

65-83: LGTM!

Clean implementation that truncates text and clamps all selection/cursor indices to stay within bounds. The early return for compliant results avoids unnecessary object creation.


227-272: LGTM! This addresses the prior review concern about enforcing maxLength on toolbar and variable insertion edits.

Both applyAction and insertVariable now route results through clampEditResult before calling onChange, ensuring programmatic edits respect the character limit. The dependency arrays are correctly updated to include maxLength.


379-389: LGTM!

The native maxLength attribute is now set on the textarea, providing browser-level enforcement for direct typing alongside the programmatic clamping for toolbar actions.

web/tests/components/ui/discord-markdown-editor.test.tsx (1)

226-265: LGTM! Good coverage for the new maxLength enforcement.

The tests correctly verify:

  • Native maxlength attribute propagation
  • Toolbar edits clamped mid-syntax ("**hell" for maxLength={6})
  • Variable insertion clamped mid-token ("hello{{u" for maxLength={8})

Note: The truncation can produce syntactically invalid markdown (unclosed markers), but this is acceptable given the hard limit enforcement takes priority over syntax validity.

web/src/app/api/guilds/route.ts (3)

104-106: Extract the nested ternary for readability.

The nested ternary for userId derivation was previously flagged by SonarCloud and in prior review. Consider extracting to an explicit conditional block.

♻️ Suggested refactor
-    const userId =
-      typeof token.id === 'string' ? token.id : typeof token.sub === 'string' ? token.sub : '';
+    let userId = '';
+    if (typeof token.id === 'string') {
+      userId = token.id;
+    } else if (typeof token.sub === 'string') {
+      userId = token.sub;
+    }
     const guildsWithAccess = userId ? await applyAccessLevels(guilds, userId, signal) : guilds;

12-13: LGTM! Access level validation constants are well-defined.

The VALID_ACCESS_LEVELS Set properly constrains accepted values, addressing the schema drift concern. The batch size constant is appropriately scoped.


15-87: LGTM! Well-structured access level augmentation with proper validation and error handling.

The implementation correctly:

  • Returns early when config is missing or no bot-present guilds exist
  • Batches requests to respect the 100-guild API cap
  • Validates each entry against VALID_ACCESS_LEVELS before mapping
  • Falls back gracefully to original guilds on any error
web/tests/api/guilds.test.ts (5)

25-28: LGTM! Mock setup follows established pattern.

The mockGetBotApiBaseUrl wrapper pattern correctly allows per-test return value configuration, consistent with other mocks in this file.


38-67: LGTM! Comprehensive setup and teardown prevents test pollution.

The test now properly saves and restores BOT_API_URL, BOT_API_SECRET, globalThis.fetch, and NEXTAUTH_SECRET, addressing the prior concern about globals leaking between tests.


149-193: LGTM! Thorough test for access level augmentation.

The test correctly verifies:

  • Bot API is called when BOT_API_SECRET and base URL are configured
  • Response includes the augmented access value from the bot API
  • Request includes the x-api-secret header

195-234: LGTM! Good edge case coverage for invalid access values.

This test validates that unknown access values from the bot API are filtered out and the original guild access is preserved, ensuring the VALID_ACCESS_LEVELS check works correctly.


236-277: LGTM! Effective batching verification.

The test creates 205 guilds and correctly asserts that:

  • fetch is called exactly 3 times
  • The guildIds query parameter sizes are [100, 100, 5]

This validates the MAX_ACCESS_LOOKUP_GUILDS chunking logic in applyAccessLevels.

web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts (2)

1-9: LGTM: Authorization downgrade to moderator level is appropriate.

The switch from authorizeGuildAdmin to authorizeGuildModerator correctly aligns with the PR objective to allow moderators (not just admins) to adjust member XP. The relevant context snippet confirms authorizeGuildModerator passes 'moderator' as the required access level, which admins and bot-owners also satisfy via superset logic.

Also applies to: 39-40


113-120: LGTM: Proxy call correctly forwards the user ID header.

The proxyToBotApi function (per context snippet 3) correctly spreads caller-supplied headers before adding the auth secret, so x-discord-user-id will be forwarded to the bot API.

web/tests/api/xp-route.test.ts (2)

6-18: LGTM: Mock setup correctly mirrors the route changes.

The hoisted mocks are properly updated to reflect the authorization change from admin to moderator, and the getToken mock is correctly wired.

Also applies to: 20-29


202-209: LGTM: Correctly verifies 401 when no user ID can be determined.

This test properly validates the fail-closed behavior when authentication data is missing.

Signed-off-by: MohsinCoding <91380732+MohsinCoding@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 3, 2026 03:43
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 3, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
3 Security Hotspots
7.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@BillChirico BillChirico merged commit 8012ee7 into main Apr 3, 2026
12 of 18 checks passed
@BillChirico BillChirico deleted the mohsin/volvox-bot-perms branch April 3, 2026 03:46
@github-project-automation github-project-automation bot moved this from In Review to Done in Volvox.Bot Apr 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 55 out of 55 changed files in this pull request and generated 2 comments.

Comment on lines 261 to 268
@@ -244,7 +264,7 @@ export function DiscordMarkdownEditor({
if (!textarea) return;

const cursor = textarea.selectionStart;
const result = insertAtCursor(value, cursor, `${varOpen}${varName}${varClose}`);
const result = clampEditResult(insertAtCursor(value, cursor, `{{${varName}}}`), maxLength);

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

insertVariable ignores the variableDelimiters prop and always inserts {{...}} (hard-coded), which breaks custom delimiter configurations and makes the UI inconsistent with the prop contract. Use the [varOpen, varClose] values when building the inserted variable string so toolbar insertion matches the configured delimiters.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +12
/**
* Repair migration for audit_logs schema drift.
*
* Background:
* `013_audit_log.cjs` now creates `audit_logs.user_tag`, but some databases
* already had an older `audit_logs` table created before the `user_tag`
* column existed. Because `013_audit_log.cjs` uses `ifNotExists`, those
* existing tables do not receive the new column automatically.
*
* Purpose:
* Preserve the historical `014_*` slot already recorded in some databases
* and backfill the missing column/index when needed.
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This migration’s header comment references 013_audit_log.cjs, but the repository’s migrations directory does not contain that migration and the base schema migration already defines audit_logs.user_tag and idx_audit_logs_guild_user (migrations/001_initial-schema.cjs). Please update the comment to reflect the actual migration history in this repo so future debugging doesn’t chase a non-existent migration.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: api REST/API routes and contracts scope: backend Bot/runtime/backend logic scope: dashboard Web dashboard scope: security Security vulnerability or hardening size:XL type: feature New feature

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants