Skip to content

feat: support ticket system with private threads and channel mode#142

Merged
BillChirico merged 28 commits intomainfrom
feat/ticket-system
Feb 28, 2026
Merged

feat: support ticket system with private threads and channel mode#142
BillChirico merged 28 commits intomainfrom
feat/ticket-system

Conversation

@BillChirico
Copy link
Collaborator

Summary

Complete support ticket system for member support. Configurable via dashboard — choose between private threads or dedicated channels.

Features

  • /ticket open [topic] — Opens a ticket (thread or channel based on config)
  • /ticket close [reason] — Closes ticket, saves transcript, archives/deletes
  • /ticket add @user / /ticket remove @user — Manage ticket membership
  • /ticket panel [channel] — Posts persistent "Open Ticket" button panel (admin only)
  • Ticket mode: thread (private threads, default) or channel (dedicated text channel with permission overrides)
  • Auto-close: Warns after 48h inactivity, closes after 72h (configurable)
  • Transcripts: Saves last 100 messages as JSON on close, optionally posts to transcript channel
  • Rate limit: Max 3 open tickets per user (configurable)

Files (20+ new)

  • migrations/014_tickets.cjs — tickets table with indexes
  • src/modules/ticketHandler.js — Core logic (open/close/add/remove/autoclose)
  • src/commands/ticket.js — Slash command with subcommands
  • src/api/routes/tickets.js — REST API (list, detail, stats)
  • web/src/app/dashboard/tickets/page.tsx — Ticket list with filters
  • web/src/app/dashboard/tickets/[ticketId]/page.tsx — Detail with transcript
  • 3 Next.js API proxy routes
  • Dashboard config section with mode dropdown
  • Button/modal handlers in events.js
  • Scheduler integration for auto-close

Tests

  • 47 new tests (17 API + 20 module + 10 channel mode)
  • All 2440 tests passing, 85%+ branch coverage

Config Options

{
  "tickets": {
    "enabled": true,
    "mode": "thread",       // "thread" or "channel"
    "supportRole": "role-id",
    "category": "category-id",
    "autoCloseHours": 48,
    "transcriptChannel": "channel-id",
    "maxOpenPerUser": 3
  }
}

Closes #134

Copilot AI review requested due to automatic review settings February 28, 2026 14:14
@claude
Copy link

claude bot commented Feb 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Warning

Rate limit exceeded

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 896cdb9 and 03a5c34.

📒 Files selected for processing (19)
  • migrations/014_tickets.cjs
  • src/api/index.js
  • src/api/routes/tickets.js
  • src/commands/ticket.js
  • src/modules/events.js
  • src/modules/scheduler.js
  • src/modules/ticketHandler.js
  • tests/api/routes/tickets.test.js
  • tests/commands/ticket.test.js
  • tests/modules/events.tickets.test.js
  • tests/modules/ticketHandler.test.js
  • 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/dashboard/tickets/[ticketId]/page.tsx
  • web/src/app/dashboard/tickets/page.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/layout/sidebar.tsx
  • web/src/types/config.ts
📝 Walkthrough

Walkthrough

Implements a comprehensive support ticket system enabling guild members to create, manage, and resolve support tickets. Includes database schema, Discord slash commands and interactive UI components, backend ticket logic supporting thread/channel modes, API endpoints with pagination and filtering, web dashboard interface with ticket listing and transcript viewing, and scheduler-integrated auto-close functionality.

Changes

Cohort / File(s) Summary
Database Migration
migrations/014_tickets.cjs
Creates tickets table with columns for ticket metadata (guild, user, topic, status, thread/channel references, timestamps, transcript) and two compound indexes for guild-status and guild-user lookups.
Core Ticket Logic
src/modules/ticketHandler.js
Implements ticket lifecycle management: config resolution with defaults, channel/thread creation with permissions, member addition/removal, transcript capture, auto-close detection with warnings, and ticket panel UI building; supports both thread and channel modes.
Scheduler Integration
src/modules/scheduler.js
Registers auto-close check within the polling loop to periodically evaluate inactive tickets.
Discord Command & Events
src/commands/ticket.js, src/modules/events.js
Adds /ticket slash command with open/close/add/remove/panel subcommands and registers interactive handlers for button and modal submissions to trigger ticket workflows.
API Routes
src/api/index.js, src/api/routes/tickets.js
Registers tickets router with endpoints for listing (with pagination/filtering), fetching single ticket details, and retrieving per-guild statistics; includes rate limiting and admin validation.
Web Dashboard Pages
web/src/app/dashboard/tickets/page.tsx, web/src/app/dashboard/tickets/[ticketId]/page.tsx
Adds ticket listing page with pagination, status/user filtering, and stats display; detail page shows ticket metadata and full transcript with message history.
Web API Routes
web/src/app/api/guilds/[guildId]/tickets/...
Introduces three Next.js proxy routes for tickets list, detail, and stats that forward requests to the bot API after authorization checks.
Configuration & UI
web/src/types/config.ts, web/src/components/dashboard/config-editor.tsx, web/src/components/layout/sidebar.tsx
Extends BotConfig with TicketsConfig interface, adds ticket settings editor UI (mode, support role, category, auto-close hours, transcript channel, max open per user), and adds "Tickets" sidebar navigation item.
Tests
tests/api/routes/tickets.test.js, tests/modules/ticketHandler.test.js
Comprehensive test coverage for API routes (auth, pagination, filtering, stats) and ticket handler logic (both thread and channel modes, member management, auto-close, transcript handling).

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature being added: a complete support ticket system with both private thread and channel modes as primary options.
Description check ✅ Passed The description is thorough and directly related to the changeset, covering features, files modified, tests, and configuration options for the ticket system.
Linked Issues check ✅ Passed The PR implements all subtasks from #134: /ticket commands (open/close/add/remove/panel), auto-close with warnings, transcripts, dashboard pages, config options, migration, and tests. All objectives are met.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the support ticket system from #134. No unrelated modifications found in migrations, commands, API, modules, dashboard, or tests.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ticket-system

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.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Review Summary

