feat: daily coding challenge system (#52)#115
Conversation
- Add migration 011 for challenge_solves table (guild_id, challenge_index, user_id, solved_at) - Add 32 challenge seed problems (10 easy, 12 medium, 8 hard) covering classic CS problems - Add challengeScheduler.js module with: - Timezone-aware time/date helpers - Daily challenge selection via day-of-year % challenges.length - Auto-posting embed with difficulty colors and discussion thread - Double-post prevention via in-memory date tracking - handleSolveButton / handleHintButton for button interactions - Add /challenge command with today, streak, and leaderboard subcommands - Register challenge_solve_ and challenge_hint_ button handlers in events.js - Wire checkDailyChallenge into existing 60s scheduler poll loop - Add challenges config section (enabled, channelId, postTime, timezone) - Add 'challenges' to SAFE_CONFIG_KEYS allowlist - Add 'challenge': 'everyone' to permissions.commands - Add challenges config panel to web dashboard - Add tests: 98 test files, 2075 tests passing, 0 lint errors Closes #52
|
Caution Review failedThe pull request is closed. ℹ️ Recent review infoConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (11)
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughIntroduces a comprehensive daily coding challenge feature for Discord, including configuration sections, database schema, challenge data, scheduler with posting logic, slash command with subcommands for viewing challenges and leaderboards, event handlers for user interactions, and dashboard UI for configuration. Changes
Possibly related PRs
✨ 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 |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
Review Summary — 3 critical, 2 warnings, 1 nitpick
🔴 Critical
row.totalstring/number type mismatch (src/commands/challenge.js:203) — PostgreSQL COUNT returns a string; strict!== 1always evaluates true, so "1 solve" incorrectly displays as "1 solves"handleSolveButtonmissing bounds validation (src/modules/challengeScheduler.js:294) — UnlikehandleHintButton, this function doesn't verifychallengeIndexexists in the CHALLENGES array before inserting into the database, allowing invalid indices to pollute data- Challenge system stagnates after 32 days (
migrations/011_challenges.cjs:14) — The composite PK(guild_id, challenge_index, user_id)+ON CONFLICT DO NOTHINGmeans once all 32 challenges cycle, re-solves are silently discarded. Streaks and leaderboards freeze permanently
🟡 Warning
4. Streak calculation ignores index wrap-around (src/commands/challenge.js:124-130) — Consecutive days spanning the 31→0 boundary produce a difference of 31, not 1, breaking the streak
5. AGENTS.md not updated — New files (challenge.js, challengeScheduler.js, challenges.json) and the challenge_solves table are missing from the Key Files and Database Tables sections
🔵 Nitpick
6. lastPostedDate Map never pruned (src/modules/challengeScheduler.js:34) — Grows unboundedly with guilds
There was a problem hiding this comment.
Review Summary — 3 critical, 2 warnings, 1 nitpick
🔴 Critical
row.totalstring/number type mismatch (src/commands/challenge.js:203) — PostgreSQL COUNT returns a string; strict!== 1always evaluates true, so "1 solve" incorrectly displays as "1 solves"handleSolveButtonmissing bounds validation (src/modules/challengeScheduler.js:294) — UnlikehandleHintButton, this function doesn't verifychallengeIndexexists in the CHALLENGES array before inserting into the database, allowing invalid indices to pollute data- Challenge system stagnates after 32 days (
migrations/011_challenges.cjs:14) — The composite PK(guild_id, challenge_index, user_id)+ON CONFLICT DO NOTHINGmeans once all 32 challenges cycle, re-solves are silently discarded. Streaks and leaderboards freeze permanently
🟡 Warning
4. Streak calculation ignores index wrap-around (src/commands/challenge.js:124-130) — Consecutive days spanning the 31→0 boundary produce a difference of 31, not 1, breaking the streak
5. AGENTS.md not updated — New files (challenge.js, challengeScheduler.js, challenges.json) and the challenge_solves table are missing from the Key Files and Database Tables sections
🔵 Nitpick
6. lastPostedDate Map never pruned (src/modules/challengeScheduler.js:34) — Grows unboundedly with guilds
|
| Filename | Overview |
|---|---|
| migrations/011_challenges.cjs | created challenge_solves table with composite PK (guild_id, challenge_date, user_id) and index, enables date-based streak calculation |
| src/modules/challengeScheduler.js | implemented timezone-aware scheduler with day-of-year cycling, auto-posting, thread creation, and button handlers for hints/solves |
| src/commands/challenge.js | added /challenge command with today/streak/leaderboard subcommands; streak calculation has timezone mismatch with stored data |
| src/modules/events.js | registered challenge button handler for solve/hint interactions with proper error handling |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[60s Scheduler Loop] -->|Check| B[checkDailyChallenge]
B -->|For each guild| C{Challenges enabled?}
C -->|Yes| D{Time to post?}
C -->|No| E[Skip]
D -->|Yes| F[postDailyChallenge]
D -->|Not yet| E
F --> G[Select challenge by dayOfYear mod length]
G --> H[Send embed with buttons to channel]
H --> I[Create discussion thread]
J[User clicks Mark Solved] --> K[handleSolveButton]
K --> L[Validate challenge index bounds]
L --> M[Get guild timezone]
M --> N[Upsert to challenge_solves table]
N --> O[Update embed with new solve count]
O --> P[Reply with user total solves]
Q[User clicks Hint] --> R[handleHintButton]
R --> S[Show hints ephemerally]
T[Command: challenge streak] --> U[Query challenge_solves by date]
U --> V[Calculate consecutive days]
V --> W[Display streak and total]
X[Command: challenge leaderboard] --> Y[Query top 10 this week]
X --> Z[Query top 10 all-time]
Y --> AA[Display leaderboards]
Z --> AA
AB[Web Dashboard] --> AC[Configure channel, time, timezone]
AC --> AD[Save to config]
Last reviewed commit: b330657
There was a problem hiding this comment.
Pull request overview
Implements a daily coding challenge feature set for the Discord bot, including scheduled posting, solve tracking, and dashboard/config support, backed by a new database table.
Changes:
- Adds
challenge_solvespersistence plus a scheduler module to post daily challenge embeds with hint/solve buttons. - Introduces
/challengecommand (today/streak/leaderboard) and registers button interaction handlers. - Extends config surfaces (bot
config.json, API allowlist, and web dashboard config editor) and adds Vitest coverage.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/components/dashboard/config-editor.tsx | Adds a “Daily Coding Challenges” section to the web dashboard editor and recognizes challenges as a config section. |
| tests/modules/challengeScheduler.test.js | Adds unit tests for challenge scheduling helpers, embed/buttons, posting, and button handlers. |
| tests/commands/challenge.test.js | Adds tests covering /challenge subcommands and DB error handling. |
| src/modules/scheduler.js | Hooks daily challenge checks into the existing 60s scheduler poll loop. |
| src/modules/events.js | Registers an interactionCreate handler for hint/solve buttons. |
| src/modules/challengeScheduler.js | New module implementing daily selection logic, posting flow, and hint/solve button behavior. |
| src/data/challenges.json | Adds the seeded challenge catalog used by the scheduler/command. |
| src/commands/challenge.js | New /challenge command implementation (today/streak/leaderboard). |
| src/api/utils/configAllowlist.js | Allowlists the new challenges config section for the API. |
| migrations/011_challenges.cjs | Adds DB table for solve tracking and an index. |
| config.json | Adds challenges config defaults and enables the challenge command permission. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- fix: row.total from COUNT(*) is a pg bigint string; convert with Number() before pluralisation comparison in leaderboard formatter - fix: add CHALLENGES[challengeIndex] bounds check to handleSolveButton (mirrors the guard already present in handleHintButton) - fix: add solved_date DATE column to challenge_solves PK so challenges can be solved again after the 32-day modular cycle repeats; update ON CONFLICT clause to match new 4-column key Fixes PRRT_kwDORICdSM5xQx3i, PRRT_kwDORICdSM5xQx5S, PRRT_kwDORICdSM5xQx8o
There was a problem hiding this comment.
Review Summary — 2 critical, 5 warnings, 3 nitpicks
🔴 Critical
-
Column name mismatch breaks solve tracking at runtime (
src/modules/challengeScheduler.js:312-317) — The INSERT referencessolved_datewhich doesn't exist (column ischallenge_date), and the ON CONFLICT targets a non-existent constraint. Every "Mark Solved" button press will throw a PostgreSQL error. -
Streak calculation is fundamentally broken (
src/commands/challenge.js:115-136) — Orders bychallenge_index DESCand checks consecutive indices. Challenge indices don't correlate with temporal order (they'redayOfYear % 33), and wrap-around from 32→0 produces a difference of 32, not 1. Should usechallenge_datefor consecutive-day streak logic.
🟡 Warning
-
Solve count queries not scoped to date (
src/commands/challenge.js:73-76,src/modules/challengeScheduler.js:327-330) — Both count bychallenge_indexonly without filtering bychallenge_date. Once challenges cycle, counts will include historical solves from prior occurrences. -
Strict time equality risks missing daily post (
src/modules/challengeScheduler.js:260) —currentTime !== postTimemeans if the 60s poll doesn't fire during the exact configured minute, the post is missed for the entire day. Use>=with the existing double-post guard. -
DST-sensitive date arithmetic (
src/modules/challengeScheduler.js:57-63) —Dateobjects constructed from strings without timezone offset are parsed in server-local time. Around DST boundaries this can cause off-by-one in day-of-year. UseDate.UTC()instead. -
AGENTS.md not updated — New files,
challenge_solvestable, and challenge cycling behavior need to be documented per project conventions. -
Embed footer mentions "React ✅" (
src/modules/challengeScheduler.js:148) — No reaction handler exists; only button handlers are wired. Misleading to users.
🔵 Nitpick
-
Redundant index (
migrations/011_challenges.cjs:29-32) —idx_challenge_solves_guildon(guild_id)is already covered by the PK prefix. A(guild_id, solved_at)composite would be more useful for leaderboard queries. -
Challenge count mismatch (
src/data/challenges.json) — JSON has 33 entries but PR description says 32; test atchallengeScheduler.test.js:209also hardcodes% 32. -
lastPostedDatenot persisted (src/modules/challengeScheduler.js:34) — Bot restart loses tracking, could double-post on the same day.
AI prompt to fix all issues
You are working on the `feat/daily-challenge` branch of VolvoxLLC/volvox-bot.
Fix the following issues in order:
1. **src/modules/challengeScheduler.js:312-317** — The INSERT statement references column `solved_date` which does not exist. The correct column name is `challenge_date`. Also the ON CONFLICT clause targets `(guild_id, challenge_index, user_id, solved_date)` which doesn't match the PK `(guild_id, challenge_date, user_id)`. Fix the INSERT to:
INSERT INTO challenge_solves (guild_id, challenge_date, challenge_index, user_id)
VALUES ($1, CURRENT_DATE, $2, $3)
ON CONFLICT (guild_id, challenge_date, user_id) DO NOTHING
2. **src/commands/challenge.js:115-136** — Streak calculation orders by `challenge_index DESC` and checks consecutive indices. Replace with a query that selects `DISTINCT challenge_date ... ORDER BY challenge_date DESC` and computes streak from consecutive calendar dates (difference of 1 day between adjacent rows).
3. **src/commands/challenge.js:73-76** — Add `AND challenge_date = CURRENT_DATE` to the solve count WHERE clause.
4. **src/modules/challengeScheduler.js:327-330** — Add `AND challenge_date = CURRENT_DATE` to the solve count WHERE clause.
5. **src/modules/challengeScheduler.js:260** — Change `if (currentTime !== postTime) return;` to `if (currentTime < postTime) return;` so the post is not missed if the poll loop drifts past the exact minute.
6. **src/modules/challengeScheduler.js:57-63** — Replace the Date arithmetic with UTC-based math:
const startOfYearUtcMs = Date.UTC(year, 0, 1);
const localDateUtcMs = Date.UTC(year, month - 1, day);
const diffMs = localDateUtcMs - startOfYearUtcMs;
7. **src/modules/challengeScheduler.js:148** — Change footer text from "React ✅ or click Mark Solved" to "Click ✅ Mark Solved when you've got it!"
8. **migrations/011_challenges.cjs:29-32** — Change the index to `ON challenge_solves(guild_id, solved_at)` for better leaderboard query support.
9. **src/data/challenges.json** — The file has 33 entries. Update the PR description to say 33 or remove one entry. Also fix test assertion at tests/modules/challengeScheduler.test.js:209 from `% 32` to `% CHALLENGES.length` or `% 33`.
10. **AGENTS.md** — Add to Key Files table: `src/commands/challenge.js` (Daily challenge command), `src/modules/challengeScheduler.js` (Challenge scheduler, embed builder, button handlers), `src/data/challenges.json` (Challenge seed data). Add to Database Tables: `challenge_solves` — Daily challenge solve tracking per guild/user/date. Add to Common Pitfalls: Challenge cycling — challenges repeat every `CHALLENGES.length` days via day-of-year modulo.
11. Update tests in tests/commands/challenge.test.js and tests/modules/challengeScheduler.test.js to match the corrected query shapes (challenge_date column, date-based streak, CURRENT_DATE filters).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Daily Coding Challenge System
Closes #52
What's in this PR
Migration (
migrations/011_challenges.cjs)challenge_solvestable with composite primary key(guild_id, challenge_index, user_id)guild_idfor efficient leaderboard queriesChallenge Seed (
src/data/challenges.json)Challenge Scheduler (
src/modules/challengeScheduler.js)Intl.DateTimeFormatdayOfYear % challenges.lengthscheduler.jsChallenge Embed
🧩 Daily Challenge #<day> — <title>/challenge command (
src/commands/challenge.js)/challenge today— Re-posts today's challenge/challenge streak— User's consecutive solve streak + total/challenge leaderboard— Top 10 this week + all-timeButton Handlers (registered in
src/modules/events.js)challenge_solve_<index>— Upserts solve, replies with total count (ephemeral)challenge_hint_<index>— Shows all hints ephemerallyConfig
challenges: { enabled, channelId, postTime, timezone }toconfig.json'challenges'toSAFE_CONFIG_KEYS"challenge": "everyone"topermissions.commandsTests
Lint