Skip to content

chore: tech debt — skeletons, error boundaries, toasts, validation, rate limit cleanup#257

Merged
BillChirico merged 18 commits intomainfrom
chore/issue-144
Mar 7, 2026
Merged

chore: tech debt — skeletons, error boundaries, toasts, validation, rate limit cleanup#257
BillChirico merged 18 commits intomainfrom
chore/issue-144

Conversation

@BillChirico
Copy link
Collaborator

Summary

Selected improvements from #144 tech debt backlog.

Changes

  • Loading skeletons — animated placeholders on audit-log, tickets, conversations, temp-roles, role/channel selectors
  • Error boundaries — reusable ErrorBoundary component with retry button; wraps analytics and moderation stats
  • Toast notifications — sonner toasts for XP adjustment and CSV export success/error
  • XP proxy validation — validates payload shape in Next.js proxy before forwarding to backend
  • Rate limit cleanup — exported startRateLimitCleanup(), added stale-sweep test with fake timers

Partially addresses #144

@BillChirico BillChirico added this to the v0.1.0 - "Big Boy MVP" milestone Mar 7, 2026
Copilot AI review requested due to automatic review settings March 7, 2026 16:23
@BillChirico BillChirico added priority: medium Medium priority scope: ops CI/CD, deploy, monitoring, operations type: task Discrete implementation task labels Mar 7, 2026
@github-project-automation github-project-automation bot moved this to Backlog in Volvox.Bot Mar 7, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 7, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added toast notifications for user feedback on XP adjustments and exports
    • Introduced error boundary to gracefully handle dashboard errors
  • UI/UX Improvements

    • Replaced loading spinners with skeleton placeholders across tables for better perceived performance
    • Enhanced mobile responsiveness with adaptive grid layouts on dashboard pages
    • Improved loading state visual feedback
  • Bug Fixes

    • Added comprehensive validation for XP adjustment API requests to ensure data integrity

Walkthrough

This PR adds planning documentation, refactors Discord interaction handlers into modular components, introduces Zustand stores for members and moderation data management, improves UI with skeleton loading states and error boundaries, enhances mobile responsiveness through grid layout adjustments, and includes infrastructure updates to rate limiting exports and API validation.

Changes

Cohort / File(s) Summary
Documentation & Planning
TASK2.md
Adds comprehensive planning document outlining eight implementation items, architectural notes, and operational workflow guidance without introducing functional code changes.
Database Migrations
migrations/012_placeholder.cjs
Adds no-op placeholder migration file documenting a sequence gap in the migration history.
API Documentation Comments
src/api/routes/conversations.js, src/api/routes/guilds.js, src/api/routes/members.js, src/api/utils/validateConfigPatch.js, src/logger.js
Adds TODO comments and explanatory notes on performance concerns, accuracy limitations, and future improvements without changing functional behavior.
Event Handler Refactoring
src/modules/events/interactionCreate.js
Extracts monolithic interaction handler implementations into dedicated, re-exported modules; public API surface remains unchanged.
Event Handler Modules
src/modules/handlers/pollHandler.js, src/modules/handlers/reviewHandler.js, src/modules/handlers/showcaseHandler.js, src/modules/handlers/ticketHandler.js, src/modules/handlers/welcomeOnboardingHandler.js, src/modules/handlers/challengeHandler.js, src/modules/handlers/reminderHandler.js
New modular handlers for Discord interactions (polls, reviews, showcases, tickets, challenges, reminders, welcome onboarding); each registers listeners with config gating and error handling.
Rate Limiting Infrastructure
src/modules/rateLimit.js, tests/modules/rateLimit.test.js
Exports startRateLimitCleanup for public use and testing; adds test suite for interval-based stale-entry cleanup behavior with fake timers.
Frontend State Management
web/src/stores/members-store.ts, web/src/stores/moderation-store.ts
Introduces two Zustand stores to centralize state management for members data (pagination, sorting, search, filters) and moderation data (user search, history lookup, action filtering).
Dashboard Page Integrations
web/src/app/dashboard/members/page.tsx, web/src/app/dashboard/moderation/page.tsx
Refactors to use new Zustand stores; replaces local state with centralized, observable store-driven state and actions.
UI Skeleton Loading States
web/src/app/dashboard/audit-log/page.tsx, web/src/app/dashboard/conversations/page.tsx, web/src/app/dashboard/tickets/page.tsx, web/src/app/dashboard/temp-roles/page.tsx, web/src/components/ui/channel-selector.tsx, web/src/components/ui/role-selector.tsx
Adds Skeleton components to provide loading placeholders matching table/dropdown layouts; replaces spinners with skeleton UI during data load.
Error Handling & Dashboard Improvements
web/src/components/ui/error-boundary.tsx, web/src/app/dashboard/page.tsx, web/src/app/dashboard/members/[userId]/page.tsx
Adds ErrorBoundary component for graceful error UI rendering; integrates it in dashboard and member detail pages; adds toast notifications for XP adjustments and exports.
Mobile Responsiveness
web/src/components/dashboard/config-editor.tsx, web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx, web/src/components/dashboard/config-sections/TriageSection.tsx, web/src/components/dashboard/ai-feedback-stats.tsx
Updates grid layouts from fixed multi-column to responsive single-column defaults with small-screen breakpoint expansions (e.g., grid-cols-1 sm:grid-cols-2).
API Validation & Dependencies
web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts, web/package.json
Adds comprehensive payload validation for XP adjustment endpoint; adds zustand v5.0.11 dependency for state management.

Possibly related PRs

  • PR #175: Shares direct code-level overlap in ChannelSelector/RoleSelector components, config-editor and moderation UI refactoring, and web/package.json zustand integration.
  • PR #118: Complements this PR by adding event-focused tests that specifically exercise the newly modularized interaction handler modules introduced here (poll, review, showcase, ticket, challenge handlers).
  • PR #240: Directly related refactoring effort—both modularize bot interaction/event handling by extracting handlers into separate modules and wiring up registration functions (registerPollButtonHandler, registerReviewClaimHandler, etc.).
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.67% 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 accurately summarizes the main changes: tech debt improvements including skeletons, error boundaries, toasts, validation, and rate limit cleanup.
Description check ✅ Passed The description provides relevant context, lists the key changes, and references the related issue #144, aligning well with the changeset.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/issue-144
  • 🛠️ 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.

@coveralls
Copy link

coveralls commented Mar 7, 2026

Coverage Status

coverage: 87.549% (+0.07%) from 87.484%
when pulling c444978 on chore/issue-144
into 4d7743e on main.

Copy link
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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps
Copy link

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR delivers solid tech debt improvements: animated loading skeletons across audit-log, tickets, conversations, temp-roles, role/channel selectors; a reusable ErrorBoundary component wrapping analytics and moderation stats; sonner toast notifications for XP adjustments and CSV exports; strong validation on the XP proxy route (enforces finite integers); and a startRateLimitCleanup() export with a fake-timer stale-sweep test. The monolithic interactionCreate.js (525 lines) is cleanly split into focused handler modules under src/modules/handlers/, and two new Zustand stores (members-store, moderation-store) replace page-local useState for members and moderation pages. Mobile responsiveness is improved throughout the config editor.

Two issues to address before merging:

  • TASK2.md should be removed — this is an AI-agent task file with shell commands and GitHub API instructions; it was marked "Do NOT push" and is not project documentation.
  • Handler filename collisionpollHandler.js, reminderHandler.js, reviewHandler.js, and ticketHandler.js in handlers/ share identical base names with the existing modules they import from, creating IDE and search confusion.

Confidence Score: 4/5

  • Safe to merge after removing TASK2.md and renaming handler files for clarity.
  • The functional changes are well-implemented: validation, tests, skeletons, and error boundaries are all correct and properly verified. Both flagged issues are organizational/hygiene — an accidentally committed agent task file and a naming convention collision that affects developer experience but not runtime behavior. Both are straightforward to fix.
  • TASK2.md (remove), src/modules/handlers/ (rename handler files to avoid collision with parent module names)