Issues Found

🔴 Critical (1)

  • Close button broken for channel-mode tickets (src/modules/events.js:677-683): registerTicketCloseButtonHandler only accepts threads (!thread?.isThread()), but channel-mode tickets use text channels. Users clicking "Close Ticket" in channel mode get an error.

🟡 Warning (2)

  • Panel description hardcodes "private thread" (src/modules/ticketHandler.js:486-489): buildTicketPanel says "A private thread will be created" even when channel mode is active.
  • Unbounded checkAutoClose query (src/modules/ticketHandler.js:410-413): Fetches ALL open tickets from ALL guilds every 60s. Should filter by connected guilds.

🔵 Nitpick (2)

  • Unused variable (src/modules/events.js:585): guildConfig declared but never used in registerTicketOpenButtonHandler.
  • Stale JSDoc (web/src/types/config.ts:218-219): "Daily challenge scheduler settings" comment belongs to ChallengesConfig, not TicketsConfig.

AI fix prompt (click to expand)
Fix the following issues in the volvox-bot repo on branch feat/ticket-system:

1. src/modules/events.js, lines 677-683: The registerTicketCloseButtonHandler
   only accepts threads via `!thread?.isThread()`. Channel-mode tickets use
   GuildText channels, so this breaks the close button for them. Change the
   guard to accept both threads and GuildText channels (type === 0), matching
   the isTicketContext() pattern in src/commands/ticket.js:71-76. Update the
   error message to say "ticket thread or channel".

2. src/modules/events.js, line 585: Remove the unused `guildConfig` variable.
   The line `const guildConfig = getConfig(interaction.guildId);` is dead code
   — only `ticketConfig` (line 586) is used. Delete line 585 entirely.

3. src/modules/ticketHandler.js, lines 486-489: Change the buildTicketPanel()
   embed description from "A private thread will be created" to
   "A private space will be created" so it's accurate for both thread and
   channel modes.

4. src/modules/ticketHandler.js, lines 410-413: In checkAutoClose(), change the
   query from `SELECT * FROM tickets WHERE status = $1` (which loads ALL open
   tickets from ALL guilds) to filter by guilds the bot is connected to:
   `SELECT * FROM tickets WHERE status = $1 AND guild_id = ANY($2)` with
   params ['open', Array.from(client.guilds.cache.keys())]. Pass `client` info
   to scope the query. Note: the function already receives `client` as a param.

5. web/src/types/config.ts, lines 218-219: Remove the stale JSDoc comment
   "/** Daily challenge scheduler settings. */" that is incorrectly placed
   before TicketsConfig. Keep only "/** Ticket system settings. */".

After fixes, run `pnpm biome check --write .` to format, then `pnpm test` to
verify all tests pass.

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: 17

🤖 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_tickets.cjs`:
- Around line 31-39: Add an index to support lookups by thread/channel id and
status used by closeTicket() and related flows: in the migrations file where
other ticket indexes are created (indexes on tickets(guild_id, status) and
tickets(guild_id, user_id)), add a CREATE INDEX IF NOT EXISTS for the tickets
table on (thread_id, status) (suggested name like idx_tickets_thread_status) so
queries that filter by thread_id + status will use the index and scale properly.

In `@src/api/routes/tickets.js`:
- Around line 89-97: Validate the ticketId before hitting the database: where
you extract const { id: guildId, ticketId } = req.params (before pool.query),
parse ticketId to an integer and check that it's a valid positive integer (e.g.,
Number.parseInt and Number.isFinite/Number.isInteger and > 0 or !Number.isNaN).
If invalid, short-circuit and return res.status(400).json({ error: 'Invalid
ticketId' }) so pool.query('SELECT * FROM tickets WHERE guild_id = $1 AND id =
$2', [guildId, Number.parseInt(ticketId, 10)]) only runs with a validated
numeric id.

In `@src/commands/ticket.js`:
- Around line 233-239: The permission-error message is misleading: the check
allows either Administrator permission or isModerator(interaction.member,
guildConfig), but the reply text says admins only; update the message in the
safeEditReply call to reflect both allowed roles (e.g., "You must be a moderator
or administrator to use this command.") so it matches the permission check
involving interaction.member.permissions.has(PermissionFlagsBits.Administrator)
and isModerator(...).
- Around line 71-75: The isTicketContext function is too permissive (it treats
any GuildText as a ticket), causing /ticket add/remove/close to modify unrelated
channels; restrict ticket detection to explicit ticket channels only by updating
isTicketContext to return true only for threads OR channels that match your
ticket-identifying criteria (e.g., channel.name pattern like
startsWith('ticket-'), channel.parentId === TICKET_CATEGORY_ID,
channel.topic/includes a ticket marker, or an entry exists in your tickets
datastore) and false otherwise, and ensure the code paths that change permission
overwrites (the handlers that call isTicketContext before performing permission
updates) reject requests when isTicketContext returns false.

In `@src/modules/events.js`:
- Around line 670-683: The handler in registerTicketCloseButtonHandler currently
rejects any interaction where interaction.channel?.isThread() is false, which
blocks channel-mode tickets; instead, change the context check to accept either
a thread or a channel that is a ticket by using the repository/lookup the
codebase already uses for tickets (e.g., call the existing getTicketByChannelId
or isTicketChannel utility) so the button works for both thread-mode and
channel-mode tickets; adjust the early-return block to fetch the ticket via that
lookup (referencing registerTicketCloseButtonHandler, Events.InteractionCreate,
the 'ticket_close_' customId, and safeEditReply) and only reply with the "only
inside a ticket" message if no ticket is found.

In `@src/modules/scheduler.js`:
- Around line 192-193: The scheduler currently calls checkAutoClose() every tick
which is too heavy; add an interval gate so checkAutoClose() runs at most once
per configured period (e.g. AUTO_CLOSE_INTERVAL_MS). Introduce a module-scoped
lastAutoCloseRun timestamp and a constant AUTO_CLOSE_INTERVAL_MS, then in the
scheduler before awaiting checkAutoClose(client) check (Date.now() -
lastAutoCloseRun) >= AUTO_CLOSE_INTERVAL_MS, update lastAutoCloseRun when you
invoke checkAutoClose, and skip the call otherwise; keep the existing call site
(the line that currently awaits checkAutoClose(client)) but wrap it with this
guard to throttle executions and avoid API pressure.

In `@src/modules/ticketHandler.js`:
- Around line 454-465: The current warning detection using
recentMessages/hasWarning and msg.content.includes('auto-close') is fragile—add
a persistent warning_sent_at timestamp column to the tickets table and use it
instead: in the logic around recentMessages/hasWarning replace the string match
check with a check of ticket.warning_sent_at (e.g., if ticket.warning_sent_at is
null or older than your threshold then send warning), and when sending the
warning via safeSend set ticket.warning_sent_at = now() (persist to DB) and log
it (info('Auto-close warning sent', { ticketId: ticket.id, warning_sent_at }));
also ensure you clear or update warning_sent_at when the ticket receives
activity or is closed so the state stays consistent.
- Around line 331-340: The fire-and-forget setTimeout (using
CHANNEL_DELETE_DELAY_MS) can orphan channels if the process restarts; instead,
when scheduling a channel deletion in ticketHandler.js, persist a deletion
marker (e.g., set ticket.deleteScheduledAt or ticket.markedForDeletion) in the
DB before calling setTimeout, update the scheduled-delete handler to clear that
marker on successful channel.delete (and leave it if deletion fails), and
implement a startup cleanup routine (e.g., cleanupScheduledDeletions or
runScheduledDeletionJobs invoked during app bootstrap) that queries tickets with
deleteScheduledAt/markedForDeletion and attempts immediate deletion of their
channels; keep the existing setTimeout handler as a best-effort fallback and
ensure error logging (logError with ticket.id and err.message) remains.
- Around line 86-94: The code uses guild.members.me.id when building an
OverwriteType.Member permission object which can throw if guild.members.me is
null; update the logic in the ticket creation path to safely obtain the bot's ID
(use guild.members.me?.id fallback to guild.client.user?.id or fetch the
GuildMember via guild.members.fetch(guild.client.user.id) if needed) before
constructing the overwrite object (the block referencing OverwriteType.Member
and PermissionFlagsBits.ViewChannel/SendMessages/ManageChannels), and ensure you
handle and log failures to resolve the bot ID so you don't pass undefined into
the overwrite.
- Around line 481-501: Change buildTicketPanel to accept a guildId parameter
(e.g., buildTicketPanel(guildId)) and use the module's existing ticket
configuration lookup for that guild to determine mode ('thread' vs 'channel');
then build the embed description dynamically (use "A private thread will be
created…" for thread mode and "A private channel will be created…" for channel
mode), leaving other embed/button fields unchanged. Update the call site that
currently invokes buildTicketPanel() to pass interaction.guildId (or equivalent
guild identifier) and update the unit test that constructs the panel to call the
new signature with a test guildId and appropriate mocked guild ticket config.
Ensure the customId 'ticket_open' and the ActionRow/ButtonBuilder remain the
same.

In `@web/src/app/dashboard/tickets/`[ticketId]/page.tsx:
- Around line 41-86: Replace the local fetch/state logic inside
TicketDetailPage: remove useState hooks (data, loading, error), the fetchDetail
function and the useEffect call, and instead call the dashboard/zustand store's
fetch-on-demand method and selectors (e.g.,
useTicketStore.getState().fetchTicket or useTicketStore(ticket => ({ ticket,
loading, error, fetchTicket }))) to load and subscribe to the ticket by guildId
and ticketId; ensure the router.replace('/login') handling is done via the
store's auth/error behavior or by reading the store error and redirecting in
TicketDetailPage, and use the store's ticket/ticketDetail, loading and error
selectors in the component render.
- Around line 52-54: The fetchDetail callback returns early when guildId or
ticketId is missing which leaves the component stuck in a loading state; update
fetchDetail to clear the loading flag before returning (e.g., call the
component's loading state setter such as setLoading(false) or
setIsLoading(false)) or rework the guard to avoid entering the loading state
when guildId/ticketId are undefined, referencing fetchDetail, guildId and
ticketId to locate the change.

In `@web/src/app/dashboard/tickets/page.tsx`:
- Around line 352-356: The TableRow currently only uses onClick (TableRow with
onClick={() => handleRowClick(ticket.id)}) which is not keyboard-accessible;
update the row to be focusable and respond to keyboard activation by either
rendering a semantic interactive element inside the row (e.g., wrap the
clickable cell content in a <button> or <a> that calls
handleRowClick(ticket.id)) or add accessibility attributes and handlers (add
tabIndex={0} to the TableRow, implement onKeyDown to call
handleRowClick(ticket.id) on Enter/Space, and set role="button"), and ensure
visible focus styles are preserved so keyboard users can discover and activate
the row.
- Around line 76-205: The page currently manages ticket list, stats and fetch
orchestration locally (TicketsPage, fetchTickets, abortControllerRef,
requestIdRef, stats, tickets, total, loading, error) but per guidelines this
logic must live in the shared Zustand store and follow a fetch-on-demand
pattern; move the network logic into the store by adding actions like
fetchTickets({ guildId, status, user, page }) and fetchStats(guildId) that
manage AbortController, request deduping (requestId), PAGE_SIZE, loading/error
state and 401 handling (call onUnauthorized or surface an unauthorized flag),
expose tickets, total, stats, loading, error and a refresh action; then refactor
TicketsPage to remove local ticket/stats/fetch state and simply read from the
store and call store.fetchTickets/store.fetchStats inside the existing effects
and handleRefresh, keeping useGuildSelection and the debounced search logic but
invoking the store actions instead of the local fetchTickets function.

In `@web/src/components/dashboard/config-editor.tsx`:
- Around line 1728-1732: The tickets ID fields are persisted using
e.target.value || null which treats whitespace-only strings as non-null IDs;
update the onChange handlers that call updateDraftConfig to trim the input
before null-coalescing (e.g., use e.target.value.trim() and set
tickets.supportRole to trimmedValue === "" ? null : trimmedValue) so inputs like
supportRole (and the other tickets ID fields at the referenced handlers) store
null for empty/whitespace-only values.
- Around line 1684-1814: The Tickets UI is mutating local component state via
draftConfig and updateDraftConfig; move this editing logic into the shared
Zustand config store and use the store's fetch-on-demand pattern instead.
Replace direct reads/writes of draftConfig and updateDraftConfig in the Tickets
Card (the select, inputs, ToggleSwitch and number handlers) to call the store's
getters and setter actions (the store's fetchConfig/fetchOnDemand and
updateConfig or equivalent actions) so the component reads from the zustand
state and updates via store actions; keep input validation (parseNumberInput)
but pass the parsed values into the store update action rather than local state.
Ensure enabled, mode, supportRole, category, autoCloseHours, maxOpenPerUser and
transcriptChannel are sourced from and persisted to the store rather than
component-local state.

In `@web/src/types/config.ts`:
- Around line 219-220: The JSDoc for "Daily challenge scheduler settings" is
currently placed above TicketsConfig; move that comment block so it sits
immediately above ChallengesConfig instead. Locate the interfaces TicketsConfig
and ChallengesConfig in this file and cut the misplaced JSDoc from above
TicketsConfig and paste it above ChallengesConfig, leaving TicketsConfig with
its correct "Ticket system settings." JSDoc content should remain unchanged and
positioned directly before the ChallengesConfig interface declaration.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c8f92c and 896cdb9.

📒 Files selected for processing (17)
  • migrations/014_tickets.cjs
  • src/api/index.js
  • src/api/routes/tickets.js
  • src/commands/ticket.js
  • src/modules/events.js
  • src/modules/scheduler.js
  • src/modules/ticketHandler.js
  • tests/api/routes/tickets.test.js
  • tests/modules/ticketHandler.test.js
  • 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/dashboard/tickets/[ticketId]/page.tsx
  • web/src/app/dashboard/tickets/page.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/layout/sidebar.tsx
  • web/src/types/config.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). (3)
  • GitHub Check: Greptile Review
  • GitHub Check: Agent
  • GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,tsx}: Always use semicolons
Use single quotes — enforced by Biome
Use 2-space indentation — enforced by Biome

Files:

  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • src/commands/ticket.js
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • web/src/types/config.ts
  • src/modules/scheduler.js
  • web/src/app/dashboard/tickets/page.tsx
  • src/modules/events.js
  • web/src/app/dashboard/tickets/[ticketId]/page.tsx
  • tests/modules/ticketHandler.test.js
  • src/api/routes/tickets.js
  • web/src/components/dashboard/config-editor.tsx
  • tests/api/routes/tickets.test.js
  • web/src/components/layout/sidebar.tsx
  • src/api/index.js
  • src/modules/ticketHandler.js
web/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/image Image component with appropriate layout and sizing props in Next.js components

Files:

  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • web/src/types/config.ts
  • web/src/app/dashboard/tickets/page.tsx
  • web/src/app/dashboard/tickets/[ticketId]/page.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/layout/sidebar.tsx
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use Zustand store (zustand) for state management in React components; implement fetch-on-demand pattern in stores

Files:

  • web/src/app/api/guilds/[guildId]/tickets/stats/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts
  • web/src/app/api/guilds/[guildId]/tickets/route.ts
  • web/src/types/config.ts
  • web/src/app/dashboard/tickets/page.tsx
  • web/src/app/dashboard/tickets/[ticketId]/page.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/layout/sidebar.tsx
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules only — import/export, never require()
Always use node: protocol for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')

Files:

  • src/commands/ticket.js
  • src/modules/scheduler.js
  • src/modules/events.js
  • tests/modules/ticketHandler.test.js
  • src/api/routes/tickets.js
  • tests/api/routes/tickets.test.js
  • src/api/index.js
  • src/modules/ticketHandler.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: NEVER use console.log, console.warn, console.error, or any console.* method in src/ files — always use Winston logger instead: import { info, warn, error } from '../logger.js'
Pass structured metadata to Winston logger: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read config; use setConfigValue(path, value, guildId?) to update at runtime
Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Files:

  • src/commands/ticket.js
  • src/modules/scheduler.js
  • src/modules/events.js
  • src/api/routes/tickets.js
  • src/api/index.js
  • src/modules/ticketHandler.js
src/commands/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/commands/*.js: Use parseDuration() from src/utils/duration.js for duration-based commands (timeout, tempban, slowmode); enforce Discord duration caps (timeouts max 28 days, slowmode max 6 hours)
Create slash commands by exporting data (SlashCommandBuilder) and execute() function from src/commands/*.js; export adminOnly = true for mod-only commands; commands are auto-discovered on startup

Files:

  • src/commands/ticket.js
src/modules/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/modules/*.js: Register event handlers in src/modules/events.js by importing handler functions and calling client.on() with config parameter
Check config.yourModule.enabled before processing in module event handlers
Prefer per-request getConfig() pattern in new modules over reactive onConfigChange() wiring; only add onConfigChange() listeners for stateful resources that cannot re-read config on each use

Files:

  • src/modules/scheduler.js
  • src/modules/events.js
  • src/modules/ticketHandler.js
🧠 Learnings (6)
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/commands/*.js : Create slash commands by exporting data (SlashCommandBuilder) and execute() function from src/commands/*.js; export adminOnly = true for mod-only commands; commands are auto-discovered on startup

Applied to files:

  • src/commands/ticket.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/commands/*.js : Use parseDuration() from src/utils/duration.js for duration-based commands (timeout, tempban, slowmode); enforce Discord duration caps (timeouts max 28 days, slowmode max 6 hours)

Applied to files:

  • src/commands/ticket.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/commands/*mod*.js : Moderation commands must follow the shared pattern: deferReply({ ephemeral: true }), validate inputs, sendDmNotification(), execute Discord action, createCase(), sendModLogEmbed(), checkEscalation()

Applied to files:

  • src/commands/ticket.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/index.js : The tempban scheduler runs on a 60s interval and catches up on missed unbans after restart; started in index.js startup and stopped in graceful shutdown

Applied to files:

  • src/modules/scheduler.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/modules/*.js : Register event handlers in src/modules/events.js by importing handler functions and calling client.on() with config parameter

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Applied to files:

  • tests/modules/ticketHandler.test.js
🧬 Code graph analysis (11)
web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts (1)
web/src/lib/bot-api-proxy.ts (4)
  • authorizeGuildAdmin (37-69)
  • getBotApiConfig (82-92)
  • buildUpstreamUrl (100-113)
  • proxyToBotApi (135-176)
src/commands/ticket.js (5)
src/modules/ticketHandler.js (13)
  • channel (423-423)
  • ticketConfig (125-125)
  • ticketConfig (301-301)
  • ticketConfig (417-417)
  • getTicketConfig (58-61)
  • openTicket (121-242)
  • ticket (210-210)
  • ticket (266-266)
  • closeTicket (252-350)
  • embed (213-221)
  • embed (289-296)
  • embed (482-490)
  • row (492-498)
src/modules/config.js (2)
  • getConfig (282-313)
  • err (94-94)
src/utils/safeSend.js (2)
  • safeEditReply (178-185)
  • safeSend (116-123)
src/utils/permissions.js (1)
  • isModerator (143-173)
src/logger.js (1)
  • info (230-232)
web/src/app/api/guilds/[guildId]/tickets/route.ts (3)
web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts (1)
  • GET (14-37)
web/src/app/api/guilds/[guildId]/tickets/stats/route.ts (1)
  • GET (14-37)
web/src/lib/bot-api-proxy.ts (4)
  • authorizeGuildAdmin (37-69)
  • getBotApiConfig (82-92)
  • buildUpstreamUrl (100-113)
  • proxyToBotApi (135-176)
src/modules/scheduler.js (1)
src/modules/ticketHandler.js (1)
  • checkAutoClose (406-474)
src/modules/events.js (1)
src/modules/ticketHandler.js (6)
  • getTicketConfig (58-61)
  • row (492-498)
  • openTicket (121-242)
  • ticket (210-210)
  • ticket (266-266)
  • closeTicket (252-350)
tests/modules/ticketHandler.test.js (1)
src/modules/ticketHandler.js (12)
  • getTicketConfig (58-61)
  • row (492-498)
  • guild (304-304)
  • guild (420-420)
  • openTicket (121-242)
  • overwrites (72-95)
  • messages (270-270)
  • closeTicket (252-350)
  • channel (423-423)
  • addMember (361-375)
  • removeMember (386-397)
  • checkAutoClose (406-474)
src/api/routes/tickets.js (3)
src/api/index.js (1)
  • router (19-19)
src/api/middleware/rateLimit.js (1)
  • rateLimit (15-56)
src/api/routes/guilds.js (1)
  • validateGuild (292-302)
web/src/components/dashboard/config-editor.tsx (2)
web/src/components/ui/card.tsx (3)
  • Card (55-55)
  • CardContent (55-55)
  • CardTitle (55-55)
web/src/components/dashboard/toggle-switch.tsx (1)
  • ToggleSwitch (14-32)
tests/api/routes/tickets.test.js (3)
src/api/routes/tickets.js (1)
  • req (124-124)
src/api/server.js (1)
  • createApp (27-87)
src/modules/ticketHandler.js (2)
  • ticket (210-210)
  • ticket (266-266)
src/api/index.js (2)
src/api/routes/tickets.js (1)
  • router (13-13)
src/api/middleware/auth.js (1)
  • requireAuth (36-70)
src/modules/ticketHandler.js (4)
src/modules/config.js (2)
  • getConfig (282-313)
  • err (94-94)
src/db.js (1)
  • getPool (142-147)
src/utils/safeSend.js (1)
  • safeSend (116-123)
src/logger.js (1)
  • info (230-232)
🪛 GitHub Check: CodeQL
src/api/routes/tickets.js

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


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


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

src/api/index.js

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

🔇 Additional comments (10)
web/src/components/layout/sidebar.tsx (1)

11-12: Looks good — Tickets nav integration is clean.

Import and sidebar entry are consistent with existing navigation patterns.

Also applies to: 45-49

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

14-45: Proxy flow is solid and consistent.

Auth, config checks, upstream URL construction, and query-param allowlisting are implemented correctly.

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

14-37: LGTM — consistent proxy endpoint implementation.

Validation, authorization, and upstream forwarding are all aligned with sibling routes.

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

14-37: Looks good — detail route is correctly structured.

The guard/auth/config/proxy sequence is implemented cleanly.

src/api/index.js (1)

16-44: Route registration order is correct.

Mounting tickets before the generic guilds router prevents path shadowing for /:id/tickets/*.

src/modules/ticketHandler.js (5)

1-50: LGTM!

Well-documented module header, proper ESM imports, and clearly defined constants with sensible defaults. The documentation accurately describes the dual-mode (thread/channel) functionality.


52-61: LGTM!

Clean implementation following the per-request getConfig() pattern as recommended in the coding guidelines.


121-242: LGTM!

The openTicket function correctly implements both thread and channel modes with proper permission handling, rate limiting via DB check, and structured logging. Good use of safeSend for Discord messaging.


352-375: LGTM!

Clean dual-mode implementation with proper safeSend usage and structured logging.


377-397: LGTM!

Mirrors addMember with appropriate removal logic for both modes.

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.

Pull request overview

This PR implements a complete support ticket system for the Volvox Discord bot, allowing members to open private support threads with staff. It is configurable via the dashboard and supports two modes: thread (private Discord threads) and channel (dedicated text channels with permission overrides). It closes issue #134.

Changes:

  • Adds the full ticket backend: migration, core handler module, slash command, API routes, and scheduler integration
  • Adds the web dashboard frontend: ticket list page, ticket detail/transcript page, 3 Next.js API proxy routes, config editor section, and sidebar navigation entry
  • Adds 47 new tests across two test files covering the handler module and API routes

Reviewed changes

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

Show a summary per file
File Description
migrations/014_tickets.cjs Creates the tickets table with indexes for guild/status and guild/user lookups
src/modules/ticketHandler.js Core ticket logic: open/close/add/remove/autoclose, supporting both thread and channel modes
src/commands/ticket.js /ticket slash command with subcommands for open, close, add, remove, and panel
src/api/routes/tickets.js REST API endpoints: list tickets, ticket detail, and stats with pagination and filtering
src/api/index.js Mounts the new tickets router
src/modules/events.js Registers button/modal handlers for the ticket panel and close button
src/modules/scheduler.js Integrates checkAutoClose into the scheduler polling loop
web/src/types/config.ts Adds TicketsConfig interface and 'tickets' to ConfigSection union
web/src/components/layout/sidebar.tsx Adds Tickets nav item to the dashboard sidebar
web/src/components/dashboard/config-editor.tsx Adds the Tickets configuration card to the config editor
web/src/app/dashboard/tickets/page.tsx Ticket list dashboard page with filters, stats, and pagination
web/src/app/dashboard/tickets/[ticketId]/page.tsx Ticket detail page with transcript viewer
web/src/app/api/guilds/[guildId]/tickets/route.ts Next.js API proxy for ticket list endpoint
web/src/app/api/guilds/[guildId]/tickets/stats/route.ts Next.js API proxy for ticket stats endpoint
web/src/app/api/guilds/[guildId]/tickets/[ticketId]/route.ts Next.js API proxy for ticket detail endpoint
tests/modules/ticketHandler.test.js Unit tests for the ticketHandler module (both modes)
tests/api/routes/tickets.test.js API route integration tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@greptile-apps
Copy link

greptile-apps bot commented Feb 28, 2026

Greptile Summary

implemented complete support ticket system with dual-mode support (private threads or dedicated channels), configurable via dashboard

Key Changes:

  • Database: added tickets table with status checks and indexes for guild/user/status queries
  • Core Logic: ticketHandler.js manages ticket lifecycle (open/close/add/remove) with auto-close scheduler checking every 5 minutes
  • Commands: /ticket slash command with 5 subcommands (open, close, add, remove, panel)
  • Dashboard: ticket list with filters/pagination, detail view with transcript, stats cards showing open count/avg resolution/weekly tickets
  • API: 3 REST endpoints (list, detail, stats) with rate limiting and guild admin authorization

All previous review threads addressed:

  • Fixed close button to handle both thread and channel modes
  • Fixed hardcoded locale strings in frontend (now uses undefined)
  • Removed JSDoc syntax errors

Configuration: 6 settings including mode (thread/channel), support role, category, auto-close hours (default 48h), transcript channel, and max open tickets per user (default 3)

Testing: 47 new tests with 85%+ branch coverage across API routes, commands, and handlers

Confidence Score: 4/5

  • safe to merge with minor documented limitations that don't impact core functionality
  • code follows all project conventions (ESM, Winston logging, parameterized queries), has comprehensive test coverage (47 tests, 85%+ branches), proper error handling throughout, and all previous review issues resolved; score reflects one documented limitation where channel deletion uses setTimeout which could orphan channels if process restarts during the 10s delay
  • src/modules/ticketHandler.js — review the channel deletion setTimeout pattern (line 357) if process restart frequency is high

Important Files Changed

Filename Overview
migrations/014_tickets.cjs creates tickets table with proper indexes and check constraints for status field
src/modules/ticketHandler.js core ticket logic supporting both thread and channel modes; includes auto-close, transcripts, and member management
src/commands/ticket.js slash command implementation with proper validation and context checks for all subcommands
src/api/routes/tickets.js REST endpoints for ticket list, detail, and stats with proper auth, rate limiting, and parameterized queries
web/src/app/dashboard/tickets/page.tsx ticket list view with filters, pagination, stats cards, and proper locale handling
tests/modules/ticketHandler.test.js comprehensive tests for ticket handler covering both thread and channel modes with 85%+ branch coverage

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User clicks Open Ticket button] --> B{Ticket system enabled?}
    B -->|No| C[Show error message]
    B -->|Yes| D[Show modal for topic input]
    D --> E[User submits topic]
    E --> F{Check max open tickets}
    F -->|Limit reached| G[Show error message]
    F -->|Under limit| H{Config mode?}
    H -->|thread| I[Create private thread]
    H -->|channel| J[Create text channel with permissions]
    I --> K[Add user & support role members]
    J --> K
    K --> L[Insert ticket into database]
    L --> M[Post ticket embed with close button]
    M --> N[Ticket active - users interact]
    N --> O{User clicks close OR /ticket close}
    N --> P{48h+ inactivity?}
    P -->|Yes| Q[Send warning message]
    Q --> R{72h+ inactivity?}
    R -->|Yes| S[Auto-close ticket]
    O --> T[Fetch last 100 messages as transcript]
    S --> T
    T --> U[Update DB: status=closed, save transcript]
    U --> V[Post close embed]
    V --> W{Mode?}
    W -->|thread| X[Archive thread]
    W -->|channel| Y[Delete channel after 10s delay]
    X --> Z[Send transcript to log channel]
    Y --> Z
    Z --> AA[Ticket closed]
Loading

Last reviewed commit: 03a5c34

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

19 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings and committed to branch feat/ticket-system (commit: c1adab3607c75d4938a89d7065aa473ce1d43070)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Note

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


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

- Add mode: 'thread' | 'channel' to TICKET_DEFAULTS
- openTicket: channel mode creates text channel with permission overrides
  (deny @everyone ViewChannel, allow user/support/bot with appropriate perms)
- closeTicket: channel mode deletes channel after 10s delay (vs archive)
- addMember/removeMember: channel mode uses permissionOverwrites
- checkAutoClose: accepts both threads and text channels
- ticket.js: isTicketContext() replaces strict isThread() check
- Add mode: 'thread' | 'channel' to TicketsConfig type
- Add 'Ticket Mode' select dropdown in tickets config section
- Thread mode (default) creates private threads
- Channel mode creates dedicated text channels with permission overrides
- Channel mode: openTicket creates text channel with permission overrides
- Channel mode: closeTicket deletes channel after delay (not archive)
- Channel mode: addMember uses permissionOverwrites.edit
- Channel mode: removeMember uses permissionOverwrites.delete
- Channel mode: checkAutoClose works with text channels
- All existing thread-mode tests preserved and passing
The close button handler only checked isThread(), rejecting channel-mode
tickets. Now accepts both thread and GuildText channel contexts.

Renamed 'thread' to 'ticketChannel' for clarity and updated error message
to mention both channel and thread.
…tion bypass

isTicketContext() previously returned true for ANY GuildText channel,
allowing /ticket add and /ticket remove to modify permission overwrites
on unrelated channels. Now queries the database to verify the channel
has an open ticket before allowing operations.

Also: fixed permission error message to mention both moderator and
administrator roles, and removed unused ticketConfig param from
handleOpen() call.
- Remove unused guildConfig in registerTicketOpenButtonHandler (events.js)
- Fix misplaced JSDoc: challenge doc was on TicketsConfig instead of ChallengesConfig
- Remove unused Clock import from tickets page
…e guild.members.me

- Add index on (thread_id, status) to support closeTicket() query
- Validate ticketId is a valid integer before DB query (returns 400 on NaN)
- Use guild.members.me?.id ?? guild.client.user.id for null safety
…arning detection

- checkAutoClose now filters tickets by guilds the bot is actually in
- Scheduler runs checkAutoClose every 5th tick (5 min) instead of every 60s
- Replace fragile msg.content.includes('auto-close') with module-level
  warningsSent Set keyed by ticket ID
Changed 'A private thread will be created' to 'A private ticket will be
opened' so the panel works correctly for both thread and channel modes.
…ort, trim

- Fix infinite spinner when guildId/ticketId absent (set loading=false)
- Fix formatDuration: >= 24 hours shows days instead of > 24
- Add keyboard accessibility (Enter/Space) to ticket table rows
- Add AbortController to stats fetch for guild switch cleanup
- Trim whitespace on ticket config ID inputs before null-coalescing
…ut limitation

- Skip CategoryChannel when resolving parent for thread creation (only
  GuildText/GuildAnnouncement support threads.create())
- Add null guard for guild.members.me in fallback channel search
- Document known limitation: setTimeout channel deletion won't survive
  process restarts
- buildTicketPanel: make description dynamic based on ticket mode
  (channel → 'A private channel will be created...',
   thread  → 'A private thread will be created...')
  Pass guildId from call site so config can be resolved.

- events.js: move ModalBuilder, TextInputBuilder, TextInputStyle,
  ActionRowBuilder from dynamic import() to top-level static import;
  eliminates unnecessary async resolution on every button click.

- checkAutoClose: filter bot messages when computing lastActivity
  so the warning message itself does not reset the inactivity timer,
  which would create an infinite warning loop on bot restarts.

- migrations/014_tickets.cjs: add CHECK (status IN ('open', 'closed'))
  constraint to match the pattern from 010_reviews.cjs.
- Fix stale auto-close test: checkAutoClose returns early when no guilds,
  so mock query count is 0 not 1
- Remove double mock for thread.messages.fetch (only one fetch call)
- Fix pre-existing checkAutoClose test failures: use Array.from() fallback
  for Map.find() since plain Map lacks Collection.find()
- Fix pre-existing ticket command test failures: mock getPool with a query
  that returns rows for thread1 (valid ticket) vs channel1 (not a ticket)
- Fix panel rejection error message mismatch in ticket command tests
- Filter GuildAnnouncement channels in thread mode: only GuildText supports
  PrivateThread creation
- Sanitize user.username for channel-mode ticket names (lowercase,
  alphanumeric + hyphens, max 100 chars)
- Remove role='link' from <tr> element in tickets dashboard page
- Update category placeholder text to 'Category for tickets' (mode-agnostic)
- Restructure Tickets card to use CardHeader for title+toggle, CardContent
  for fields (matches other config section patterns)
Copilot AI review requested due to automatic review settings February 28, 2026 16:38
@BillChirico BillChirico merged commit e8bee63 into main Feb 28, 2026
9 of 15 checks passed
@BillChirico BillChirico deleted the feat/ticket-system branch February 28, 2026 16:38
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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +490 to +491
// Close the ticket
await closeTicket(channel, client.user, 'Auto-closed due to inactivity');
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The checkAutoClose function at line 491 calls closeTicket(channel, client.user, ...). However, client.user is a ClientUser object, not a User object — and closeTicket accesses closer.id, which exists on ClientUser. But the closeTicket function also does <@${closer.id}> in the embed (line 315) and accesses closer.id in the DB update. While ClientUser does have an id, the function's JSDoc types say it expects import('discord.js').User. This could cause issues with any future code that relies on user-specific methods not available on ClientUser. The JSDoc for closeTicket should be updated to accept User | ClientUser, or client.user should be cast appropriately.

Suggested change
// Close the ticket
await closeTicket(channel, client.user, 'Auto-closed due to inactivity');
// Close the ticket using the bot user as the closer.
/** @type {import('discord.js').User} */
const autoCloserUser = client.user;
await closeTicket(channel, autoCloserUser, 'Auto-closed due to inactivity');

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +199
// Auto-close inactive support tickets (every 5 minutes / 5th tick)
tickCount++;
if (tickCount % 5 === 0) {
await checkAutoClose(client);
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The tickCount variable in scheduler.js (line 23) is a module-level variable that is never reset. On a long-running bot process, tickCount will eventually overflow JavaScript's Number.MAX_SAFE_INTEGER after trillions of minutes of operation. While this is not a practical concern in the near term, using tickCount % 5 === 0 after the increment also means auto-close never runs on the very first tick (tick 1). If you want auto-close to run immediately on startup (first tick), the condition should be checked before incrementing. This is a minor semantic issue but may confuse future maintainers.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +47
}

module.exports = { up };
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

All other migrations in this project (e.g., 010_reviews.cjs, 011_challenges.cjs, 012_flagged_messages.cjs, 013_public_profiles.cjs) export both an up and a down function. Migration 014_tickets.cjs is missing the down function, which means the migration cannot be rolled back. Following the established convention, a down function should be added that drops the indexes and the tickets table in reverse order.

Copilot uses AI. Check for mistakes.
/** Delay (ms) before deleting a channel-mode ticket so the close message is visible */
const CHANNEL_DELETE_DELAY_MS = 10_000;

/** Track ticket IDs that have received an auto-close warning in this process run */
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The warningsSent in-memory Set at line 53 tracks ticket IDs that have received auto-close warnings. This state is lost on process restart. After a bot restart, tickets that were already warned will receive a second warning before being closed — and worse, they may be closed sooner than expected because the auto-close timer resets from when the process restarted, not from when the warning was sent. This is a known limitation but it is not documented in the code. The comment only describes the "process run" scope but doesn't describe the behavior after a restart (incorrect close timing). Consider adding a note about this limitation in the comment.

Suggested change
/** Track ticket IDs that have received an auto-close warning in this process run */
/**
* Tracks ticket IDs that have received an auto-close warning during this process run.
*
* NOTE: This state is stored in-memory only and is lost when the bot process restarts.
* After a restart, tickets that were already warned may receive a second warning,
* and the auto-close timing will effectively be recalculated from the restart time,
* which can cause tickets to close sooner than the intended interval after the
* original warning.
*/

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: /ticket — support ticket system with private threads

2 participants