feat: /showcase project showcase with upvotes (#50)#113
Conversation
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a Project Showcase feature: new /showcase command with submit/browse/top/view, DB migrations for showcases and votes, event handlers for modal/button interactions, dashboard toggle and API allowlist update, and comprehensive tests and UI/embed helpers. Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| Filename | Overview |
|---|---|
| migrations/009_showcases.cjs | Creates showcases and showcase_votes tables with proper indexes and foreign key constraints. Clean migration with appropriate CASCADE behavior. |
| src/commands/showcase.js | Implements full /showcase command with submit, browse, top, view subcommands. Uses transactions for atomic upvote toggling, validates URLs for protocol safety, includes self-upvote prevention. Previous threads identified minor error handling gaps. |
| src/modules/events.js | Registers button and modal handlers for showcase feature, following existing poll handler pattern. Includes proper error handling and database availability checks. |
| tests/commands/showcase.test.js | Comprehensive test suite with 37 tests covering all subcommands, upvote toggling, self-upvote prevention, config gates, and edge cases. |
Sequence Diagram
sequenceDiagram
participant U as User
participant D as Discord
participant C as showcase.js
participant E as events.js
participant DB as PostgreSQL
Note over U,DB: /showcase submit Flow
U->>D: /showcase submit
D->>C: execute()
C->>C: Check config.showcase.enabled
C->>D: showModal()
D->>U: Display modal form
U->>D: Submit modal
D->>E: InteractionCreate (modal)
E->>E: registerShowcaseModalHandler
E->>C: handleShowcaseModalSubmit()
C->>C: Validate URLs (http/https only)
C->>D: deferReply (ephemeral)
C->>DB: INSERT INTO showcases
C->>D: safeSend (embed + button)
C->>DB: UPDATE message_id
C->>U: Success message
Note over U,DB: Upvote Flow
U->>D: Click upvote button
D->>E: InteractionCreate (button)
E->>E: registerShowcaseButtonHandler
E->>C: handleShowcaseUpvote()
C->>C: Check config.showcase.enabled
C->>DB: SELECT showcase
C->>C: Prevent self-upvote
C->>DB: BEGIN transaction
C->>DB: Check existing vote
alt Vote exists
C->>DB: DELETE vote + UPDATE upvotes-1
else No vote
C->>DB: INSERT vote + UPDATE upvotes+1
end
C->>DB: COMMIT
C->>U: Ephemeral confirmation
C->>D: Update button count
Note over U,DB: Browse Flow
U->>D: /showcase browse [tag] [page]
D->>C: execute() -> handleBrowse()
C->>D: deferReply
C->>DB: SELECT COUNT with tag filter
C->>DB: SELECT showcases LIMIT/OFFSET
C->>D: Paginated embed
D->>U: Display results
Last reviewed commit: c3ad323
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/009_showcases.cjs`:
- Around line 31-38: Add a non-unique index on the showcase_votes.showcase_id
column to speed queries that filter or aggregate by showcase_id; update the
migration (after CREATE TABLE for showcase_votes) to create an index (e.g.,
CREATE INDEX IF NOT EXISTS on showcase_votes(showcase_id)) so counts and lookups
by showcase_id use the index without changing the existing composite primary
key.
In `@src/commands/showcase.js`:
- Around line 512-522: The vote toggle logic in src/commands/showcase.js
performs separate queries (DELETE/INSERT on showcase_votes and UPDATE on
showcases) causing a race condition; wrap each branch (the "toggle off" branch
using DELETE + UPDATE that sets newUpvotes and the "add vote" branch around
lines handling INSERT + UPDATE) in a single database transaction using
pool.query('BEGIN') / pool.query('COMMIT') with a rollback on error, or replace
with a single atomic SQL statement that updates the showcases.upvotes based on
COUNT(*) from showcase_votes; ensure you run all queries for the given guildId,
showcaseId, userId within the same transaction and return/assign newUpvotes from
the transaction result.
- Line 229: The line using interaction.channel.send may throw if
interaction.channel is null; update the code around the const msg = await
interaction.channel.send({ embeds: [embed], components: [row] }); to first guard
that interaction.channel exists (e.g., if (!interaction.channel) ...) and use a
safe fallback such as sending via interaction.reply or fetching the channel
before sending. Locate the send call in src/commands/showcase.js and modify the
logic so it either uses interaction.reply({ embeds:[embed], components:[row] })
when channel is null, or fetches a valid channel from
interaction.client.channels.fetch(...) before calling send, and ensure the
result is assigned to the same const msg variable.
- Around line 443-447: getPool() throws when the DB pool is uninitialized so the
current if (!pool) branch is dead; either remove that unreachable check or wrap
the getPool() call in a try/catch around where it's used in the showcase command
(the code that calls getPool() in this file) and in the catch call
safeEditReply(interaction, { content: '❌ Database is not available.' }) then
return. Locate the getPool() invocation in this module (and related logic that
uses safeEditReply and interaction) and implement the chosen approach to ensure
the database-unavailable case is actually caught and handled.
In `@src/modules/events.js`:
- Line 7: The file imports handleShowcaseUpvote at the top but dynamically
imports handleShowcaseModalSubmit inside the event handler; make the pattern
consistent by importing handleShowcaseModalSubmit alongside handleShowcaseUpvote
at the top of src/modules/events.js (add handleShowcaseModalSubmit to the
top-level import from ../commands/showcase.js), remove the dynamic import of
handleShowcaseModalSubmit inside the handler (where it’s currently imported at
line ~426), and ensure the handler invocation still calls
handleShowcaseModalSubmit with the same arguments used previously.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (7)
config.jsonmigrations/009_showcases.cjssrc/api/utils/configAllowlist.jssrc/commands/showcase.jssrc/modules/events.jstests/commands/showcase.test.jsweb/src/components/dashboard/config-editor.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Greptile Review
- GitHub Check: Agent
- GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (7)
**/*.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/api/utils/configAllowlist.jssrc/modules/events.jssrc/commands/showcase.jstests/commands/showcase.test.js
**/*.{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:
src/api/utils/configAllowlist.jssrc/modules/events.jsweb/src/components/dashboard/config-editor.tsxsrc/commands/showcase.jstests/commands/showcase.test.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/api/utils/configAllowlist.jssrc/modules/events.jssrc/commands/showcase.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/events.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/components/dashboard/config-editor.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/components/dashboard/config-editor.tsx
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/showcase.js
🧠 Learnings (5)
📚 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 : Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
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/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/modules/events.jssrc/commands/showcase.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/db.js : Database connection requires PostgreSQL (via pg driver with raw SQL, no ORM); the bot works without a database but config persistence requires PostgreSQL
Applied to files:
migrations/009_showcases.cjs
📚 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/showcase.jstests/commands/showcase.test.js
🧬 Code graph analysis (3)
src/modules/events.js (2)
src/utils/safeSend.js (1)
safeReply(138-145)src/commands/showcase.js (2)
handleShowcaseUpvote(469-554)handleShowcaseModalSubmit(175-244)
src/commands/showcase.js (4)
src/utils/safeSend.js (2)
safeReply(138-145)safeEditReply(178-185)src/modules/config.js (2)
getConfig(282-313)err(94-94)src/logger.js (2)
info(230-232)warn(237-239)src/db.js (1)
getPool(142-147)
tests/commands/showcase.test.js (1)
src/commands/showcase.js (20)
name(194-194)id(387-387)rows(257-257)subcommand(433-433)userId(471-471)buildShowcaseEmbed(67-92)buildUpvoteRow(101-108)embed(70-74)embed(226-226)embed(322-325)embed(362-365)embed(402-402)row(227-227)row(403-403)execute(415-461)handleShowcaseModalSubmit(175-244)showcase(225-225)showcase(401-401)showcase(493-493)handleShowcaseUpvote(469-554)
🔇 Additional comments (11)
src/api/utils/configAllowlist.js (1)
20-20: LGTM!The addition of
'showcase'toSAFE_CONFIG_KEYScorrectly enables API access to the new showcase configuration section, consistent with the existing pattern for other feature modules.config.json (1)
153-154: LGTM!The showcase permission and config additions follow established patterns:
- Default permission
"everyone"is appropriate for a community feature- Disabled by default (
enabled: false) is a safe opt-in approach- Structure mirrors existing community features like
poll,tldr, andafkAlso applies to: 169-171
web/src/components/dashboard/config-editor.tsx (2)
51-51: LGTM!Adding
'showcase'to theknownSectionsarray ensures the type guard properly validates showcase config objects returned from the API.
1203-1203: LGTM!The showcase entry follows the established pattern for community feature toggles, with appropriate label and description that clearly communicates the feature's purpose.
migrations/009_showcases.cjs (1)
10-24: LGTM! Schema design is solid.The table structure appropriately supports the showcase feature:
SERIAL PRIMARY KEYfor auto-incrementing IDsTEXT[]for flexible tech stack storageTIMESTAMPTZ DEFAULT NOW()for proper timezone-aware timestamps- Indexes on
guild_idand(guild_id, author_id)will optimize common query patternstests/commands/showcase.test.js (1)
1-857: LGTM! Comprehensive test coverage.The test suite thoroughly covers:
- Core exports and helper functions
- All subcommand flows (submit, browse, top, view)
- Modal submission handling with edge cases
- Upvote toggle mechanics including self-vote prevention
- Error states (no guild, disabled feature, missing DB)
The mock factories are well-designed and follow consistent patterns.
src/modules/events.js (2)
355-400: LGTM! Handler follows established patterns.The
registerShowcaseButtonHandlercorrectly:
- Checks for button interaction type and customId prefix
- Obtains the DB pool with proper unavailability handling
- Delegates to the command handler
- Provides ephemeral error feedback on failure
- Matches the pattern from
registerPollButtonHandler
402-448: LGTM! Modal handler is well-structured.The
registerShowcaseModalHandlerproperly handles:
- Modal submit interaction filtering by customId
- Database availability check with user feedback
- Error logging with context
- Fallback ephemeral error reply when not yet replied/deferred
src/commands/showcase.js (3)
29-57: LGTM! Well-structured slash command definition.The
SlashCommandBuilderis properly configured with:
- Descriptive command and subcommand names
- Appropriate option types (string for tag, integer for page/id)
- Required flag correctly set for the view ID option
67-92: LGTM! Embed builder with proper length limits.The
buildShowcaseEmbedfunction correctly:
- Uses
.slice()to enforce Discord's embed field limits (256 for title, 4096 for description, 1024 for field values)- Conditionally adds fields only when data exists
- Includes helpful metadata in footer with relative timestamp
469-554: LGTM on upvote logic flow (aside from transaction concern noted separately).The handler correctly:
- Prevents self-upvotes
- Implements toggle behavior (vote/unvote)
- Updates the button label with new count
- Uses ephemeral replies for user feedback
- Gracefully handles message edit failures
There was a problem hiding this comment.
Pull request overview
Implements a new /showcase feature across the bot and dashboard, enabling community members to submit projects, browse/view listings, and toggle upvotes backed by new database tables and event handlers.
Changes:
- Adds
showcases/showcase_votestables via a new migration. - Introduces
/showcasecommand (submit via modal, browse/top/view, upvote button). - Wires new showcase interaction handlers into the events system and exposes the config toggle in the dashboard/API allowlist.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/components/dashboard/config-editor.tsx | Adds showcase to recognized config sections and dashboard feature list. |
| src/commands/showcase.js | New /showcase command implementation, embed/button builders, modal submit + upvote handlers. |
| src/modules/events.js | Registers interaction handlers for showcase upvote buttons and modal submits. |
| src/api/utils/configAllowlist.js | Allows showcase config keys to be returned/edited safely via API. |
| migrations/009_showcases.cjs | Creates DB schema for showcases + votes. |
| config.json | Adds showcase feature toggle and permission entry. |
| tests/commands/showcase.test.js | Adds Vitest coverage for showcase subcommands, modal submit, and upvote toggling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Review Summary
Found 4 warnings and 2 nitpicks across the showcase implementation.
Issues
🟡 Warnings:
- Race condition in upvote toggle (
src/commands/showcase.js:504-545) — The check-then-act vote flow (SELECT → INSERT/DELETE → UPDATE count) is not wrapped in a transaction, allowing concurrent clicks to corrupt the upvote count. - No URL validation (
src/commands/showcase.js:194-198) — User-supplied repo/live URLs are stored without validating they are well-formed http(s) URLs. interaction.channelcan be null (src/commands/showcase.js:229) — After a bot restart, the channel may not be cached, causing an unhandled TypeError.- AGENTS.md / README.md not updated (
migrations/009_showcases.cjs) — Key Files table, Database Tables section, and README command list need updating per project conventions.
🔵 Nitpicks:
5. parseInt on customId can produce NaN (src/commands/showcase.js:470) — Malformed button customIds pass NaN to SQL queries.
6. Discord timestamp in embed footer won't render (src/commands/showcase.js:74) — <t:...:R> only works in message content, not embed footers.
🟡 Structural note:
7. Inconsistent import pattern in events.js (src/modules/events.js:7) — handleShowcaseUpvote is statically imported from a command file while handleShowcaseModalSubmit uses dynamic import. Consider a dedicated src/modules/showcaseHandler.js to match the poll pattern.
There was a problem hiding this comment.
Review Summary — 4 warnings, 3 nitpicks
🟡 Warnings
-
src/commands/showcase.js:483-487—getPool()throw uncaught + dead code —getPool()throws when uninitialized but is called outside the try/catch block. Theif (!pool)guard is unreachable dead code. -
src/commands/showcase.js:34-42— URL validation accepts any protocol —isValidUrl()acceptsjavascript:,data:,file:etc. Should restrict tohttp:/https:. -
src/modules/events.js:434-443— Modal error handler ignores deferred state — IfhandleShowcaseModalSubmitdefers then throws, no error response is sent. User sees "thinking..." until Discord times out. -
migrations/009_showcases.cjs— AGENTS.md not updated — Per project conventions, Database Tables and Key Files sections need new entries.
🔵 Nitpicks
-
src/modules/events.js:366-377, 413-424— Dead null checks ongetPool()— Same deadif (!pool)pattern in both event handlers. -
tests/commands/showcase.test.js— Missing test coverage — No tests for:interaction.channelnull guard, invalid URL rejection, NaN showcaseId guard.
Prompt to fix all issues
Fix these issues on branch feat/showcase-command in VolvoxLLC/volvox-bot:
1. In src/commands/showcase.js around lines 483-487, getPool() throws when uninitialized (never returns falsy), so `if (!pool)` is dead code and the throw is uncaught. Wrap getPool() in try/catch:
let pool;
try { pool = getPool(); }
catch { await safeEditReply(interaction, { content: '❌ Database is not available.' }); return; }
2. In src/commands/showcase.js lines 34-42, isValidUrl() accepts any URL protocol. Add protocol check:
const parsed = new URL(str);
return parsed.protocol === 'https:' || parsed.protocol === 'http:';
3. In src/modules/events.js lines 434-443, the modal error handler only handles !replied && !deferred. Add else branch for deferred case using safeEditReply to send error when interaction.deferred && !interaction.replied.
4. Update AGENTS.md: add showcases and showcase_votes to Database Tables section (lines 136-143), add src/commands/showcase.js to Key Files table.
5. In src/modules/events.js lines 366-377 and 413-424, wrap (await import('../db.js')).getPool() in try/catch instead of the dead if (!pool) check.
6. In tests/commands/showcase.test.js, add tests for: (a) modal submit with interaction.channel = null, (b) modal submit with invalid URL (e.g. 'not-a-url'), (c) upvote with malformed customId like 'showcase_upvote_abc'.
There was a problem hiding this comment.
3 warnings and 2 nitpicks found. The core showcase logic is solid — URL validation, atomic transactions, null guards, and NaN checks were all well-addressed in the fix commits. Remaining issues are in the event handler wiring and documentation.
🟡 Warnings:
src/modules/events.js:366-377, 413-424—getPool()throws when uninitialized but the call is outside any try/catch. Theif (!pool)checks are dead code. Users get no error response when DB is down.migrations/009_showcases.cjs— AGENTS.md Key Files table and Database Tables section not updated per project conventions.
🔵 Nitpicks:
3. src/modules/events.js:428-432 — Modal error handler missing ephemeral: true on safeReply path and no try/catch around reply itself (inconsistent with button handler pattern).
4. tests/commands/showcase.test.js — Missing test coverage for new guard clauses (channel null, invalid URLs, NaN showcaseId).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
getPool() throws when DB is uninitialized (never returns null), so the null-check guard was unreachable and uninitialized-DB errors would propagate uncaught. Replace with try/catch in both registerShowcaseButtonHandler and registerShowcaseModalHandler. Resolves: PRRT_kwDORICdSM5xQ6gA, PRRT_kwDORICdSM5xQ6hV
🛡️ PR Patrol — Code FixedFixed:
Replaced with All 2070 tests ✅ | biome clean ✅ |
There was a problem hiding this comment.
Review Summary — 4 warnings, 3 nitpicks
The core showcase feature is well-implemented — URL validation, atomic transactions, null guards, and NaN checks were all addressed in the fix commits. Remaining issues are in error handling consistency, a missing config gate, and documentation.
🟡 Warnings
src/commands/showcase.js:515— Uses rawinteraction.reply({ flags: 64 })instead ofsafeReplywithephemeral: true, bypassing safeSend sanitizationsrc/commands/showcase.js:520—handleShowcaseUpvotemissingshowcase.enabledconfig check — upvotes work even when feature is disabled (unlikeexecute()andhandleShowcaseModalSubmitwhich both check)src/commands/showcase.js:269-272— IfsafeSendthrows after INSERT, orphaned DB record persists withoutmessage_idsrc/modules/events.js:432-436— Modal error handler missingephemeral: trueon safeReply path and no try/catch around reply (inconsistent with button handler at lines 390-398)
🟡 Documentation
- AGENTS.md & README.md not updated — Missing Key Files entry, Database Tables entries, and README Features entry per project conventions
🔵 Nitpicks
migrations/009_showcases.cjs:38— Missing index onshowcase_votes(showcase_id)tests/commands/showcase.test.js— No tests for channel null guard, invalid URL rejection, or NaN showcaseId guard
Prompt to fix all issues
Fix these issues on branch feat/showcase-command in VolvoxLLC/volvox-bot:
1. In src/commands/showcase.js line 515, replace `await interaction.reply({ content: '❌ Invalid showcase ID.', flags: 64 });` with `await safeReply(interaction, { content: '❌ Invalid showcase ID.', ephemeral: true });`
2. In src/commands/showcase.js, add a showcase.enabled config check at the top of handleShowcaseUpvote (after extracting guildId around line 520):
const guildConfig = getConfig(guildId);
if (guildConfig.showcase?.enabled === false) {
await safeReply(interaction, { content: '❌ The showcase feature is disabled in this server.', ephemeral: true });
return;
}
3. In src/commands/showcase.js lines 269-272, wrap the safeSend + UPDATE in try/catch. On failure, DELETE the orphaned showcase record and reply with error.
4. In src/modules/events.js lines 432-436, replace the modal error handler with:
} catch (err) {
logError('Showcase modal error', { error: err.message });
try {
if (interaction.deferred || interaction.replied) {
await safeEditReply(interaction, { content: '❌ Something went wrong.' });
} else {
await safeReply(interaction, { content: '❌ Something went wrong.', ephemeral: true });
}
} catch {
// Ignore — we tried
}
}
5. Update AGENTS.md: add `src/commands/showcase.js` to Key Files table (around line 64), add `showcases` and `showcase_votes` to Database Tables section (around line 143).
6. Update README.md: add a showcase feature entry in the Features section (around line 18), e.g.:
- **🏆 Project Showcase** — Submit, browse, and upvote community projects with `/showcase`. Includes tech stack filtering and a top-10 leaderboard.
7. In migrations/009_showcases.cjs after line 38, add:
pgm.sql('CREATE INDEX IF NOT EXISTS idx_showcase_votes_showcase ON showcase_votes(showcase_id)');
8. In tests/commands/showcase.test.js, add tests for:
(a) handleShowcaseModalSubmit with interaction.channel = null — should editReply with error
(b) handleShowcaseModalSubmit with invalid URL like 'not-a-url' — should reply with URL error
(c) handleShowcaseUpvote with malformed customId 'showcase_upvote_abc' — should reply with invalid ID error
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/showcase.js`:
- Around line 244-273: The code inserts a showcases row before verifying the
channel and before safeSend succeeds, which can create orphaned DB records;
update the flow in the function that calls pool.query(...) for the INSERT (the
block that creates showcase, calls buildShowcaseEmbed and buildUpvoteRow and
uses safeSend/safeEditReply) so that you confirm interaction.channel exists and
safeSend completes successfully before persisting the showcase row (or
alternatively perform the INSERT inside a transaction and only COMMIT after
safeSend, rolling back or deleting the inserted row on send failure), and ensure
you set message_id via the UPDATE to the same showcase.id only after a
successful send.
- Around line 514-517: The invalid showcase ID branch currently calls
interaction.reply directly; replace that call with the project's safe send
wrapper (e.g., safeReply or safeSend used elsewhere in this file) to ensure
mentions are sanitized and allowedMentions enforced. Locate the branch that
checks Number.isNaN(showcaseId) and swap the interaction.reply(...) for the
equivalent await safeReply(interaction, { content: '❌ Invalid showcase ID.',
flags: 64 }) (or safeSend with the same options) so behavior and ephemeral flag
remain unchanged.
In `@src/modules/events.js`:
- Around line 361-365: In registerShowcaseButtonHandler (the
Events.InteractionCreate handler) gate processing by calling
getConfig(interaction.guildId) at the top of the button branch and check the
showcase feature flag (e.g., config.showcase.enabled); if the flag is false,
immediately reply ephemerally (disabled message) and return so no DB/upvote
mutations run; otherwise continue with the existing
customId.startsWith('showcase_upvote_') logic and DB work.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
migrations/009_showcases.cjssrc/commands/showcase.jssrc/modules/events.jstests/commands/showcase.test.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Greptile Review
- GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (5)
**/*.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/showcase.jstests/commands/showcase.test.jssrc/modules/events.js
**/*.{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:
src/commands/showcase.jstests/commands/showcase.test.jssrc/modules/events.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/showcase.jssrc/modules/events.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/showcase.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/events.js
🧠 Learnings (8)
📚 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/showcase.jssrc/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/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/showcase.jssrc/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/modules/moderation.js : Case numbering must be per-guild sequential, assigned atomically using COALESCE(MAX(case_number), 0) + 1 in a single INSERT statement
Applied to files:
src/commands/showcase.jsmigrations/009_showcases.cjs
📚 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 : Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Applied to files:
src/commands/showcase.jssrc/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/commands/showcase.test.js
📚 Learning: 2026-02-27T16:24:15.055Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.055Z
Learning: Update AGENTS.md, README.md, CONTRIBUTING.md, .env.example, and config.json documentation whenever code changes are made (new commands, modules, env vars, or architecture changes)
Applied to files:
migrations/009_showcases.cjs
📚 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/db.js : Database connection requires PostgreSQL (via pg driver with raw SQL, no ORM); the bot works without a database but config persistence requires PostgreSQL
Applied to files:
migrations/009_showcases.cjssrc/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/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
🧬 Code graph analysis (2)
tests/commands/showcase.test.js (1)
src/commands/showcase.js (26)
name(213-213)id(427-427)rows(297-297)subcommand(473-473)userId(519-519)data(47-75)data(47-75)buildShowcaseEmbed(85-111)buildUpvoteRow(120-127)embed(88-93)embed(261-261)embed(362-365)embed(402-405)embed(442-442)row(262-262)row(443-443)execute(455-502)handleShowcaseModalSubmit(194-284)showcase(260-260)showcase(441-441)showcase(541-541)pool(244-258)pool(386-393)pool(429-432)pool(483-483)handleShowcaseUpvote(510-617)
src/modules/events.js (2)
src/commands/showcase.js (7)
client(556-556)pool(244-258)pool(386-393)pool(429-432)pool(483-483)handleShowcaseUpvote(510-617)handleShowcaseModalSubmit(194-284)src/utils/safeSend.js (2)
safeReply(138-145)safeEditReply(178-185)
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/commands/showcase.js (1)
244-273:⚠️ Potential issue | 🟠 MajorOrphaned DB records when channel is unavailable.
The showcase record is inserted at line 244 before validating
interaction.channelat line 264. If the channel is null, the function returns early, leaving an orphaned record in the database with no associated message.Move the channel check before the INSERT, or wrap in a transaction that rolls back on send failure.
,🛠️ Proposed fix: check channel before insert
await interaction.deferReply({ ephemeral: true }); + if (!interaction.channel) { + await safeEditReply(interaction, { content: '❌ Cannot post in this channel.' }); + return; + } + const { rows } = await pool.query( `INSERT INTO showcases (guild_id, author_id, name, description, tech_stack, repo_url, live_url, channel_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`, @@ const showcase = rows[0]; const embed = buildShowcaseEmbed(showcase); const row = buildUpvoteRow(showcase.id, 0); - if (!interaction.channel) { - await safeEditReply(interaction, { content: '❌ Cannot post in this channel.' }); - return; - } - const msg = await safeSend(interaction.channel, { embeds: [embed], components: [row] });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/showcase.js` around lines 244 - 273, The code inserts a showcase via pool.query before verifying interaction.channel, which can leave orphaned DB rows if posting fails; fix by moving the channel availability check (interaction.channel) before the INSERT, or wrap the INSERT + safeSend + UPDATE in a DB transaction so you can rollback on send failure (use the same pool client for BEGIN/COMMIT/ROLLBACK around the queries), ensuring the safeSend call and subsequent UPDATE to set message_id only run after a successful send; reference pool.query, interaction.channel, safeSend, safeEditReply, buildShowcaseEmbed, buildUpvoteRow, and the UPDATE that sets message_id when applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/commands/showcase.js`:
- Around line 244-273: The code inserts a showcase via pool.query before
verifying interaction.channel, which can leave orphaned DB rows if posting
fails; fix by moving the channel availability check (interaction.channel) before
the INSERT, or wrap the INSERT + safeSend + UPDATE in a DB transaction so you
can rollback on send failure (use the same pool client for BEGIN/COMMIT/ROLLBACK
around the queries), ensuring the safeSend call and subsequent UPDATE to set
message_id only run after a successful send; reference pool.query,
interaction.channel, safeSend, safeEditReply, buildShowcaseEmbed,
buildUpvoteRow, and the UPDATE that sets message_id when applying the change.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
src/commands/showcase.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Greptile Review
- GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (4)
**/*.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/showcase.js
**/*.{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:
src/commands/showcase.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/showcase.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/showcase.js
🧠 Learnings (5)
📚 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/showcase.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/showcase.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/showcase.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/moderation.js : Case numbering must be per-guild sequential, assigned atomically using COALESCE(MAX(case_number), 0) + 1 in a single INSERT statement
Applied to files:
src/commands/showcase.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 : Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Applied to files:
src/commands/showcase.js
🔇 Additional comments (2)
src/commands/showcase.js (2)
455-502: LGTM!The execute function properly validates server context and feature flag, handles the modal-based submit flow separately, and wraps database access in try/catch. Error handling logs structured metadata and provides user-friendly error messages.
561-606: LGTM!The vote toggle is now properly wrapped in a transaction with BEGIN/COMMIT/ROLLBACK, addressing the race condition concern. The client is correctly released in the
finallyblock.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Implements a
/showcasecommand system for community members to submit, browse, and upvote projects.What's included
migrations/009_showcases.cjs):showcasesandshowcase_votestables with indexes/showcase submit: Modal with 5 fields (name, description, tech stack, repo URL, live URL) — saves to DB and posts embed with upvote button/showcase browse [tag]: Paginated list (5/page), optional tech stack filter/showcase top: Top 10 most upvoted projects/showcase view <id>: Full project details with upvote buttonshowcase_upvote_<id>), prevents self-upvote, updates count in real-timeregisterShowcaseButtonHandlerandregisterShowcaseModalHandlerfollowing poll patternshowcase.enabledtoggle,showcase: everyonepermission,showcaseinSAFE_CONFIG_KEYSTest Results
Lint
Closes #50