Sequence Diagram

sequenceDiagram
    participant Client as Browser Client
    participant Proxy as Next.js XP Proxy<br/>/api/guilds/[guildId]/members/[userId]/xp
    participant Backend as Express Backend

    Client->>Proxy: POST { amount, reason?, ...extras }
    Proxy->>Proxy: Verify x-api-secret header
    alt Invalid secret
        Proxy-->>Client: 401 Unauthorized
    end
    Proxy->>Proxy: Parse JSON body (unknown)
    alt Not a plain object
        Proxy-->>Client: 400 Body must be a JSON object
    end
    Proxy->>Proxy: Check 'amount' present
    alt Missing amount
        Proxy-->>Client: 400 Missing required field: amount
    end
    Proxy->>Proxy: Validate finite integer
    alt Not finite integer
        Proxy-->>Client: 400 Field "amount" must be a finite integer
    end
    Proxy->>Proxy: Validate optional 'reason' string
    alt reason present but not string
        Proxy-->>Client: 400 Field "reason" must be a string
    end
    Proxy->>Proxy: Strip unknown keys → { amount, reason? }
    Proxy->>Backend: POST sanitized payload
    Backend-->>Proxy: { xp: newTotal }
    Proxy-->>Client: 200 { xp: newTotal }
    Client->>Client: toast.success("XP adjusted")
Loading

Last reviewed commit: c444978

Copilot AI review requested due to automatic review settings March 7, 2026 16:41
Copy link
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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings March 7, 2026 16:46
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 7, 2026
@github-project-automation github-project-automation bot moved this from Backlog to In Review in Volvox.Bot Mar 7, 2026
Copy link
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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copy link
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: 11

Caution

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

⚠️ Outside diff range comments (2)
src/api/routes/guilds.js (1)

1188-1202: ⚠️ Potential issue | 🟠 Major

All-time metrics mixed with time-scoped analytics creates misleading data.

The userEngagement metrics (and similarly xpEconomy at lines 1210-1227) return all-time cumulative values, while the rest of the /guilds/:id/analytics endpoint respects the range query parameter (today, week, month, custom). This creates a significant inconsistency: when a consumer requests "last 7 days" analytics, they receive time-bounded message volume, AI usage, and channel activity, but totalMessagesSent, totalReactionsGiven, and XP stats reflect all-time values. The API response provides no indication that these metrics have different scoping, likely causing dashboards to misrepresent engagement trends.

Consider one of these approaches:

  1. Omit these metrics until proper time-series data is available (breaking change but prevents misleading data)
  2. Add a clear indicator in the response, e.g.:
    "userEngagement": {
      "scope": "all-time",
      "trackedUsers": 150,
      ...
    }
  3. Implement the time-series solution now by adding a message_events or user_activity_log table with timestamps, then aggregate within the requested range
