Summary
Harden the contest tracker’s lifecycle handling, reminder data flow, and filtering behavior by resolving the five Medium-severity findings from the production review of #269.
Backlinks
Goals
The tracker should display an accurate contest lifecycle, avoid avoidable per-tab network/database work, remain correct after delayed synchronization, prevent duplicate in-app reminders across browser tabs where feasible, and expose every backend-derived division/category in the UI.
Findings and required work
1. Persist system-testing lifecycle phases so contests do not remain falsely “live”
Affected area: server/modules/contests/service.js, especially ContestService.syncCodeforcesContests(); downstream repository/frontend display behavior.
Current behavior
The sync selection persists only BEFORE, CODING, and a bounded group of FINISHED contests. Codeforces contests can transition through phases such as PENDING_SYSTEM_TEST and SYSTEM_TEST after submissions close and before they become FINISHED.
When those intermediate phases are excluded, the cached document can retain its previous CODING phase. The frontend treats CODING as running, so it can show “Live Now” even though the contest is no longer accepting submissions.
Production impact
Users receive inaccurate real-time status. A contest can look active for up to the next successful sync—or longer if combined with the finished-window issue below. This undermines the core promise of a live contest tracker and can cause users to set expectations around a contest that has ended.
Required change
Persist all Codeforces phases that are necessary to transition an already cached upcoming/running contest to a non-runnable state. Define a clear UI/domain mapping for intermediate testing phases—e.g., show “System testing” or a non-live completed state—and ensure upcoming queries and reminder eligibility match that mapping.
2. Eliminate duplicate polling of active reminders within one authenticated tab
Affected area: frontend/src/components/contests/ContestReminderBell.jsx, frontend/src/components/shared/ContestReminderNotifier.jsx, frontend/src/hooks/useContests.js, and possibly a shared context/provider or query cache.
Current behavior
The navbar bell polls getMyActiveReminders() every 60 seconds, while the global reminder notifier independently polls the same endpoint every 30 seconds. Both are mounted on authenticated pages.
Production impact
Each active browser tab produces duplicate network requests, authentication work, and database reads for identical information. As user traffic grows, this costs capacity without improving freshness and creates two independent sources of state that can disagree momentarily.
Required change
Introduce one shared reminder-data owner per browser tab (for example, a context/provider, a dedicated hook with a shared cache, or an existing application query layer). The bell and notifier should consume the same cached state. Preserve cleanup on auth changes/unmounts and define a single polling/refetch cadence appropriate for reminder urgency.
3. Ensure delayed or bursty syncs cannot leave contests permanently stale
Affected area: server/modules/contests/service.js and relevant repository queries/tests.
Current behavior
Only the 20 most recently started FINISHED contests are selected for persistence on each sync. Under normal hourly operation, a contest usually gets recorded as finished. But after downtime, API failure, a burst of contest events, or an unusual data ordering, a previously cached BEFORE/CODING contest may fall outside that 20-item window before its FINISHED phase is ever persisted.
Production impact
A stale document may remain eligible for the “upcoming/running” query indefinitely, showing users an event that has ended and retaining stale reminders. This is a data-lifecycle correctness issue, not merely a display delay.
Required change
Make phase reconciliation complete for records the application already tracks. Options include:
- persist the entire relevant Codeforces response when reasonably bounded; or
- explicitly update every locally cached non-finished contest from the latest API response, regardless of a historical finished-contest display/cache cap.
Keep any retention cap separate from lifecycle reconciliation. If old finished records need pruning, perform that as an explicit retention policy rather than relying on a limited fetch subset to communicate state transitions.
4. Coordinate reminder toasts across browser tabs
Affected area: frontend/src/components/shared/ContestReminderNotifier.jsx; potentially reminder API semantics and cross-tab browser coordination.
Current behavior
The notifier’s shownThisSession set exists only inside one mounted component instance. If the same authenticated user has multiple tabs open, both can fetch a reminder with notifiedAt: null before either fire-and-forget markReminderNotified() request completes. Both tabs can show the toast.
Production impact
Users can receive duplicate “starting soon” notifications. The impact is mostly user-experience noise, but reminders are a core feature and duplicate alerts diminish trust in their reliability. The risk grows for users who keep dashboards/trackers open in multiple tabs.
Required change
Provide cross-tab coordination where supported, such as BroadcastChannel or a carefully scoped localStorage event/key, while retaining server-side notifiedAt as the durable source of truth. Consider making the server’s mark-notified operation expose clear idempotent/conditional semantics so clients can tell whether they won the notification claim. The implementation must degrade safely when browser storage/channel APIs are unavailable.
5. Make all backend-generated division/category values reachable in the UI
Affected area: server/modules/contests/service.js (parseDivision) and frontend/src/components/contests/UpcomingContestsList.jsx (DIVISIONS and filtering).
Current behavior
The backend can classify contests into values such as Div. 1 + 2, Kotlin Heroes, ICPC, and Other. The UI’s explicit filter list includes only Div. 1 through Div. 4, Educational, and Global.
Production impact
Contests in the omitted categories are displayed only under the unfiltered “all” view and cannot be selected through a dedicated category filter. This is inconsistent behavior between the API’s domain vocabulary and the UI’s navigation model.
Required change
Establish one canonical category vocabulary or mapping contract. Either include every backend value in the filter UI, deliberately group them into documented categories, or generate filters from returned data while preserving a stable and accessible order. Make sure filtering labels, badge labels, and empty states remain understandable on small screens.
Acceptance criteria
Test plan
Backend/service and repository tests
- Phase transitions:
BEFORE → CODING → PENDING_SYSTEM_TEST/SYSTEM_TEST → FINISHED.
- A locally cached running contest that is absent from the “latest 20 finished” window is still reconciled to finished.
- Reminder eligibility and active-reminder queries for all lifecycle states.
- API response and ordering edge cases, including delayed syncs and temporary Codeforces failures.
Frontend tests
- Exactly one request owner/cadence is used when both bell and notifier render.
- Logout/unmount cancels polling and does not update stale component state.
- Bell count and toast use the same reminder snapshot.
- Two simulated tabs coordinate notification display; the losing tab suppresses its duplicate toast.
- Every parsed division/category is represented or grouped correctly by the UI filter.
Contributor learning guide
Review these topics before starting:
- State-machine modeling: model contest lifecycle phases explicitly rather than treating state as a few independent strings.
- Cache reconciliation: distinguish data retention/display limits from authoritative state synchronization.
- Client-side data ownership: shared query caches, context providers, subscription lifecycles, polling deduplication, and stale-while-revalidate patterns.
- React effect correctness: dependency arrays, cancellation, stale async responses, interval cleanup, and avoiding duplicated side effects.
- Cross-tab web coordination:
BroadcastChannel, storage events, atomicity limitations, and progressive enhancement.
- Distributed/idempotent notifications: server-side durable state, conditional updates, and why fire-and-forget client requests can race.
- Domain contracts: keep backend enum/parser outputs and frontend filters/types synchronized.
- User-facing time/status semantics: distinguish “running,” “ended,” and “system testing” instead of deriving status only from timestamps.
Definition of done
The tracker reports the correct contest lifecycle, performs no duplicate reminder polling within a tab, remains correct after synchronization gaps, avoids multi-tab duplicate alerts in supported browsers, and presents a complete consistent division/category filter model with automated coverage.
Summary
Harden the contest tracker’s lifecycle handling, reminder data flow, and filtering behavior by resolving the five Medium-severity findings from the production review of #269.
Backlinks
Goals
The tracker should display an accurate contest lifecycle, avoid avoidable per-tab network/database work, remain correct after delayed synchronization, prevent duplicate in-app reminders across browser tabs where feasible, and expose every backend-derived division/category in the UI.
Findings and required work
1. Persist system-testing lifecycle phases so contests do not remain falsely “live”
Affected area:
server/modules/contests/service.js, especiallyContestService.syncCodeforcesContests(); downstream repository/frontend display behavior.Current behavior
The sync selection persists only
BEFORE,CODING, and a bounded group ofFINISHEDcontests. Codeforces contests can transition through phases such asPENDING_SYSTEM_TESTandSYSTEM_TESTafter submissions close and before they becomeFINISHED.When those intermediate phases are excluded, the cached document can retain its previous
CODINGphase. The frontend treatsCODINGas running, so it can show “Live Now” even though the contest is no longer accepting submissions.Production impact
Users receive inaccurate real-time status. A contest can look active for up to the next successful sync—or longer if combined with the finished-window issue below. This undermines the core promise of a live contest tracker and can cause users to set expectations around a contest that has ended.
Required change
Persist all Codeforces phases that are necessary to transition an already cached upcoming/running contest to a non-runnable state. Define a clear UI/domain mapping for intermediate testing phases—e.g., show “System testing” or a non-live completed state—and ensure upcoming queries and reminder eligibility match that mapping.
2. Eliminate duplicate polling of active reminders within one authenticated tab
Affected area:
frontend/src/components/contests/ContestReminderBell.jsx,frontend/src/components/shared/ContestReminderNotifier.jsx,frontend/src/hooks/useContests.js, and possibly a shared context/provider or query cache.Current behavior
The navbar bell polls
getMyActiveReminders()every 60 seconds, while the global reminder notifier independently polls the same endpoint every 30 seconds. Both are mounted on authenticated pages.Production impact
Each active browser tab produces duplicate network requests, authentication work, and database reads for identical information. As user traffic grows, this costs capacity without improving freshness and creates two independent sources of state that can disagree momentarily.
Required change
Introduce one shared reminder-data owner per browser tab (for example, a context/provider, a dedicated hook with a shared cache, or an existing application query layer). The bell and notifier should consume the same cached state. Preserve cleanup on auth changes/unmounts and define a single polling/refetch cadence appropriate for reminder urgency.
3. Ensure delayed or bursty syncs cannot leave contests permanently stale
Affected area:
server/modules/contests/service.jsand relevant repository queries/tests.Current behavior
Only the 20 most recently started
FINISHEDcontests are selected for persistence on each sync. Under normal hourly operation, a contest usually gets recorded as finished. But after downtime, API failure, a burst of contest events, or an unusual data ordering, a previously cachedBEFORE/CODINGcontest may fall outside that 20-item window before itsFINISHEDphase is ever persisted.Production impact
A stale document may remain eligible for the “upcoming/running” query indefinitely, showing users an event that has ended and retaining stale reminders. This is a data-lifecycle correctness issue, not merely a display delay.
Required change
Make phase reconciliation complete for records the application already tracks. Options include:
Keep any retention cap separate from lifecycle reconciliation. If old finished records need pruning, perform that as an explicit retention policy rather than relying on a limited fetch subset to communicate state transitions.
4. Coordinate reminder toasts across browser tabs
Affected area:
frontend/src/components/shared/ContestReminderNotifier.jsx; potentially reminder API semantics and cross-tab browser coordination.Current behavior
The notifier’s
shownThisSessionset exists only inside one mounted component instance. If the same authenticated user has multiple tabs open, both can fetch a reminder withnotifiedAt: nullbefore either fire-and-forgetmarkReminderNotified()request completes. Both tabs can show the toast.Production impact
Users can receive duplicate “starting soon” notifications. The impact is mostly user-experience noise, but reminders are a core feature and duplicate alerts diminish trust in their reliability. The risk grows for users who keep dashboards/trackers open in multiple tabs.
Required change
Provide cross-tab coordination where supported, such as
BroadcastChannelor a carefully scopedlocalStorageevent/key, while retaining server-sidenotifiedAtas the durable source of truth. Consider making the server’s mark-notified operation expose clear idempotent/conditional semantics so clients can tell whether they won the notification claim. The implementation must degrade safely when browser storage/channel APIs are unavailable.5. Make all backend-generated division/category values reachable in the UI
Affected area:
server/modules/contests/service.js(parseDivision) andfrontend/src/components/contests/UpcomingContestsList.jsx(DIVISIONSand filtering).Current behavior
The backend can classify contests into values such as
Div. 1 + 2,Kotlin Heroes,ICPC, andOther. The UI’s explicit filter list includes onlyDiv. 1throughDiv. 4,Educational, andGlobal.Production impact
Contests in the omitted categories are displayed only under the unfiltered “all” view and cannot be selected through a dedicated category filter. This is inconsistent behavior between the API’s domain vocabulary and the UI’s navigation model.
Required change
Establish one canonical category vocabulary or mapping contract. Either include every backend value in the filter UI, deliberately group them into documented categories, or generate filters from returned data while preserving a stable and accessible order. Make sure filtering labels, badge labels, and empty states remain understandable on small screens.
Acceptance criteria
notifiedAtstate.Test plan
Backend/service and repository tests
BEFORE → CODING → PENDING_SYSTEM_TEST/SYSTEM_TEST → FINISHED.Frontend tests
Contributor learning guide
Review these topics before starting:
BroadcastChannel,storageevents, atomicity limitations, and progressive enhancement.Definition of done
The tracker reports the correct contest lifecycle, performs no duplicate reminder polling within a tab, remains correct after synchronization gaps, avoids multi-tab duplicate alerts in supported browsers, and presents a complete consistent division/category filter model with automated coverage.