fix: consistent param validation, index hygiene, async cleanup, a11y, and test coverage - #334
Conversation
… and test coverage for contest module
- Added validateParams middleware + contestIdParamSchema, applied to
DELETE /reminders/:contestId and POST /reminders/:contestId/notified.
Both now enforce the same positive-integer contract as POST
/reminders instead of a manual parseInt(...) that permissively
accepted strings like '12abc'.
- Removed the redundant standalone contestId index on Contest — the
compound unique {platform, contestId} index already covers every
query pattern in the codebase. Documented the rationale in
server/modules/contests/INDEXES.md and added a migration script
(dropRedundantContestIndex.js) since a schema change alone never
drops an existing production index.
- useContests now guards against stale/superseded async responses via
a per-fetch sequence token, and invalidates in-flight requests on
unmount, matching the cancellation pattern already used elsewhere
(bell, notifier).
- ContestReminderBell now has a dynamic aria-label (including the
due-soon count) instead of relying on title alone, and marks its
decorative icon/badge aria-hidden.
- Added 28 new tests (46 backend total, 28 frontend total) covering
the validation contract, reminder lifecycle edge cases, repository
idempotency/joins/cleanup, the stale-response race condition, and
bell accessibility (accessible name, count, keyboard focus) — using
the project's existing test tooling (node:test, vitest), no new
testing stack introduced.
Closes kunalverma2512#279
|
@ida-jemi is attempting to deploy a commit to the Kunal Verma's projects Team on Vercel. A member of the Team first needs to authorize it. |
🎉 Welcome to CodeLens — Thank You for Your Contribution!Hey @ida-jemi! 👋 We are genuinely excited to have you here. Every single PR — big or small — makes CodeLens better, and yours is no exception. Take a moment to review the checklist below to help us merge your work quickly and smoothly. ✅ Before Requesting a Review
💬 Join Our Community Channel — This is MandatoryBeing part of our communication channel is compulsory for all contributors, not optional. Why join? This is where all important announcements, PR review updates, contribution discussions, and maintainer decisions happen in real time. Contributors who are not in the channel regularly miss critical context and updates, which often leads to duplicated or misaligned work. Staying connected here is what keeps the community strong and your contributions impactful. We are rooting for you! If you have any questions, drop them in the channel or comment right here on this PR. Let's build something great together. 🚀✨ |
📝 WalkthroughWalkthroughThis PR hardens the contest tracker with route-level ID validation, stale-request protection, reminder accessibility updates, contest index cleanup, and frontend and backend automated tests. ChangesFrontend contest behavior
Backend contest validation and index management
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
server/modules/contests/validation.test.js (1)
13-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd direct
validateParamsmiddleware tests.These tests validate schemas only. Add tests that verify an invalid parameter returns
400without callingnext, and that a valid string parameter reachesnextas a numericreq.params.contestId.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/modules/contests/validation.test.js` around lines 13 - 65, Add direct validateParams middleware tests alongside the existing schema tests: verify an invalid contestId produces a 400 response without invoking next, and verify a valid string contestId invokes next with req.params.contestId converted to a number. Reuse the existing contest validation schemas and middleware setup symbols rather than testing schema.safeParse alone.frontend/src/hooks/useContests.test.js (1)
114-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStrengthen or reframe the post-unmount test; it cannot currently detect a regression.
This test unmounts the hook, then resolves the pending fetch, and asserts nothing about
result.currentor about any spy. React 18 removed the "Can't perform a React state update on an unmounted component" warning: We've removed a warning when you call setState on an unmounted component. An update to an unmounted component is now a silent no-op with no re-render, no warning, and no thrown error, whether or not thecontestsRequestId/remindersRequestIdguard inuseContests.js(Lines 72-80) exists.Since
result.currentonly reflects the last re-render, and no re-render occurs for a truly unmounted tree, this test passes identically whether or not the requestId invalidation logic is removed. It does not verify the guard it claims to test.Consider one of these:
- Add a
console.errorspy assertion to make the "no warning/no crash" claim explicit in the test, even though it can't discriminate the guard's presence.- Rely on the existing stale-response race test (Lines 86-112) as the actual regression check for this guard, and reword this test's description to state it only verifies "no crash on late resolution after unmount," not "the response was ignored."
🧪 Example of making the smoke-test intent explicit
it("does not apply a fetch response that resolves after unmount", async () => { let resolveFetch; mockGetUpcomingCodeforcesContests.mockReturnValue( new Promise((resolve) => { resolveFetch = resolve; }) ); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const { unmount } = renderHook(() => useContests()); unmount(); // Should not throw / cause an act() warning even though state would // otherwise be updated after unmount. await act(async () => { resolveFetch({ data: { data: [CONTEST_A] } }); }); + + expect(errorSpy).not.toHaveBeenCalled(); + errorSpy.mockRestore(); });Since React 18+ no-ops these updates unconditionally, this doesn't restore the test's ability to catch a removed guard; it only documents intent. The real regression coverage for the requestId contract is the stale-response race test above it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/hooks/useContests.test.js` around lines 114 - 128, Reframe the test description and comments around the unmount flow in useContests to state that it only verifies late resolution causes no crash or warning, rather than claiming it validates response suppression. Keep the existing stale-response race test as coverage for the contestsRequestId/remindersRequestId guard, and optionally add an explicit console.error spy assertion if retaining the no-warning claim.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@server/modules/contests/INDEXES.md`:
- Around line 9-13: Update the index inventory to include Contest.js’s
standalone { platform: 1 } index and justify its platform-only query coverage,
or remove that index from both the schema and deployed databases. Correct the {
platform: 1, contestId: 1 } entry so its explanation only claims support for
platform-prefixed queries, not phase-filtered queries, and revise the phase
index description accordingly.
In `@server/modules/contests/validation.js`:
- Around line 39-50: Update the contestId validation in addReminderSchema and
contestIdParamSchema to reject booleans, arrays, and other non-string/non-number
inputs before numeric coercion, while preserving integer and positive-value
checks. Add regression tests covering boolean and array contestId values in the
add-reminder body.
In `@server/scripts/dropRedundantContestIndex.js`:
- Around line 5-15: Update the migration documentation and the script’s Usage
comment to consistently use the repository-root command `node
server/scripts/dropRedundantContestIndex.js`, or explicitly state that the
command must be run from the server directory; ensure both documented locations
follow the same convention.
- Around line 19-27: Update the migration around the standalone index lookup to
first verify that a unique compound index on `{ platform: 1, contestId: 1 }`
exists. Abort before `collection.dropIndex(standalone.name)` when the
replacement index is missing, while preserving the existing drop behavior when
both indexes are present.
---
Nitpick comments:
In `@frontend/src/hooks/useContests.test.js`:
- Around line 114-128: Reframe the test description and comments around the
unmount flow in useContests to state that it only verifies late resolution
causes no crash or warning, rather than claiming it validates response
suppression. Keep the existing stale-response race test as coverage for the
contestsRequestId/remindersRequestId guard, and optionally add an explicit
console.error spy assertion if retaining the no-warning claim.
In `@server/modules/contests/validation.test.js`:
- Around line 13-65: Add direct validateParams middleware tests alongside the
existing schema tests: verify an invalid contestId produces a 400 response
without invoking next, and verify a valid string contestId invokes next with
req.params.contestId converted to a number. Reuse the existing contest
validation schemas and middleware setup symbols rather than testing
schema.safeParse alone.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 02b9bdba-dcc9-4873-93e4-c8a194767806
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (15)
frontend/package.jsonfrontend/src/components/contests/ContestCountdown.test.jsxfrontend/src/components/contests/ContestReminderBell.jsxfrontend/src/components/contests/ContestReminderBell.test.jsxfrontend/src/hooks/useContests.jsfrontend/src/hooks/useContests.test.jsserver/models/Contest.jsserver/modules/contests/INDEXES.mdserver/modules/contests/controller.jsserver/modules/contests/repository.test.jsserver/modules/contests/routes.jsserver/modules/contests/service.test.jsserver/modules/contests/validation.jsserver/modules/contests/validation.test.jsserver/scripts/dropRedundantContestIndex.js
| | Index | Backs | | ||
| |---|---| | ||
| | `{ platform: 1, contestId: 1 }` (unique) | `findByContestId` (repository.js), `bulkUpsertContests`'s upsert filter — both always filter on `platform` + `contestId` together. Also enforces the one-document-per-(platform, contestId) invariant. Its leftmost prefix (`platform`) also serves any query that filters on `platform` alone. | | ||
| | `{ phase: 1 }` | `getUpcomingContests`, `getNonFinishedContestIds`, `pruneStaleReminders`, `getActiveReminderContests` all filter on `phase` (combined with `platform`, which the compound index above already indexes as a prefix — see note below). | | ||
| | `{ startTimeSeconds: 1 }` | `getUpcomingContests`'s sort, and range queries if added later (e.g. "contests starting in the next N hours"). | |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
Make the index inventory match Contest.js.
server/models/Contest.js Line 15 still declares a standalone { platform: 1 } index, but this table omits it. Also, { platform: 1, contestId: 1 } contains no phase key; its leftmost prefix covers platform only. List and justify { platform: 1 }, or remove it from the schema and deployed databases. Rewrite the Line 12 explanation to describe the actual index support.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/modules/contests/INDEXES.md` around lines 9 - 13, Update the index
inventory to include Contest.js’s standalone { platform: 1 } index and justify
its platform-only query coverage, or remove that index from both the schema and
deployed databases. Correct the { platform: 1, contestId: 1 } entry so its
explanation only claims support for platform-prefixed queries, not
phase-filtered queries, and revise the phase index description accordingly.
| export const addReminderSchema = z.object({ | ||
| contestId: z.coerce.number().int().positive(), | ||
| }); | ||
|
|
||
| // Same positive-integer contract as addReminderSchema, applied at the | ||
| // route boundary for :contestId params instead of a request body. Keeping | ||
| // this separate from addReminderSchema (rather than reusing it directly) | ||
| // makes the two independently evolvable if param- and body-level rules | ||
| // ever need to diverge (e.g. bounds specific to one context). | ||
| export const contestIdParamSchema = z.object({ | ||
| contestId: z.coerce.number().int().positive(), | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For Zod 4.3.6, does z.coerce.number() use JavaScript Number() coercion and accept inputs such as true and [42]?
💡 Result:
Yes, in Zod 4.3.6, z.coerce.number relies on the JavaScript Number constructor for coercion [1][2]. Because it uses Number internally, it accepts the same inputs that JavaScript's native Number function converts [1]. The behavior for the requested inputs is: 1. true: Coerces to 1 [3][1]. 2.: Coerces to 42. In JavaScript, an array containing a single number is coerced to a string "42" and then to the number 42 by the Number constructor. Official test cases for Zod 4.3.6 confirm these behaviors [3]. You can verify this by running: const schema = z.coerce.number; schema.parse(true); // 1 schema.parse; // 42
Citations:
- 1: https://mintlify.com/colinhacks/zod/api/utilities/coerce
- 2: z.coerce.number() defaults empty strings to 0 colinhacks/zod#2461
- 3: https://cdn.jsdelivr.net/npm/zod@4.3.6/src/v4/classic/tests/coerce.test.ts
🏁 Script executed:
#!/bin/bash
set -euo pipefail
if [ -x "$(command -v gh)" ]; then
echo "GitHub CLI: available"
gh --version | head -1
else
echo "GitHub CLI: unavailable"
fi
echo "--- package manifests with zod versions ---"
for f in package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lockb; do
[ -e "$f" ] && echo "### $f" && rg -n '"?zod"?|zod@|version:' "$f" | head -80 || true
done
echo "--- target file excerpt ---"
if [ -f server/modules/contests/validation.js ]; then
cat -n server/modules/contests/validation.js | sed -n '1,120p'
else
echo "validation.js not found"
fi
echo "--- route/body usage references ---"
rg -n "addReminderSchema|contestIdParamSchema|z\.coerce\.number|addReminder" server -g '*.{js,ts,mjs,cjs}' || true
echo "--- local zod behavior probe (if package is present) ---"
node - <<'JS'
try {
const z = require('./node_modules/zod');
let schemaV4, schemaV3, coerce3;
try {
schemaV4 = z.v4.z.coerce.number;
console.log("zod version:", require('./node_modules/zod/package.json').version);
console.log("v4 true parse:", schemaV4.parse(true));
console.log("v4 [42] parse:", schemaV4.parse([42]));
} catch (err) {
console.log("No zod 4 available at require path", err && err.message);
schemaV3 = z.coerce.number;
console.log("v3 true parse:", schemaV3.safeParse(true));
console.log("v3 [42] parse:", schemaV3.safeParse([42]));
}
} catch (e) {
console.log("Could not run local zod probe:", e && e.message);
}
JSRepository: kunalverma2512/CodeLens
Length of output: 5693
Reject non-numeric input types before coercion.
z.coerce.number() uses JavaScript coercion, so the add-reminder body can accept true as 1 and [42] as 42. Constrain contestId to strings or numbers before applying int().positive(), and add regression tests for boolean and array values.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/modules/contests/validation.js` around lines 39 - 50, Update the
contestId validation in addReminderSchema and contestIdParamSchema to reject
booleans, arrays, and other non-string/non-number inputs before numeric
coercion, while preserving integer and positive-value checks. Add regression
tests covering boolean and array contestId values in the add-reminder body.
| /** | ||
| * One-off migration: drops the standalone `contestId_1` index on the | ||
| * contests collection, if present. Mongoose schema changes never | ||
| * automatically drop existing indexes in production — this must be run | ||
| * explicitly against each environment (staging, production) after the | ||
| * schema change deploys. | ||
| * | ||
| * Safe to run multiple times — it's a no-op if the index is already gone. | ||
| * | ||
| * Usage: node scripts/dropRedundantContestIndex.js | ||
| */ |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use one documented working directory.
INDEXES.md Line 35 uses node server/scripts/dropRedundantContestIndex.js, while Line 14 uses node scripts/dropRedundantContestIndex.js. The second command works only from the server directory. State the working directory or use the repository-root command in both locations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/scripts/dropRedundantContestIndex.js` around lines 5 - 15, Update the
migration documentation and the script’s Usage comment to consistently use the
repository-root command `node server/scripts/dropRedundantContestIndex.js`, or
explicitly state that the command must be run from the server directory; ensure
both documented locations follow the same convention.
| const indexes = await collection.indexes(); | ||
|
|
||
| const standalone = indexes.find( | ||
| (idx) => JSON.stringify(idx.key) === JSON.stringify({ contestId: 1 }) | ||
| ); | ||
|
|
||
| if (standalone) { | ||
| await collection.dropIndex(standalone.name); | ||
| console.log(`Dropped redundant index: ${standalone.name}`); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
: "${MONGO_URI:?Set MONGO_URI for the target environment}"
mongosh "$MONGO_URI" --quiet --eval '
const contests = db.getSiblingDB("CodeLens").contests;
printjson(contests.getIndexes());
'Repository: kunalverma2512/CodeLens
Length of output: 227
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== locate target file =="
fd -a 'dropRedundantContestIndex\.js$' . || true
echo "== target file =="
file="$(fd 'dropRedundantContestIndex\.js$' . | head -n 1)"
if [ -n "${file:-}" ]; then
wc -l "$file"
cat -n "$file"
fi
echo "== related index references =="
rg -n "dropRedundantContestIndex|REDFund|contestId|platform|INDEXES|createIndex|unique" -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -n 200Repository: kunalverma2512/CodeLens
Length of output: 24917
Require the compound unique index before dropping contestId_1.
The migration only checks for { contestId: 1 }; it can drop that index even when the replacement { platform: 1, contestId: 1 } unique index is missing. A drifted production database would then lose the one-document-per-(platform, contestId) invariant. Abort before dropIndex unless that unique index exists.
Guard the destructive operation
const indexes = await collection.indexes();
+const compoundUnique = indexes.find(
+ (idx) =>
+ JSON.stringify(idx.key) ===
+ JSON.stringify({ platform: 1, contestId: 1 }) &&
+ idx.unique === true
+ );
if (standalone) {
+ if (!compoundUnique) {
+ throw new Error(
+ "Refusing to drop contestId_1: compound unique index is missing"
+ );
+ }
await collection.dropIndex(standalone.name);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const indexes = await collection.indexes(); | |
| const standalone = indexes.find( | |
| (idx) => JSON.stringify(idx.key) === JSON.stringify({ contestId: 1 }) | |
| ); | |
| if (standalone) { | |
| await collection.dropIndex(standalone.name); | |
| console.log(`Dropped redundant index: ${standalone.name}`); | |
| const indexes = await collection.indexes(); | |
| const compoundUnique = indexes.find( | |
| (idx) => | |
| JSON.stringify(idx.key) === | |
| JSON.stringify({ platform: 1, contestId: 1 }) && | |
| idx.unique === true | |
| ); | |
| const standalone = indexes.find( | |
| (idx) => JSON.stringify(idx.key) === JSON.stringify({ contestId: 1 }) | |
| ); | |
| if (standalone) { | |
| if (!compoundUnique) { | |
| throw new Error( | |
| "Refusing to drop contestId_1: compound unique index is missing" | |
| ); | |
| } | |
| await collection.dropIndex(standalone.name); | |
| console.log(`Dropped redundant index: ${standalone.name}`); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/scripts/dropRedundantContestIndex.js` around lines 19 - 27, Update the
migration around the standalone index lookup to first verify that a unique
compound index on `{ platform: 1, contestId: 1 }` exists. Abort before
`collection.dropIndex(standalone.name)` when the replacement index is missing,
while preserving the existing drop behavior when both indexes are present.
📌 Pull Request Summary
🔗 Related Issue
Closes #279
📝 Description
Provide a clear and concise summary of the changes made in this pull request.
Changes Made
validateParamsZod middleware andcontestIdParamSchema, applied toDELETE /reminders/:contestIdandPOST /reminders/:contestId/notified, both now enforce the same positive-integer contract asPOST /remindersat the route boundary, replacing a manualparseInt(...)that only rejectedNaNand would silently accept permissive strings like"12abc".contestIdindex on theContestmodel, the existing compound unique index{ platform: 1, contestId: 1 }already covers every current query pattern via its leftmost-prefix behavior. Documented the full rationale inserver/modules/contests/INDEXES.mdand addedserver/scripts/dropRedundantContestIndex.js, since a schema change alone never drops an already-existing index from a deployed database.useContestsnow guards against stale/superseded async responses using a per-fetch sequence token (incremented on every new fetch, and again on unmount), so a slower earlier request can never overwrite a newer one's state, and no response can write state after unmount, matching the cancellation pattern the bell and notifier already used.ContestReminderBellnow has a dynamicaria-label(including the due-soon count when applicable) instead of relying ontitlealone, and marks its decorative icon and count badgearia-hiddenso they aren't redundantly announced.node:testfor backend,vitestfor frontend, no new testing stack introduced): the validation contract (valid/zero/negative/decimal/non-numeric/"12abc"cases), reminder add/remove/mark-notified lifecycle edge cases (missing contest, non-upcoming phase), repository idempotency/sorting/joins/stale-cleanup, theuseContestsstale-response race condition and unmount safety, and bell accessibility (accessible name, due-soon count, keyboard focus).Motivation
This resolves the 5 Low-severity findings consolidated from the production review of #269. None of these individually blocks a single-instance release, but together they close real gaps: split validation logic between layers with inconsistent error contracts, a database index adding write overhead without ever being selected by the query planner, a React hook that could apply stale data after a slower request resolves late, an icon-only control with no reliable accessible name for screen readers, and a substantial feature area (contest tracker + reminders) shipped without any automated coverage protecting its lifecycle/async/validation behavior.
🚀 Type of Change
Select all that apply:
🧪 Testing
Verification
Test Details
validation.test.jsruns bothaddReminderSchemaandcontestIdParamSchemathrough a shared table of 9 cases each, valid integers, valid numeric strings, zero, negatives, decimals, non-numeric strings, the specific"12abc"permissive-parsing case, missing values, and empty strings.service.test.jswith 6 new tests coveringaddReminderthrowing 404 for a missing contest, 400 for aFINISHEDcontest, 400 for aSYSTEM_TESTcontest (not justFINISHED), successful add persisting correctly, plusremoveReminder/markReminderNotifieddelegating with correct arguments.repository.test.jscoversaddReminder's upsert-with-$setOnInsertidempotency,getUpcomingContests's filter/sort shape,getActiveReminderContests's join behavior (including dropping reminders for contests that are no longer upcoming), andpruneStaleReminders's deletion scope.useContests.test.jsincludes a genuine race-condition test, a slow initial fetch is made to resolve after a fastrefetch()call, and asserts the late response does not overwrite the newer data, plus an unmount test confirming a late-resolving response doesn't throw or apply.ContestReminderBell.test.jsxqueries by accessible role/name (getByRole("link", { name: ... })), the same mechanism assistive tech uses, verifies the due-soon count is included and correctly filtered (excludes >24h-out and already-started contests), confirms decorative elements arearia-hidden, and verifies real keyboard tab-focus reaches the control.npm testinserver/) and all 28 frontend tests (npm testinfrontend/) pass locally.📸 Screenshots / Demo (If Applicable)
N/A - this PR is backend validation/indexing logic, a React hook's async-safety internals, and accessibility attributes, not new UI. No visible design changes.
✅ Checklist
📚 Additional Notes
This is the 4th and final issue from the #269 production review - #276/#280, #277/#284, and #278/#286 are already merged. The index removal requires an explicit one-time run of
node server/scripts/dropRedundantContestIndex.jsagainst any already-deployed database (staging/production) after this merges, since Mongoose'sautoIndexonly ever adds missing indexes, never drops ones no longer declared in the schema, flagging this for whoever handles the deploy. Test coverage here is intentionally proportionate rather than exhaustive per the issue's own guidance ("avoid introducing a second testing stack," "prioritize behavior over implementation details"), full Playwright E2E flows and an exhaustiveUpcomingContestsListstate matrix were left as natural follow-ups for the existing Playwright setup at the repo root, rather than duplicated here.Summary by CodeRabbit
Accessibility
Bug Fixes
Tests
Maintenance