Option 2: Add scope indicator
       userEngagement: userEngagementResult.rows[0]
         ? {
+            scope: 'all-time',
             trackedUsers: Number(userEngagementResult.rows[0].tracked_users || 0),
             totalMessagesSent: Number(userEngagementResult.rows[0].total_messages_sent || 0),

And similarly for xpEconomy:

       xpEconomy: xpEconomyResult.rows[0]
         ? {
+            scope: 'all-time',
             totalUsers: Number(xpEconomyResult.rows[0].total_users || 0),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/routes/guilds.js` around lines 1188 - 1202, The userEngagement and
xpEconomy objects returned by the /guilds/:id/analytics endpoint mix all-time
aggregates (queries against user_stats) with time-scoped metrics, so add an
explicit scope indicator to each impacted object instead of changing the data
source: update the code that builds the userEngagement and xpEconomy response
objects (look for the SQL that SELECTs from user_stats and the places
constructing userEngagement and xpEconomy) to include a "scope": "all-time"
field alongside trackedUsers/totalMessagesSent/totalReactionsGiven and XP
fields, and ensure any API docs/tests reflecting these objects are updated to
expect the new scope property so consumers can tell these metrics are not
time-bounded.
web/src/app/dashboard/tickets/page.tsx (1)

385-445: ⚠️ Potential issue | 🟡 Minor

Suppress the empty state when the request failed.

When error is set and there are no rows yet, this branch renders both the destructive alert and “No tickets found.” / “No tickets match your filters.” That makes a failed fetch look like a valid empty result.

Proposed fix
-          ) : (
+          ) : error ? null : (
             <div className="flex h-48 items-center justify-center rounded-lg border border-dashed">
               <p className="text-sm text-muted-foreground">
                 {statusFilter || debouncedSearch
                   ? 'No tickets match your filters.'
                   : 'No tickets found.'}
               </p>
             </div>
           )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/dashboard/tickets/page.tsx` around lines 385 - 445, The
empty-state branch is rendered even when the fetch failed (error is set),
causing the “No tickets…” message to show alongside the error alert; update the
rendering conditions so the final empty-state block only renders when there is
no error. Concretely, change the ternary that decides between TicketsSkeleton /
table / empty message to include an error check (use the existing loading,
tickets, and error symbols) so the empty message (the block that uses
statusFilter/debouncedSearch) is only shown when !error and tickets.length ===
0; keep TicketsSkeleton for loading and the table for tickets.length > 0 so the
destructive alert (rendered elsewhere when error is truthy) is not duplicated by
the empty state.
🤖 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/012_placeholder.cjs`:
- Around line 1-21: The placeholder migration doesn't resolve duplicate
migration IDs — instead rename the conflicting files
(004_performance_indexes.cjs and 004_voice_sessions.cjs) to unique, sequential
IDs so there are no duplicate "004_" prefixes: choose the next available numbers
in sequence, update the filenames accordingly (preserving the rest of each
filename and their exports.up/exports.down), remove this no-op placeholder if
you used 012 for one of them, and run your migration tooling to verify no
out-of-order/duplicate-ID errors remain.

In `@src/api/utils/validateConfigPatch.js`:
- Around line 83-87: Add deep per-key JSON Schema validation before accepting
config patches: create a JSON Schema registry keyed by top-level config sections
and integrate an AJV validator (or equivalent) into the patch acceptance flow in
validateConfigPatch.js so that, for each incoming patch path you first select
the matching top-level schema and run full schema.validate against the patch
object; keep validateSingleValue for existing type/range checks but reject
patches that fail the AJV schema validation and return clear validation errors.

In `@src/modules/handlers/pollHandler.js`:
- Around line 29-42: The logError call currently accesses err.message directly
which can throw if the caught value isn't an Error; update the error extraction
in the catch block around logError in poll vote handler to use a defensive
lookup such as err?.message || String(err) (or JSON.stringify for objects) and
pass that value into the error field, so replace the direct err.message usage in
the logError payload (referencing logError, interaction, and safeReply) with the
safe extraction.

In `@src/modules/handlers/reminderHandler.js`:
- Around line 37-49: The catch block in the reminder button handler uses
err.message directly which can throw if a non-Error (or null/undefined) was
thrown; update the catch in the Reminder button handler to defensively extract
an error message (e.g., check instanceof Error or coerce with String(err) /
fallback like 'unknown error') before calling logError, and ensure logError is
passed a stable string for the error field; keep the existing safeReply call
(safeReply(interaction, ...)) and the replied/deferred guard intact so the
user-facing fallback still runs when err is not an Error.

In `@src/modules/handlers/showcaseHandler.js`:
- Line 25: Change the feature-gate checks from explicit false comparisons to the
negation pattern used by other handlers: replace occurrences of
"guildConfig.showcase?.enabled === false" with "!guildConfig.showcase?.enabled"
(update both checks around the showcase handler, e.g., the checks referencing
guildConfig.showcase?.enabled at the start of the handler and later near the
secondary guard) so the showcase feature defaults to disabled when the config is
missing or undefined.

In `@TASK.md`:
- Around line 9-47: Add a blank line before and after each Markdown heading in
TASK.md to satisfy MD022 (e.g., ensure lines around "### 1. Loading skeletons
for dashboard pages", "### 2. Error boundaries for dashboard", "### 3. Toast
notifications for actions", "### 4. XP proxy route validation", and "### 5.
Stale rate limit cleanup" have an empty line above and below); update all other
headings in the file similarly for consistent spacing and re-run the markdown
linter to confirm the MD022 warning is resolved.

In `@tests/modules/rateLimit.test.js`:
- Line 361: The import of startRateLimitCleanup and stopRateLimitCleanup is
placed mid-file; move that import so it is grouped with the other imports from
rateLimit.js at the top of the test file, combine it into the existing rateLimit
import statement (so startRateLimitCleanup and stopRateLimitCleanup are imported
alongside other symbols from rateLimit.js), and delete the now-redundant import
line that currently appears after the first tests.

In `@web/src/app/api/guilds/`[guildId]/members/[userId]/xp/route.ts:
- Around line 40-72: The client-side validation in this route currently checks
payload, amount type, and reason but omits the backend's additional constraints
(reject zero and enforce magnitude <= 1_000_000), so update the validation
around payload.amount (the block that currently checks Number.isFinite and
Number.isInteger) to also return 400 when payload.amount === 0 and when
Math.abs(payload.amount) > 1_000_000, using NextResponse.json with clear error
messages consistent with the backend; ensure sanitized remains created only
after these checks and that body = JSON.stringify(sanitized) is unchanged.

In `@web/src/app/dashboard/members/page.tsx`:
- Around line 84-86: The hook currently doesn't clear persisted members when
revisiting the page with the same guild, so stale entries can remain visible if
the initial re-fetch fails; update the logic so that either onGuildChange is
invoked on mount or—preferably—modify fetchMembers to clear the stored members
when called with append: false (e.g., set members = [] / call resetAll at the
start of fetchMembers) so the UI doesn't show stale members while the fresh
request is in-flight; reference the fetchMembers function and the
resetAll/onGuildChange handlers to implement this change.

In `@web/src/app/dashboard/moderation/page.tsx`:
- Around line 87-101: The callbacks handleUserHistorySearch and
handleClearUserHistory mix store actions (setLookupUserId, setUserHistoryPage,
clearUserHistory) with hook state setters (setUserHistoryData,
setUserHistoryError), which splits user-history state between the store and the
useUserHistory hook; either consolidate or document that split. To fix: choose
one approach — (A) consolidate into the store by adding/using store actions like
setUserHistoryData and setUserHistoryError and replace the hook setters in
handleUserHistorySearch/handleClearUserHistory with those store actions, or (B)
keep fetched data in the hook and move UI state into the hook by exposing
setters for lookupUserId and userHistoryPage from useUserHistory and replace
store calls in the handlers; update imports/usages of
setLookupUserId/setUserHistoryPage/clearUserHistory or
setUserHistoryData/setUserHistoryError accordingly and add a short inline
comment above handleUserHistorySearch/handleClearUserHistory documenting the
chosen ownership model.

In `@web/src/stores/moderation-store.ts`:
- Line 7: The property actionFilter in moderation-store.ts is typed too loosely
as string; tighten it to a union type (e.g., type ActionFilter = 'all' | 'warn'
| 'ban' | 'mute' | ...) and use that type for the actionFilter property to
prevent invalid values and enable editor autocomplete; update the declaration
(actionFilter: ActionFilter), export the ActionFilter type if consumed
elsewhere, and adjust any places that set or read actionFilter (including
default values and setter functions) to use the new union members or a mapped
constant list.

---

Outside diff comments:
In `@src/api/routes/guilds.js`:
- Around line 1188-1202: The userEngagement and xpEconomy objects returned by
the /guilds/:id/analytics endpoint mix all-time aggregates (queries against
user_stats) with time-scoped metrics, so add an explicit scope indicator to each
impacted object instead of changing the data source: update the code that builds
the userEngagement and xpEconomy response objects (look for the SQL that SELECTs
from user_stats and the places constructing userEngagement and xpEconomy) to
include a "scope": "all-time" field alongside
trackedUsers/totalMessagesSent/totalReactionsGiven and XP fields, and ensure any
API docs/tests reflecting these objects are updated to expect the new scope
property so consumers can tell these metrics are not time-bounded.

In `@web/src/app/dashboard/tickets/page.tsx`:
- Around line 385-445: The empty-state branch is rendered even when the fetch
failed (error is set), causing the “No tickets…” message to show alongside the
error alert; update the rendering conditions so the final empty-state block only
renders when there is no error. Concretely, change the ternary that decides
between TicketsSkeleton / table / empty message to include an error check (use
the existing loading, tickets, and error symbols) so the empty message (the
block that uses statusFilter/debouncedSearch) is only shown when !error and
tickets.length === 0; keep TicketsSkeleton for loading and the table for
tickets.length > 0 so the destructive alert (rendered elsewhere when error is
truthy) is not duplicated by the empty state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5ff4d968-46fe-444f-8c88-1e27d9358ab9

📥 Commits

Reviewing files that changed from the base of the PR and between 89dd4c0 and 9a5b24a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (37)
  • TASK.md
  • TASK2.md
  • migrations/012_placeholder.cjs
  • src/api/routes/conversations.js
  • src/api/routes/guilds.js
  • src/api/routes/members.js
  • src/api/utils/validateConfigPatch.js
  • src/logger.js
  • src/modules/events/interactionCreate.js
  • src/modules/handlers/challengeHandler.js
  • src/modules/handlers/pollHandler.js
  • src/modules/handlers/reminderHandler.js
  • src/modules/handlers/reviewHandler.js
  • src/modules/handlers/showcaseHandler.js
  • src/modules/handlers/ticketHandler.js
  • src/modules/handlers/welcomeOnboardingHandler.js
  • src/modules/rateLimit.js
  • tests/modules/rateLimit.test.js
  • web/package.json
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/dashboard/audit-log/page.tsx
  • web/src/app/dashboard/conversations/page.tsx
  • web/src/app/dashboard/members/[userId]/page.tsx
  • web/src/app/dashboard/members/page.tsx
  • web/src/app/dashboard/moderation/page.tsx
  • web/src/app/dashboard/page.tsx
  • web/src/app/dashboard/temp-roles/page.tsx
  • web/src/app/dashboard/tickets/page.tsx
  • web/src/components/dashboard/ai-feedback-stats.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • web/src/components/dashboard/config-sections/TriageSection.tsx
  • web/src/components/ui/channel-selector.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/components/ui/role-selector.tsx
  • web/src/stores/members-store.ts
  • web/src/stores/moderation-store.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). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{js,cjs,mjs}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only with import/export syntax, never CommonJS except in migration files (.cjs)

Files:

  • src/api/utils/validateConfigPatch.js
  • src/api/routes/conversations.js
  • src/logger.js
  • src/modules/handlers/reviewHandler.js
  • src/modules/rateLimit.js
  • src/modules/handlers/welcomeOnboardingHandler.js
  • src/modules/handlers/challengeHandler.js
  • src/modules/handlers/showcaseHandler.js
  • src/modules/handlers/pollHandler.js
  • src/api/routes/guilds.js
  • src/modules/handlers/ticketHandler.js
  • src/modules/handlers/reminderHandler.js
  • migrations/012_placeholder.cjs
  • src/api/routes/members.js
  • tests/modules/rateLimit.test.js
  • src/modules/events/interactionCreate.js
**/*.{js,mjs,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,mjs,jsx,ts,tsx}: Use single quotes for strings in code, double quotes only allowed in JSON files
Always end statements with semicolons
Use 2-space indentation, enforced by Biome

Files:

  • src/api/utils/validateConfigPatch.js
  • web/src/stores/moderation-store.ts
  • src/api/routes/conversations.js
  • src/logger.js
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • src/modules/handlers/reviewHandler.js
  • src/modules/rateLimit.js
  • src/modules/handlers/welcomeOnboardingHandler.js
  • src/modules/handlers/challengeHandler.js
  • web/src/app/dashboard/members/[userId]/page.tsx
  • src/modules/handlers/showcaseHandler.js
  • web/src/components/ui/role-selector.tsx
  • web/src/app/dashboard/page.tsx
  • src/modules/handlers/pollHandler.js
  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • src/api/routes/guilds.js
  • src/modules/handlers/ticketHandler.js
  • web/src/components/dashboard/ai-feedback-stats.tsx
  • web/src/app/dashboard/conversations/page.tsx
  • src/modules/handlers/reminderHandler.js
  • web/src/app/dashboard/tickets/page.tsx
  • web/src/components/dashboard/config-sections/TriageSection.tsx
  • web/src/app/dashboard/temp-roles/page.tsx
  • src/api/routes/members.js
  • web/src/app/dashboard/moderation/page.tsx
  • web/src/stores/members-store.ts
  • web/src/components/ui/channel-selector.tsx
  • web/src/components/ui/error-boundary.tsx
  • tests/modules/rateLimit.test.js
  • src/modules/events/interactionCreate.js
  • web/src/app/dashboard/members/page.tsx
  • web/src/app/dashboard/audit-log/page.tsx
src/**/*.{js,mjs,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{js,mjs,jsx,ts,tsx}: Use src/logger.js Winston logger singleton, never use console.* methods
Use safe Discord message methods: safeReply(), safeSend(), safeEditReply() instead of direct Discord.js methods
Use parameterized SQL queries, never string interpolation for database queries

Files:

  • src/api/utils/validateConfigPatch.js
  • src/api/routes/conversations.js
  • src/logger.js
  • src/modules/handlers/reviewHandler.js
  • src/modules/rateLimit.js
  • src/modules/handlers/welcomeOnboardingHandler.js
  • src/modules/handlers/challengeHandler.js
  • src/modules/handlers/showcaseHandler.js
  • src/modules/handlers/pollHandler.js
  • src/api/routes/guilds.js
  • src/modules/handlers/ticketHandler.js
  • src/modules/handlers/reminderHandler.js
  • src/api/routes/members.js
  • src/modules/events/interactionCreate.js
{.env*,README.md,src/**/!(*.test).{js,ts}}

📄 CodeRabbit inference engine (CLAUDE.md)

Remove GUILD_ID from shared environment variables in production/deployment configurations; preserve dev-only guild-scoped deploy support via CLI flag --guild-id <guild_id>

Files:

  • src/api/utils/validateConfigPatch.js
  • src/api/routes/conversations.js
  • src/logger.js
  • src/modules/handlers/reviewHandler.js
  • src/modules/rateLimit.js
  • src/modules/handlers/welcomeOnboardingHandler.js
  • src/modules/handlers/challengeHandler.js
  • src/modules/handlers/showcaseHandler.js
  • src/modules/handlers/pollHandler.js
  • src/api/routes/guilds.js
  • src/modules/handlers/ticketHandler.js
  • src/modules/handlers/reminderHandler.js
  • src/api/routes/members.js
  • src/modules/events/interactionCreate.js
web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Next.js 16 web dashboard uses App Router with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Files:

  • web/src/stores/moderation-store.ts
  • web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts
  • web/src/app/dashboard/members/[userId]/page.tsx
  • web/src/components/ui/role-selector.tsx
  • web/src/app/dashboard/page.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • web/src/components/dashboard/ai-feedback-stats.tsx
  • web/src/app/dashboard/conversations/page.tsx
  • web/src/app/dashboard/tickets/page.tsx
  • web/src/components/dashboard/config-sections/TriageSection.tsx
  • web/src/app/dashboard/temp-roles/page.tsx
  • web/src/app/dashboard/moderation/page.tsx
  • web/src/stores/members-store.ts
  • web/src/components/ui/channel-selector.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/app/dashboard/members/page.tsx
  • web/src/app/dashboard/audit-log/page.tsx
src/api/routes/**/*.{js,mjs}

📄 CodeRabbit inference engine (AGENTS.md)

Create API routes with proper authentication middleware, mount in src/api/server.js, and document in OpenAPI spec

Files:

  • src/api/routes/conversations.js
  • src/api/routes/guilds.js
  • src/api/routes/members.js
src/modules/**/*.{js,mjs}

📄 CodeRabbit inference engine (AGENTS.md)

Create new modules for features with corresponding config sections in config.json and entries in SAFE_CONFIG_KEYS

Files:

  • src/modules/handlers/reviewHandler.js
  • src/modules/rateLimit.js
  • src/modules/handlers/welcomeOnboardingHandler.js
  • src/modules/handlers/challengeHandler.js
  • src/modules/handlers/showcaseHandler.js
  • src/modules/handlers/pollHandler.js
  • src/modules/handlers/ticketHandler.js
  • src/modules/handlers/reminderHandler.js
  • src/modules/events/interactionCreate.js
migrations/**/*.cjs

📄 CodeRabbit inference engine (AGENTS.md)

Use .cjs file extension for database migrations, use sequential migration numbering (001, 002, etc.) with node-pg-migrate

Files:

  • migrations/012_placeholder.cjs
migrations/[0-9]*_*.cjs

📄 CodeRabbit inference engine (CLAUDE.md)

Database migrations must be sequentially numbered with non-conflicting IDs; rename conflicting migration files to resolve out-of-order execution errors (e.g., migrations/004_*.cjsmigrations/007_*.cjs, etc.)

Files:

  • migrations/012_placeholder.cjs
tests/**/*.{js,mjs,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Maintain 80% code coverage threshold minimum, never lower this threshold

Files:

  • tests/modules/rateLimit.test.js
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-05T18:07:15.752Z
Learning: Applies to web/**/*.{ts,tsx} : Next.js 16 web dashboard uses App Router with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T15:34:56.495Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{js,jsx,ts,tsx} : Config editor save contract must maintain: global save/discard, diff-modal confirmation, per-section PATCH batching, and partial-failure behavior
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T15:34:56.495Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{js,jsx,ts,tsx} : Refactor config feature presentation to use reusable `SettingsFeatureCard` pattern with structure: header + master toggle + Basic/Advanced blocks
📚 Learning: 2026-03-05T18:07:15.752Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-05T18:07:15.752Z
Learning: Applies to src/**/*.{js,mjs,jsx,ts,tsx} : Use `src/logger.js` Winston logger singleton, never use `console.*` methods

Applied to files:

  • src/logger.js
📚 Learning: 2026-03-05T18:07:15.752Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-05T18:07:15.752Z
Learning: Applies to src/modules/**/*.{js,mjs} : Create new modules for features with corresponding config sections in `config.json` and entries in `SAFE_CONFIG_KEYS`

Applied to files:

  • src/modules/handlers/welcomeOnboardingHandler.js
📚 Learning: 2026-03-05T18:07:15.752Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-05T18:07:15.752Z
Learning: Applies to web/**/*.{ts,tsx} : Next.js 16 web dashboard uses App Router with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Applied to files:

  • web/src/app/dashboard/members/[userId]/page.tsx
  • web/src/components/dashboard/config-editor.tsx
  • TASK.md
  • TASK2.md
  • web/src/components/dashboard/config-sections/TriageSection.tsx
  • web/src/app/dashboard/moderation/page.tsx
  • web/src/app/dashboard/members/page.tsx
  • web/src/app/dashboard/audit-log/page.tsx
📚 Learning: 2026-03-07T15:34:56.495Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T15:34:56.495Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{js,jsx,ts,tsx} : Refactor config feature presentation to use reusable `SettingsFeatureCard` pattern with structure: header + master toggle + Basic/Advanced blocks

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • web/src/components/dashboard/config-sections/TriageSection.tsx
  • web/src/app/dashboard/moderation/page.tsx
  • web/src/app/dashboard/members/page.tsx
📚 Learning: 2026-03-07T15:34:56.495Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T15:34:56.495Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{js,jsx,ts,tsx} : Config editor must implement metadata-driven config search with cross-category quick jump, focus/scroll targeting, and auto-open advanced sections when search hits advanced controls

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • web/src/components/dashboard/config-sections/TriageSection.tsx
📚 Learning: 2026-03-07T15:34:56.495Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T15:34:56.495Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{js,jsx,ts,tsx} : Config editor save contract must maintain: global save/discard, diff-modal confirmation, per-section PATCH batching, and partial-failure behavior

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • TASK.md
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • TASK2.md
  • web/src/components/dashboard/config-sections/TriageSection.tsx
  • web/src/app/dashboard/moderation/page.tsx
📚 Learning: 2026-03-07T15:34:56.495Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T15:34:56.495Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{js,jsx,ts,tsx} : Web dashboard config editor must use category workspace navigation with categories: `AI & Automation`, `Onboarding & Growth`, `Moderation & Safety`, `Community Tools`, `Support & Integrations` located in `web/src/components/dashboard/config-workspace/`

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • TASK.md
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • web/src/components/dashboard/config-sections/TriageSection.tsx
📚 Learning: 2026-03-07T15:34:56.495Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T15:34:56.495Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.test.{js,jsx,ts,tsx} : Config editor tests must cover manual-save workspace behavior (not autosave assumptions), category switching, search functionality, and dirty badges

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • TASK.md
  • TASK2.md
📚 Learning: 2026-03-07T15:34:56.495Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-07T15:34:56.495Z
Learning: Applies to migrations/[0-9]*_*.cjs : Database migrations must be sequentially numbered with non-conflicting IDs; rename conflicting migration files to resolve out-of-order execution errors (e.g., `migrations/004_*.cjs` → `migrations/007_*.cjs`, etc.)

Applied to files:

  • migrations/012_placeholder.cjs
📚 Learning: 2026-03-05T18:07:15.752Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-05T18:07:15.752Z
Learning: Applies to migrations/**/*.cjs : Use `.cjs` file extension for database migrations, use sequential migration numbering (001, 002, etc.) with node-pg-migrate

Applied to files:

  • migrations/012_placeholder.cjs
📚 Learning: 2026-03-05T18:07:15.752Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-05T18:07:15.752Z
Learning: Applies to src/utils/discordCache.js : Cache Discord API responses for channels, roles, and members with auto-invalidation on config changes

Applied to files:

  • src/api/routes/members.js
🪛 LanguageTool
TASK2.md

[uncategorized] ~53-~53: The official name of this software platform is spelled with a capital “H”.
Context: ...oderabbitai style) + CodeRabbit - Check .github/ for reviewer config files - Check `CO...

(GITHUB)

🪛 markdownlint-cli2 (0.21.0)
TASK.md

[warning] 12-12: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 19-19: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 24-24: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 29-29: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 35-35: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 41-41: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 45-45: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🔇 Additional comments (39)
src/api/routes/conversations.js (1)

224-227: Helpful performance note here.

Documenting the current trigram-index path and the planned move to full-text search/keyset pagination makes the tradeoff explicit without changing behavior. Nice addition.

web/src/app/dashboard/temp-roles/page.tsx (1)

14-14: LGTM! Skeleton loading implementation is clean.

The skeleton placeholder provides good UX feedback during initial data fetch. The implementation correctly renders only when loading && rows.length === 0, avoiding flicker on subsequent fetches.

Also applies to: 203-213

web/src/components/ui/channel-selector.tsx (1)

28-28: LGTM! Skeleton loading in dropdown is well-structured.

The skeleton layout mirrors the actual channel item structure (icon + text), providing a faithful placeholder during loading. Good use of CommandEmpty for conditional rendering.

Also applies to: 322-329

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

317-323: LGTM! Helpful documentation of API limitations.

The TODO clearly documents the member search cache limitation and proposes concrete approaches (DB table with bulk sync, Discord HTTP API). This aids future maintainers in understanding the tradeoff.

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

88-88: LGTM! Responsive grid layout improvement.

The change from fixed grid-cols-2 to responsive grid-cols-1 sm:grid-cols-2 ensures form fields stack properly on mobile viewports, aligning with the project's mobile-responsive design goals.

Also applies to: 106-106, 122-122

src/logger.js (1)

9-14: LGTM! Useful documentation for browser compatibility.

The TODO clearly explains why this module cannot be used in browser contexts and provides a concrete path forward (thin shim in web/src/lib/logger.ts). This prevents future confusion when adding client-side logging.

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

290-290: LGTM! Consistent responsive layout update.

Matches the same responsive pattern applied in TriageSection.tsx, ensuring visual consistency across config sections on mobile devices.

web/src/components/dashboard/ai-feedback-stats.tsx (1)

93-93: LGTM! Responsive summary grid.

The three stat cards (Positive/Negative/Approval Rate) will now stack on narrow viewports and display as a row on sm: and larger, improving mobile readability.

web/src/app/dashboard/conversations/page.tsx (2)

27-68: LGTM! Well-structured skeleton component.

The ConversationsSkeleton follows the established pattern from other dashboard pages, with appropriate skeleton widths matching expected content sizes and responsive classes (hidden md:table-cell) matching the actual table structure.


371-444: Consistent loading and empty-state handling.

The conditional rendering logic properly:

  • Shows skeleton only on initial load (when loading && conversations.length === 0)
  • Preserves stale data during pagination/refresh
  • Provides context-aware empty state messages
src/modules/handlers/challengeHandler.js (1)

1-64: Clean handler implementation following established patterns.

The module correctly:

  • Gates on challenges?.enabled config
  • Validates the challenge index with Number.isNaN check
  • Uses the Winston logger (warn, error) instead of console methods
  • Uses safeReply for error responses
  • Checks reply/deferred state before attempting error reply

This aligns well with the other handler modules in this PR.

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

1352-1383: Good mobile-first responsive refactor.

Changing from fixed grid-cols-2 to grid-cols-1 gap-4 sm:grid-cols-2 ensures proper stacking on mobile devices while maintaining the multi-column layout on larger screens.


1384-1431: Consistent responsive pattern applied.

The 3-column grid for mute settings now properly collapses to single-column on mobile with grid-cols-1 gap-4 sm:grid-cols-3.

src/modules/handlers/welcomeOnboardingHandler.js (1)

24-75: Handler correctly passes guildConfig to delegate functions.

Per the context snippets from src/modules/welcomeOnboarding.js, both handleRulesAcceptButton and handleRoleMenuSelection expect (interaction, config) where config is the full guild config object. The handler correctly:

  • Passes guildConfig (not guildConfig.welcome)
  • Uses safeEditReply since the delegate functions call deferReply internally
  • Swallows secondary errors to prevent unhandled rejections
web/src/app/dashboard/audit-log/page.tsx (2)

66-107: Well-structured skeleton matching table layout.

The skeleton widths and responsive visibility classes (hidden md:table-cell, hidden lg:table-cell) correctly mirror the actual table structure, ensuring a smooth loading experience.


388-465: Consistent loading state handling.

The rendering logic follows the same pattern as other dashboard pages, showing skeleton on initial load while preserving data during pagination.

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

643-706: Good test coverage for the stale entry cleanup interval.

The test properly:

  • Manages fake/real timer transitions in beforeEach/afterEach
  • Restarts the cleanup interval under fake timers to capture it
  • Uses short retention windows (1s) for fast test execution
  • Advances time past both the cleanup interval (5 min) and retention window
  • Verifies the entry count drops to 0

This validates the interval-based sweep behavior documented in the PR objectives.

src/modules/handlers/reviewHandler.js (1)

18-48: Handler correctly passes only interaction to delegate.

Per the context snippet from src/modules/reviewHandler.js:148-158, handleReviewClaim only takes the interaction parameter (it acquires the pool internally). The handler correctly:

  • Gates on review?.enabled
  • Passes only interaction to handleReviewClaim
  • Follows the established error handling pattern
src/modules/handlers/showcaseHandler.js (2)

27-40: Lazy pool acquisition handles DB unavailability gracefully.

The dynamic import pattern for getPool() with try-catch properly handles cases where the database module isn't initialized, providing user feedback before early return.

Also applies to: 79-92


51-56: Smart reply method selection based on interaction state.

The logic to choose between safeEditReply and safeReply based on interaction.deferred || interaction.replied ensures proper error responses regardless of the delegate function's reply state.

Also applies to: 98-103

web/src/components/ui/error-boundary.tsx (1)

1-92: LGTM! Well-structured reusable error boundary.

The implementation follows React error boundary best practices with proper lifecycle methods, a clean default UI, and dev-mode debugging. The biome-ignore comment correctly justifies console.error usage in the browser context.

web/src/app/dashboard/page.tsx (1)

5-12: LGTM! Clean error boundary integration.

The custom title and description provide helpful context for analytics failures, and the suggestion to "select a different server" gives users an actionable recovery path.

src/modules/handlers/ticketHandler.js (3)

1-68: LGTM! Well-structured ticket open handler with proper error handling.

The handler correctly validates ticket system status, uses safe Discord methods, and logs errors appropriately. The empty catch block at line 37-39 is acceptable for ignoring reply failures when the system is disabled.


76-122: LGTM! Modal handler follows robust patterns.

Good use of deferReply before async operations, proper error logging with context, and nested try/catch for fallback error replies.


130-180: LGTM! Close handler with proper channel type validation.

The validation at lines 147-155 correctly handles both thread and channel modes as documented in the relevant code snippets from closeTicket. Error handling is comprehensive.

src/modules/events/interactionCreate.js (1)

1-21: LGTM! Clean refactor to modular handler exports.

The barrel re-export pattern maintains API compatibility while enabling better separation of concerns. Each handler type now lives in its own focused module.

web/src/stores/moderation-store.ts (1)

1-73: LGTM! Clean Zustand store implementation.

The store follows Zustand 5 conventions correctly with clear state/action separation. The resetOnGuildChange action comprehensively resets all state fields.

web/src/stores/members-store.ts (1)

1-75: LGTM! Well-organized members store.

The store is cleanly structured with extracted initialState for reset functionality. The appendMembers action correctly uses a functional update for immutable state modification.

web/package.json (1)

37-38: Zustand ^5.0.11 is the latest available version.

The dependency is correctly specified and compatible with the store implementation using the create<State>((set) => ...) pattern.

web/src/app/dashboard/moderation/page.tsx (4)

5-15: LGTM — clean import additions for store and ErrorBoundary.

The imports are properly organized, bringing in useCallback (already needed), ErrorBoundary for render error handling, and useModerationStore for centralized state management.


20-42: Good migration to store-driven state.

The destructured state and actions from useModerationStore cleanly replace what would have been multiple useState calls. The onGuildChange callback correctly depends on resetOnGuildChange, which is stable from Zustand.


142-144: ErrorBoundary correctly wraps render errors.

Good separation of concerns: the ErrorBoundary catches JavaScript exceptions during rendering, while statsError (passed as a prop) handles API-level errors. The title prop provides a user-friendly message.


159-162: Store callbacks correctly match CaseTableProps interface.

Per CaseTableProps (context snippet 3), onSortToggle and onClearFilters expect () => void signatures. The store's toggleSortDesc and clearFilters match exactly, making this a clean pass-through.

web/src/app/dashboard/members/page.tsx (6)

36-60: Good store integration with comprehensive state destructuring.

The large destructuring block clearly documents all state and actions used by this component. This makes the component's data dependencies explicit and aligns with the Zustand pattern of selecting specific slices.


69-75: Debounce effect correctly includes setDebouncedSearch in deps.

The dependency array properly includes both search (triggers the effect) and setDebouncedSearch (used in the effect). Since Zustand actions are stable references, this won't cause unnecessary re-runs.


136-165: Fetch logic correctly uses store actions with proper dependency array.

The appendMembers and setMembers actions are correctly used based on the append flag. All store setters used inside fetchMembers are included in the dependency array, and since Zustand actions are stable, this won't cause unnecessary re-creations.


181-192: Sort handler correctly resets pagination.

Calling resetPagination() when sort changes ensures fresh data loads from the beginning rather than trying to paginate with stale cursor state. The dependency array is complete.


206-217: Refresh handler correctly resets pagination before fetching.

Calling resetPagination() clears the members array and nextAfter cursor, then immediately fetches fresh data. This ensures no stale pagination state interferes with the refresh.


227-230: Clear search correctly bypasses debounce.

Setting both search and debouncedSearch to empty immediately triggers the fetch effect without waiting for the 300ms debounce. The debounce effect (lines 69-75) will also run, but since debouncedSearch is already '', Zustand's shallow equality check prevents a redundant state update and double fetch.

Comment on lines +1 to +21
/**
* Migration 012 — Placeholder (gap filler)
*
* There are two migrations numbered 004 in this project:
* - 004_performance_indexes.cjs
* - 004_voice_sessions.cjs
*
* This no-op migration occupies the 012 slot to make the numbering sequence
* explicit going forward. No schema changes are made here.
*/

/** @type {import('node-pg-migrate').MigrationBuilder} */
exports.up = async (pgm) => {
// No-op — this migration exists only to document the sequence gap
pgm.noTransaction();
};

/** @type {import('node-pg-migrate').MigrationBuilder} */
exports.down = async (_pgm) => {
// Nothing to undo
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This placeholder does not fix the duplicate migration IDs.

The header documents that the repo already has two 004_* migrations. Filling the 012 slot leaves that conflict in place, so node-pg-migrate can still hit out-of-order or duplicate-ID problems. The fix needs to renumber the conflicting migration file(s), not add a no-op gap marker.

As per coding guidelines, "Database migrations must be sequentially numbered with non-conflicting IDs; rename conflicting migration files to resolve out-of-order execution errors."

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

In `@migrations/012_placeholder.cjs` around lines 1 - 21, The placeholder
migration doesn't resolve duplicate migration IDs — instead rename the
conflicting files (004_performance_indexes.cjs and 004_voice_sessions.cjs) to
unique, sequential IDs so there are no duplicate "004_" prefixes: choose the
next available numbers in sequence, update the filenames accordingly (preserving
the rest of each filename and their exports.up/exports.down), remove this no-op
placeholder if you used 012 for one of them, and run your migration tooling to
verify no out-of-order/duplicate-ID errors remain.

Comment on lines +83 to +87
// TODO: Deep per-key schema validation — currently validateSingleValue only checks
// type/range for known paths. Unknown paths pass through without structural validation.
// For full coverage, add a per-key JSON schema registry (one schema per top-level config
// section) and run deep validation against it here before accepting the patch.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

TODO acknowledged — schema validation gap documented.

The comment clearly describes the current limitation (unknown paths bypass structural validation) and proposes a reasonable solution (per-key JSON schema registry). This is good documentation of technical debt.

Would you like me to help by:

  1. Opening an issue to track this schema validation enhancement, or
  2. Drafting an implementation approach using a library like ajv for JSON schema validation?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/utils/validateConfigPatch.js` around lines 83 - 87, Add deep per-key
JSON Schema validation before accepting config patches: create a JSON Schema
registry keyed by top-level config sections and integrate an AJV validator (or
equivalent) into the patch acceptance flow in validateConfigPatch.js so that,
for each incoming patch path you first select the matching top-level schema and
run full schema.validate against the patch object; keep validateSingleValue for
existing type/range checks but reject patches that fail the AJV schema
validation and return clear validation errors.

Comment on lines +29 to +42
} catch (err) {
logError('Poll vote handler failed', {
customId: interaction.customId,
userId: interaction.user?.id,
error: err.message,
});

// Try to send an ephemeral error if we haven't replied yet
if (!interaction.replied && !interaction.deferred) {
try {
await safeReply(interaction, {
content: '❌ Something went wrong processing your vote.',
ephemeral: true,
});
Copy link
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
rg -n -C2 'catch \(err\)|error:\s*err\.message' src/modules/handlers/pollHandler.js

Repository: VolvoxLLC/volvox-bot

Length of output: 323


🏁 Script executed:

#!/bin/bash
# Find all handler files to compare error handling patterns
fd -e js src/modules/handlers | head -20

Repository: VolvoxLLC/volvox-bot

Length of output: 449


🏁 Script executed:

#!/bin/bash
# Search for catch blocks and error logging patterns in all handlers
rg -n 'catch\s*\(\w+\)' src/modules/handlers/ -A 5 | head -100

Repository: VolvoxLLC/volvox-bot

Length of output: 6497


Use optional chaining or defensive check for error message access.

The direct access to err.message here can fail if the caught value is not an Error object. While some handlers in the codebase use err?.message for safety, JavaScript catch bindings are not guaranteed to be Error instances—they can be primitives, null, or other objects.

Suggested fix
    } catch (err) {
+      const errorMessage = err instanceof Error ? err.message : String(err);
       logError('Poll vote handler failed', {
         customId: interaction.customId,
         userId: interaction.user?.id,
-        error: err.message,
+        error: errorMessage,
       });
📝 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
} catch (err) {
logError('Poll vote handler failed', {
customId: interaction.customId,
userId: interaction.user?.id,
error: err.message,
});
// Try to send an ephemeral error if we haven't replied yet
if (!interaction.replied && !interaction.deferred) {
try {
await safeReply(interaction, {
content: '❌ Something went wrong processing your vote.',
ephemeral: true,
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
logError('Poll vote handler failed', {
customId: interaction.customId,
userId: interaction.user?.id,
error: errorMessage,
});
// Try to send an ephemeral error if we haven't replied yet
if (!interaction.replied && !interaction.deferred) {
try {
await safeReply(interaction, {
content: '❌ Something went wrong processing your vote.',
ephemeral: true,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/handlers/pollHandler.js` around lines 29 - 42, The logError call
currently accesses err.message directly which can throw if the caught value
isn't an Error; update the error extraction in the catch block around logError
in poll vote handler to use a defensive lookup such as err?.message ||
String(err) (or JSON.stringify for objects) and pass that value into the error
field, so replace the direct err.message usage in the logError payload
(referencing logError, interaction, and safeReply) with the safe extraction.

Comment on lines +37 to +49
} catch (err) {
logError('Reminder button handler failed', {
customId: interaction.customId,
userId: interaction.user?.id,
error: err.message,
});

if (!interaction.replied && !interaction.deferred) {
try {
await safeReply(interaction, {
content: '❌ Something went wrong processing your request.',
ephemeral: true,
});
Copy link
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
rg -n -C2 'catch \(err\)|error:\s*err\.message' src/modules/handlers/reminderHandler.js

Repository: VolvoxLLC/volvox-bot

Length of output: 336


🏁 Script executed:

# Check if TypeScript is used and search for catch block patterns
rg -n 'catch\s*\(\s*\w+\s*\)' src/modules/ -A2 | head -40

Repository: VolvoxLLC/volvox-bot

Length of output: 2149


🏁 Script executed:

# Check the handler structure and see if there's a shared pattern
git ls-files | grep -E '\.(ts|tsx|js)$' | head -5
cat tsconfig.json 2>/dev/null || echo "No tsconfig found"

Repository: VolvoxLLC/volvox-bot

Length of output: 212


🏁 Script executed:

# Look at logger implementation to understand what it expects
rg -n 'export.*logError|function logError' src/ -A5

Repository: VolvoxLLC/volvox-bot

Length of output: 46


🏁 Script executed:

# Find and read the logger file
fd logger.js src/ -x cat -n {}

Repository: VolvoxLLC/volvox-bot

Length of output: 11487


🏁 Script executed:

# Read the full reminderHandler.js to understand context
head -80 src/modules/handlers/reminderHandler.js | cat -n

Repository: VolvoxLLC/volvox-bot

Length of output: 2254


🏁 Script executed:

# Check if this handler is called in an event listener that might catch non-Error values
rg -n 'handleReminderSnooze|handleReminderDismiss|InteractionCreate' src/modules/handlers/reminderHandler.js -B5 -A2

Repository: VolvoxLLC/volvox-bot

Length of output: 949


Guard error logging against non-Error throws.

Line 41 accesses err.message without type checking. If the catch block receives null, undefined, or a non-Error value, this will fail silently and skip the user-facing fallback reply.

Fix
    } catch (err) {
+     const errorMessage = err instanceof Error ? err.message : String(err);
      logError('Reminder button handler failed', {
        customId: interaction.customId,
        userId: interaction.user?.id,
-       error: err.message,
+       error: errorMessage,
      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/handlers/reminderHandler.js` around lines 37 - 49, The catch
block in the reminder button handler uses err.message directly which can throw
if a non-Error (or null/undefined) was thrown; update the catch in the Reminder
button handler to defensively extract an error message (e.g., check instanceof
Error or coerce with String(err) / fallback like 'unknown error') before calling
logError, and ensure logError is passed a stable string for the error field;
keep the existing safeReply call (safeReply(interaction, ...)) and the
replied/deferred guard intact so the user-facing fallback still runs when err is
not an Error.


// Gate on showcase feature being enabled for this guild
const guildConfig = getConfig(interaction.guildId);
if (guildConfig.showcase?.enabled === false) return;
Copy link
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
# Check how other handlers gate on feature enablement to confirm consistency
rg -n '\.enabled\)? return' src/modules/handlers/ --type js

Repository: VolvoxLLC/volvox-bot

Length of output: 518


Fix inconsistent feature gate check—use negation pattern matching other handlers.

The showcase handler uses guildConfig.showcase?.enabled === false on lines 25 and 77, which enables the feature by default when config is missing or undefined. Every other handler in src/modules/handlers/ uses !guildConfig.feature?.enabled to default-disable the feature when config is missing. Change to if (!guildConfig.showcase?.enabled) return; for consistency.

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

In `@src/modules/handlers/showcaseHandler.js` at line 25, Change the feature-gate
checks from explicit false comparisons to the negation pattern used by other
handlers: replace occurrences of "guildConfig.showcase?.enabled === false" with
"!guildConfig.showcase?.enabled" (update both checks around the showcase
handler, e.g., the checks referencing guildConfig.showcase?.enabled at the start
of the handler and later near the secondary guard) so the showcase feature
defaults to disabled when the config is missing or undefined.

Comment on lines +40 to +72
const json: unknown = await request.json();

// Validate payload shape before forwarding (defense-in-depth)
if (typeof json !== 'object' || json === null || Array.isArray(json)) {
return NextResponse.json({ error: 'Body must be a JSON object' }, { status: 400 });
}
const payload = json as Record<string, unknown>;

if (!('amount' in payload)) {
return NextResponse.json({ error: 'Missing required field: amount' }, { status: 400 });
}
if (
typeof payload.amount !== 'number' ||
!Number.isFinite(payload.amount) ||
!Number.isInteger(payload.amount)
) {
return NextResponse.json(
{ error: 'Field "amount" must be a finite integer' },
{ status: 400 },
);
}
if ('reason' in payload && payload.reason !== undefined && typeof payload.reason !== 'string') {
return NextResponse.json(
{ error: 'Field "reason" must be a string when provided' },
{ status: 400 },
);
}

// Only forward the known fields — strip unknown keys
const sanitized: { amount: number; reason?: string } = { amount: payload.amount as number };
if (typeof payload.reason === 'string') sanitized.reason = payload.reason;

body = JSON.stringify(sanitized);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider aligning validation with the backend for fail-fast behavior.

The validation correctly ensures amount is a finite integer and strips unknown fields. However, the backend (src/api/routes/members.js:906-917) has two additional checks that this proxy doesn't replicate:

  1. amount !== 0 — zero is rejected
  2. Math.abs(amount) <= 1_000_000 — magnitude limit

Invalid requests will still fail at the backend, but users receive a less specific error after a network round-trip. For better UX, consider adding these checks here:

♻️ Suggested validation alignment
     if (
       typeof payload.amount !== 'number' ||
       !Number.isFinite(payload.amount) ||
       !Number.isInteger(payload.amount)
     ) {
       return NextResponse.json(
         { error: 'Field "amount" must be a finite integer' },
         { status: 400 },
       );
     }
+    if (payload.amount === 0) {
+      return NextResponse.json(
+        { error: 'Field "amount" must be non-zero' },
+        { status: 400 },
+      );
+    }
+    if (Math.abs(payload.amount) > 1_000_000) {
+      return NextResponse.json(
+        { error: 'Field "amount" must be between -1000000 and 1000000' },
+        { status: 400 },
+      );
+    }
     if ('reason' in payload && payload.reason !== undefined && typeof payload.reason !== 'string') {
📝 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 json: unknown = await request.json();
// Validate payload shape before forwarding (defense-in-depth)
if (typeof json !== 'object' || json === null || Array.isArray(json)) {
return NextResponse.json({ error: 'Body must be a JSON object' }, { status: 400 });
}
const payload = json as Record<string, unknown>;
if (!('amount' in payload)) {
return NextResponse.json({ error: 'Missing required field: amount' }, { status: 400 });
}
if (
typeof payload.amount !== 'number' ||
!Number.isFinite(payload.amount) ||
!Number.isInteger(payload.amount)
) {
return NextResponse.json(
{ error: 'Field "amount" must be a finite integer' },
{ status: 400 },
);
}
if ('reason' in payload && payload.reason !== undefined && typeof payload.reason !== 'string') {
return NextResponse.json(
{ error: 'Field "reason" must be a string when provided' },
{ status: 400 },
);
}
// Only forward the known fields — strip unknown keys
const sanitized: { amount: number; reason?: string } = { amount: payload.amount as number };
if (typeof payload.reason === 'string') sanitized.reason = payload.reason;
body = JSON.stringify(sanitized);
const json: unknown = await request.json();
// Validate payload shape before forwarding (defense-in-depth)
if (typeof json !== 'object' || json === null || Array.isArray(json)) {
return NextResponse.json({ error: 'Body must be a JSON object' }, { status: 400 });
}
const payload = json as Record<string, unknown>;
if (!('amount' in payload)) {
return NextResponse.json({ error: 'Missing required field: amount' }, { status: 400 });
}
if (
typeof payload.amount !== 'number' ||
!Number.isFinite(payload.amount) ||
!Number.isInteger(payload.amount)
) {
return NextResponse.json(
{ error: 'Field "amount" must be a finite integer' },
{ status: 400 },
);
}
if (payload.amount === 0) {
return NextResponse.json(
{ error: 'Field "amount" must be non-zero' },
{ status: 400 },
);
}
if (Math.abs(payload.amount) > 1_000_000) {
return NextResponse.json(
{ error: 'Field "amount" must be between -1000000 and 1000000' },
{ status: 400 },
);
}
if ('reason' in payload && payload.reason !== undefined && typeof payload.reason !== 'string') {
return NextResponse.json(
{ error: 'Field "reason" must be a string when provided' },
{ status: 400 },
);
}
// Only forward the known fields — strip unknown keys
const sanitized: { amount: number; reason?: string } = { amount: payload.amount as number };
if (typeof payload.reason === 'string') sanitized.reason = payload.reason;
body = JSON.stringify(sanitized);
🤖 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
40 - 72, The client-side validation in this route currently checks payload,
amount type, and reason but omits the backend's additional constraints (reject
zero and enforce magnitude <= 1_000_000), so update the validation around
payload.amount (the block that currently checks Number.isFinite and
Number.isInteger) to also return 400 when payload.amount === 0 and when
Math.abs(payload.amount) > 1_000_000, using NextResponse.json with clear error
messages consistent with the backend; ensure sanitized remains created only
after these checks and that body = JSON.stringify(sanitized) is unchanged.

Comment on lines 84 to +86
const onGuildChange = useCallback(() => {
setMembers([]);
setNextAfter(null);
setTotal(0);
setFilteredTotal(null);
setError(null);
}, []);
resetAll();
}, [resetAll]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider stale data visibility on page revisit.

The useGuildSelection hook only calls onGuildChange when the guild ID actually changes via localStorage or CustomEvent. On component mount when revisiting the page, if the same guild is still selected, onGuildChange is NOT called.

Combined with the Zustand store that persists state across component unmount/remount, this means old members data will briefly render before the fetch effect (lines 169-179) refreshes it.

This is a valid stale-while-revalidate (SWR) pattern, but be aware that if the initial fetch fails, stale data remains visible alongside the error message. If this UX is undesirable, consider clearing members at the start of fetchMembers when append: false.

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

In `@web/src/app/dashboard/members/page.tsx` around lines 84 - 86, The hook
currently doesn't clear persisted members when revisiting the page with the same
guild, so stale entries can remain visible if the initial re-fetch fails; update
the logic so that either onGuildChange is invoked on mount or—preferably—modify
fetchMembers to clear the stored members when called with append: false (e.g.,
set members = [] / call resetAll at the start of fetchMembers) so the UI doesn't
show stale members while the fresh request is in-flight; reference the
fetchMembers function and the resetAll/onGuildChange handlers to implement this
change.

// Cases filters & pagination
page: number;
sortDesc: boolean;
actionFilter: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider: Type actionFilter more strictly.

actionFilter: string could be a union type (e.g., 'all' | 'warn' | 'ban' | ...) to prevent invalid values and improve autocomplete. This is optional if the valid values are dynamic or defined elsewhere.

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

In `@web/src/stores/moderation-store.ts` at line 7, The property actionFilter in
moderation-store.ts is typed too loosely as string; tighten it to a union type
(e.g., type ActionFilter = 'all' | 'warn' | 'ban' | 'mute' | ...) and use that
type for the actionFilter property to prevent invalid values and enable editor
autocomplete; update the declaration (actionFilter: ActionFilter), export the
ActionFilter type if consumed elsewhere, and adjust any places that set or read
actionFilter (including default values and setter functions) to use the new
union members or a mapped constant list.

Replace raw loading text with animated Skeleton components on:
- Audit log, tickets, conversations, temp-roles pages (skeleton table rows)
- Role selector and channel selector popovers (skeleton list items)

Partially addresses #144
Bill Chirico and others added 17 commits March 7, 2026 14:50
- Create ErrorBoundary class component in web/src/components/ui/
- Shows friendly error card with AlertTriangle icon and 'Try Again' button
- Supports custom fallback render prop for flexible error UIs
- Wrap AnalyticsDashboard (main dashboard page) and ModerationStats
- Dev mode surfaces the raw error message for easier debugging

Partially addresses #144
Use sonner toasts to surface success/error feedback for:
- XP adjustment (toast.success / toast.error alongside inline state)
- Member CSV export (toast.success on download, toast.error on failure)

Config editor already had comprehensive toasts. Partially addresses #144
Validate the incoming JSON body before forwarding to the bot API:
- Reject non-object payloads with 400
- Require 'amount' field as a finite number
- Reject non-string 'reason' values
- Strip unknown keys (only forward amount + reason)

Defense-in-depth: catches bad clients before they hit the backend.
Partially addresses #144
- Export startRateLimitCleanup() for testing and graceful re-init
- Add 'stale entry cleanup (interval sweep)' test that verifies the
  5-minute setInterval sweep actually removes entries whose last activity
  is older than their retention window (uses fake timers)

The cleanup itself was already implemented; this adds test coverage and
makes the function available for external callers.

Partially addresses #144
- Replace console.error in ErrorBoundary with project logger
- Use 'skeleton-${i}' key prefix instead of bare index (noArrayIndexKey)
- Organize imports in tickets and conversations pages
- Format pass (biome)
Copilot AI review requested due to automatic review settings March 7, 2026 19:50
Copy link
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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@BillChirico BillChirico disabled auto-merge March 7, 2026 19:58
@BillChirico BillChirico merged commit f53f0be into main Mar 7, 2026
18 of 22 checks passed
@BillChirico BillChirico deleted the chore/issue-144 branch March 7, 2026 19:58
@github-project-automation github-project-automation bot moved this from In Review to Done in Volvox.Bot Mar 7, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 7, 2026

🧹 Preview Environment Cleaned Up

The Railway preview environment for this PR has been removed.

Environment: pr-257

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

Labels

priority: medium Medium priority scope: ops CI/CD, deploy, monitoring, operations type: task Discrete implementation task

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants