feat: add division cutoff rank with leaderboard line#384
feat: add division cutoff rank with leaderboard line#384zacjones93 wants to merge 3 commits intomainfrom
Conversation
Add configurable cutoff rank that renders a bold orange line on the leaderboard after the Nth-ranked athlete. Uses the same two-level fallback as capacity: competition-wide default with per-division override. - Schema: defaultCutoffRank on competitionsTable, cutoffRank on competitionDivisionsTable - Organizer UI: cutoff input in CapacitySettingsForm and per-division items - Leaderboard: orange line in both in-person and online tables (desktop + mobile) - Only shown on overall view, hidden in single-event view - Handles ties correctly (line after all tied athletes at cutoff) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WalkthroughThis PR introduces a leaderboard cutoff rank feature allowing organizers to set a default cutoff rank per competition and override it per division. Visual orange separators mark the cutoff boundary on leaderboards. Changes span database schema, UI components for configuration, leaderboard rendering logic, and server-side persistence. Changes
Sequence DiagramsequenceDiagram
actor Organizer
participant UI as OrganizerDivisionItem
participant Manager as OrganizerDivisionManager
participant Server as updateDivisionCutoffFn
participant DB as Database
Organizer->>UI: Enter cutoff rank value
UI->>Manager: onCutoffRankSave(rank)
Manager->>Manager: Optimistic UI update
Manager->>Server: updateDivisionCutoffFn(divisionId, rank)
Server->>Server: Validate & authenticate
Server->>DB: Upsert cutoffRank
DB-->>Server: Confirm
Server-->>Manager: Success
Manager->>Manager: Show success toast
Note over Organizer,DB: Division cutoff now configured
actor Athlete
participant LeaderboardPage as LeaderboardPageContent
participant Table as CompetitionLeaderboardTable
participant Renderer as UI Render
Athlete->>LeaderboardPage: View leaderboard
LeaderboardPage->>LeaderboardPage: Derive cutoffRank from division
LeaderboardPage->>Table: Pass cutoffRank prop
Table->>Renderer: Render rows + orange separator at boundary
Renderer-->>Athlete: Display leaderboard with cutoff line
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
🚀 Preview DeployedURL: https://wodsmith-app-pr-384.zacjones93.workers.dev
This comment is automatically updated on each push to this PR. |
There was a problem hiding this comment.
4 issues found across 16 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/wodsmith-start/src/components/competition-leaderboard-table.tsx">
<violation number="1" location="apps/wodsmith-start/src/components/competition-leaderboard-table.tsx:835">
P2: Cutoff line placement is incorrect when the leaderboard is sorted by anything other than overall-rank ascending.</violation>
</file>
<file name="apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/capacity-settings-form.tsx">
<violation number="1" location="apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/capacity-settings-form.tsx:105">
P2: `hasChanges` does not reject cutoff ranks below 1, so invalid values enable Save and only fail later on submit.</violation>
</file>
<file name="apps/wodsmith-start/src/components/divisions/organizer-division-manager.tsx">
<violation number="1" location="apps/wodsmith-start/src/components/divisions/organizer-division-manager.tsx:292">
P2: Rollback uses `??` with a nullable original value, so failed saves do not revert when the previous cutoff was `null`.</violation>
</file>
<file name="apps/wodsmith-start/src/components/online-competition-leaderboard-table.tsx">
<violation number="1" location="apps/wodsmith-start/src/components/online-competition-leaderboard-table.tsx:1264">
P2: Desktop cutoff line logic is also incorrect unless sorting is overall-rank ascending.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const showCutoff = | ||
| cutoffRank != null && | ||
| !selectedEventId && | ||
| entry.overallRank <= cutoffRank && |
There was a problem hiding this comment.
P2: Cutoff line placement is incorrect when the leaderboard is sorted by anything other than overall-rank ascending.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/components/competition-leaderboard-table.tsx, line 835:
<comment>Cutoff line placement is incorrect when the leaderboard is sorted by anything other than overall-rank ascending.</comment>
<file context>
@@ -825,14 +827,26 @@ export function CompetitionLeaderboardTable({
+ const showCutoff =
+ cutoffRank != null &&
+ !selectedEventId &&
+ entry.overallRank <= cutoffRank &&
+ (!nextEntry || nextEntry.overallRank > cutoffRank)
+ return (
</file context>
| const parsedTotal = maxTotal.trim() === "" ? null : Number(maxTotal) | ||
| if (parsedTotal !== null && (!Number.isFinite(parsedTotal) || !Number.isInteger(parsedTotal))) return false | ||
| const parsedCutoff = cutoffRank.trim() === "" ? null : parseInt(cutoffRank, 10) | ||
| if (parsedCutoff !== null && Number.isNaN(parsedCutoff)) return false |
There was a problem hiding this comment.
P2: hasChanges does not reject cutoff ranks below 1, so invalid values enable Save and only fail later on submit.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/capacity-settings-form.tsx, line 105:
<comment>`hasChanges` does not reject cutoff ranks below 1, so invalid values enable Save and only fail later on submit.</comment>
<file context>
@@ -85,9 +101,12 @@ export function CapacitySettingsForm({ competition }: Props) {
const parsedTotal = maxTotal.trim() === "" ? null : Number(maxTotal)
if (parsedTotal !== null && (!Number.isFinite(parsedTotal) || !Number.isInteger(parsedTotal))) return false
+ const parsedCutoff = cutoffRank.trim() === "" ? null : parseInt(cutoffRank, 10)
+ if (parsedCutoff !== null && Number.isNaN(parsedCutoff)) return false
return (
parsed !== competition.defaultMaxSpotsPerDivision ||
</file context>
| if (parsedCutoff !== null && Number.isNaN(parsedCutoff)) return false | |
| if (parsedCutoff !== null && (Number.isNaN(parsedCutoff) || parsedCutoff < 1)) return false |
| setDivisions((prev) => | ||
| prev.map((d) => | ||
| d.id === divisionId | ||
| ? { ...d, cutoffRank: original?.cutoffRank ?? newCutoffRank } |
There was a problem hiding this comment.
P2: Rollback uses ?? with a nullable original value, so failed saves do not revert when the previous cutoff was null.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/components/divisions/organizer-division-manager.tsx, line 292:
<comment>Rollback uses `??` with a nullable original value, so failed saves do not revert when the previous cutoff was `null`.</comment>
<file context>
@@ -255,6 +259,43 @@ export function OrganizerDivisionManager({
+ setDivisions((prev) =>
+ prev.map((d) =>
+ d.id === divisionId
+ ? { ...d, cutoffRank: original?.cutoffRank ?? newCutoffRank }
+ : d,
+ ),
</file context>
| ? { ...d, cutoffRank: original?.cutoffRank ?? newCutoffRank } | |
| ? { ...d, cutoffRank: original ? original.cutoffRank : newCutoffRank } |
| cutoffRank != null && | ||
| !selectedEventId && | ||
| entry.overallRank <= cutoffRank && | ||
| (!nextRow || nextRow.original.overallRank > cutoffRank) |
There was a problem hiding this comment.
P2: Desktop cutoff line logic is also incorrect unless sorting is overall-rank ascending.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/components/online-competition-leaderboard-table.tsx, line 1264:
<comment>Desktop cutoff line logic is also incorrect unless sorting is overall-rank ascending.</comment>
<file context>
@@ -1239,43 +1253,61 @@ export function OnlineCompetitionLeaderboardTable({
+ cutoffRank != null &&
+ !selectedEventId &&
+ entry.overallRank <= cutoffRank &&
+ (!nextRow || nextRow.original.overallRank > cutoffRank)
+ return (
+ <Fragment key={row.id}>
</file context>
| (!nextRow || nextRow.original.overallRank > cutoffRank) | |
| (!nextRow || nextRow.original.overallRank > cutoffRank) && | |
| currentSortId === "overallRank" && | |
| !currentSortDesc |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/wodsmith-start/src/server-fns/competition-divisions-fns.ts (1)
1878-1898:⚠️ Potential issue | 🟠 MajorPreserve
cutoffRankwhen migrating division config to a new scaling group.This migration now carries
feeCents,description, andmaxSpots, but it never copiesoldConfig.cutoffRank. Any per-division cutoff override will be lost after switching the competition to another scaling group or series template. Please migratecutoffRankalongside the rest of the division config.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/wodsmith-start/src/server-fns/competition-divisions-fns.ts` around lines 1878 - 1898, The insert that migrates a division config (inside the transaction using tx.query.competitionDivisionsTable.findFirst to get oldConfig and tx.insert(competitionDivisionsTable).values to create newConfig) currently copies feeCents, description, and maxSpots but omits cutoffRank; modify the values passed to tx.insert(...) to also include cutoffRank: oldConfig.cutoffRank so any per-division cutoff override is preserved when moving to the new divisionId/scaling group.
🧹 Nitpick comments (1)
apps/wodsmith-start/test/components/leaderboard-page-content.test.tsx (1)
82-95: Add cutoff-specific assertions (and@latlinks) in this test suite.This fixture update is necessary, but the new cutoff behavior (fallback/override/tie boundary placement) still isn’t asserted here. Please add at least one focused test that proves
cutoffRankis propagated/rendered correctly.As per coding guidelines,
**/test/**/*.{ts,tsx,js}requires//@lat: [[section-id]]comments next to relevant tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/wodsmith-start/test/components/leaderboard-page-content.test.tsx` around lines 82 - 95, Add a focused test in the leaderboard-page-content.test.tsx suite that renders LeaderboardPageContent (or the existing test case that uses the updated fixture entries "div-1"/"div-2") with entries having cutoffRank set (including a tie/boundary and a null case), then assert the rendered output shows the expected cutoffRank values for each entry (e.g., by querying the DOM for the cutoff number or a data-testid/label associated with that entry id) to prove propagation/override/fallback behavior; also add the required test marker comment // `@lat`: [[section-id]] immediately next to the new or updated test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/wodsmith-start/src/components/competition-leaderboard-table.tsx`:
- Around line 902-929: The cutoff marker logic using table.getRowModel().rows
and showCutoff (checking cutoffRank, selectedEventId, entry.overallRank, and
nextRow.original.overallRank) can display incorrectly after user resorting;
update the render condition so the cutoff only appears when the table is
currently sorted by overallRank in ascending order (or recompute cutoff
positions from a dedicated rank-sorted row model). Concretely, detect the active
sort state via the table instance (e.g., table.getState().sorting) and only
evaluate the existing showCutoff condition when the first sort column is
"overallRank" and direction is "asc"; alternatively build a separate rows array
sorted by overallRank and use that to compute the cutoff instead of
table.getRowModel().rows, ensuring the orange divider is stable when sorting
changes.
In `@apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx`:
- Around line 345-359: The blur handler for the number input truncates decimals
by using parseInt, causing mismatches between the displayed value and saved
value; replace parseInt(localCutoffRank, 10) with Number(localCutoffRank) and
validate using Number.isInteger so decimals are rejected (or reset) rather than
silently truncated; update the logic in the onBlur block that computes newVal
and the subsequent validation checks around localCutoffRank, cutoffRank,
onCutoffRankSave, and setLocalCutoffRank to use Number(...) +
Number.isInteger(...) and the same newVal < 1 and NaN checks before calling
onCutoffRankSave.
In `@apps/wodsmith-start/src/components/divisions/organizer-division-manager.tsx`:
- Around line 262-296: The rollback uses a stale/null-hostile value from
initialDivisions; modify handleCutoffRankSave to read and store the pre-edit
cutoff from the live divisions state (use the current divisions lookup before
calling setDivisions) into a local variable (e.g., previousCutoff) and use that
exact value in the catch rollback instead of original?.cutoffRank ??
newCutoffRank; ensure updateDivisionCutoffFn is still awaited and that after a
successful save you also update whatever source-of-truth (if any) you use for
initialDivisions so future errors can rollback correctly.
In `@apps/wodsmith-start/src/components/online-competition-leaderboard-table.tsx`:
- Around line 1256-1307: Summary: The cutoff separator is computed from the
current visual row order, so when the table is sorted by other columns the top-N
overall athletes are non-contiguous and the separator is wrong. Fix: change the
showCutoff logic to only render when the table is sorted by overallRank
ascending or compute cutoff from a rank-sorted slice. Specifically, in the
component use table.getState().sorting (or table.getState().sorting[0]) to
detect a single active sort whose id matches the overall-rank column id (the
column that provides entry.overallRank) and direction === "asc", and only then
allow the existing showCutoff code to run; alternatively, derive
rowsSortedByRank =
[...table.getRowModel().rows].sort((a,b)=>a.original.overallRank -
b.original.overallRank) and compute whether entry is the last within the top
cutoffRank using that sorted array instead of the visual rows. Apply this change
where showCutoff is computed (referencing showCutoff, cutoffRank,
entry.overallRank, table.getRowModel(), and table.getState().sorting).
In `@apps/wodsmith-start/src/db/schemas/commerce.ts`:
- Around line 161-162: The cutoffRank field on competition_divisions currently
uses int() and allows 0/negative values; change its schema to enforce the same
rank-domain constraints as the competition default (e.g., replace cutoffRank:
int() with cutoffRank: int().min(1).nullable() or the project’s equivalent
validation chain used by competition.defaultCutoffRank) so only positive ordinal
ranks or null are permitted; update any related tests or callers that assume
0/negative values.
In `@apps/wodsmith-start/src/db/schemas/competitions.ts`:
- Around line 132-133: The defaultCutoffRank field currently uses int(), which
allows 0/negative values; change the schema for defaultCutoffRank on the
competitions schema to only accept null or positive integers (>0) (e.g., use a
validator like int().min(1) / positiveInt() or equivalent) and add a DB-level
CHECK constraint (DEFAULT_CUTOFF_RANK IS NULL OR DEFAULT_CUTOFF_RANK > 0) so
both the application validator and persistence layer enforce the same domain;
update any related input/DTO validators (e.g., create/update competition inputs)
to mirror this rule.
In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-components/capacity-settings-form.tsx:
- Around line 70-79: Replace parseInt-based parsing of cutoffRank with numeric
validation using Number() and Number.isFinite/Number.isInteger checks: treat an
empty cutoffRank string as null, otherwise call Number(cutoffRank) and ensure
Number.isFinite(parsed) && Number.isInteger(parsed) && parsed >= 1; if
validation fails show the same toast and abort. Mirror this same
parsing/validation for any hasChanges comparison so the change detection
compares the canonical parsedCutoff (or null) rather than the truncated parseInt
result; follow the same pattern already used for maxTotal (Number +
Number.isFinite + Number.isInteger).
In `@lat.md/domain.md`:
- Line 48: The doc text incorrectly states that division config is stored in the
competition `settings` JSON; update the sentence in lat.md/domain.md to reflect
that division-specific registration fee, capacity (max spots), and leaderboard
cutoff rank are stored on the competition_divisions data model/ table (e.g.,
`competition_divisions`) rather than in the competition `settings` JSON, while
keeping that athletes register into a specific division and noting that these
fields override the competition defaults.
In `@lat.md/organizer-dashboard.md`:
- Around line 29-33: Replace the raw code references in this section with wiki
links so the knowledge graph picks them up: convert
"competitionsTable.defaultCutoffRank" and "competitionDivisionsTable.cutoffRank"
to wiki links (e.g. [[competitionsTable.defaultCutoffRank]] /
[[competitionDivisionsTable.cutoffRank]]), change
"getPublicCompetitionDivisionsFn" to [[getPublicCompetitionDivisionsFn]] and the
component names "CompetitionLeaderboardTable" and
"OnlineCompetitionLeaderboardTable" to [[CompetitionLeaderboardTable]] and
[[OnlineCompetitionLeaderboardTable]] (use aliasing if you want different
display text); keep the surrounding explanatory text intact and ensure the
overall/single-event view wording remains unchanged.
---
Outside diff comments:
In `@apps/wodsmith-start/src/server-fns/competition-divisions-fns.ts`:
- Around line 1878-1898: The insert that migrates a division config (inside the
transaction using tx.query.competitionDivisionsTable.findFirst to get oldConfig
and tx.insert(competitionDivisionsTable).values to create newConfig) currently
copies feeCents, description, and maxSpots but omits cutoffRank; modify the
values passed to tx.insert(...) to also include cutoffRank: oldConfig.cutoffRank
so any per-division cutoff override is preserved when moving to the new
divisionId/scaling group.
---
Nitpick comments:
In `@apps/wodsmith-start/test/components/leaderboard-page-content.test.tsx`:
- Around line 82-95: Add a focused test in the leaderboard-page-content.test.tsx
suite that renders LeaderboardPageContent (or the existing test case that uses
the updated fixture entries "div-1"/"div-2") with entries having cutoffRank set
(including a tie/boundary and a null case), then assert the rendered output
shows the expected cutoffRank values for each entry (e.g., by querying the DOM
for the cutoff number or a data-testid/label associated with that entry id) to
prove propagation/override/fallback behavior; also add the required test marker
comment // `@lat`: [[section-id]] immediately next to the new or updated test.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: be573c83-5867-4b54-b541-4d835e15960c
📒 Files selected for processing (16)
apps/wodsmith-start/src/components/competition-leaderboard-table.tsxapps/wodsmith-start/src/components/divisions/organizer-division-item.tsxapps/wodsmith-start/src/components/divisions/organizer-division-manager.tsxapps/wodsmith-start/src/components/leaderboard-page-content.tsxapps/wodsmith-start/src/components/online-competition-leaderboard-table.tsxapps/wodsmith-start/src/db/schemas/commerce.tsapps/wodsmith-start/src/db/schemas/competitions.tsapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/capacity-settings-form.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/divisions.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/settings.tsxapps/wodsmith-start/src/server-fns/competition-detail-fns.tsapps/wodsmith-start/src/server-fns/competition-divisions-fns.tsapps/wodsmith-start/src/server-fns/competition-fns.tsapps/wodsmith-start/test/components/leaderboard-page-content.test.tsxlat.md/domain.mdlat.md/organizer-dashboard.md
| table.getRowModel().rows.map((row, rowIdx) => { | ||
| const entry = row.original | ||
| const rows = table.getRowModel().rows | ||
| const nextRow = rows[rowIdx + 1] | ||
| const showCutoff = | ||
| cutoffRank != null && | ||
| !selectedEventId && | ||
| entry.overallRank <= cutoffRank && | ||
| (!nextRow || nextRow.original.overallRank > cutoffRank) | ||
| return ( | ||
| <Fragment key={row.id}> | ||
| <TableRow className="table-row"> | ||
| {row.getVisibleCells().map((cell) => ( | ||
| <TableCell key={cell.id} className="table-cell"> | ||
| {flexRender( | ||
| cell.column.columnDef.cell, | ||
| cell.getContext(), | ||
| )} | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| {showCutoff && ( | ||
| <tr> | ||
| <td colSpan={columns.length} className="p-0"> | ||
| <div className="h-[3px] bg-orange-500" /> | ||
| </td> | ||
| </tr> | ||
| )} |
There was a problem hiding this comment.
Hide or recompute the cutoff when the table is re-sorted.
This boundary check runs against the current sorted row model, so as soon as the user sorts overall view by athlete/event or flips rank descending, the orange line can land in the middle of the table or disappear entirely. The cutoff marker should only render in ascending overallRank order, or be computed from a separately rank-sorted view.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/wodsmith-start/src/components/competition-leaderboard-table.tsx` around
lines 902 - 929, The cutoff marker logic using table.getRowModel().rows and
showCutoff (checking cutoffRank, selectedEventId, entry.overallRank, and
nextRow.original.overallRank) can display incorrectly after user resorting;
update the render condition so the cutoff only appears when the table is
currently sorted by overallRank in ascending order (or recompute cutoff
positions from a dedicated rank-sorted row model). Concretely, detect the active
sort state via the table instance (e.g., table.getState().sorting) and only
evaluate the existing showCutoff condition when the first sort column is
"overallRank" and direction is "asc"; alternatively build a separate rows array
sorted by overallRank and use that to compute the cutoff instead of
table.getRowModel().rows, ensuring the orange divider is stable when sorting
changes.
| onBlur={() => { | ||
| const newVal = | ||
| localCutoffRank.trim() === "" | ||
| ? null | ||
| : parseInt(localCutoffRank, 10) | ||
| if (newVal !== cutoffRank) { | ||
| if ( | ||
| newVal !== null && | ||
| (Number.isNaN(newVal) || newVal < 1) | ||
| ) { | ||
| setLocalCutoffRank(cutoffRank?.toString() ?? "") | ||
| return | ||
| } | ||
| onCutoffRankSave(newVal) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
node - <<'NODE'
for (const raw of ["1.5", "2.9", "1e2"]) {
console.log({
raw,
parsedInt: parseInt(raw, 10),
numeric: Number(raw),
isInteger: Number.isInteger(Number(raw)),
})
}
NODERepository: wodsmith/thewodapp
Length of output: 243
🏁 Script executed:
cat -n apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx | sed -n '330,370p'Repository: wodsmith/thewodapp
Length of output: 1949
🏁 Script executed:
rg -B 20 "localCutoffRank.trim" apps/wodsmith-start/src/components/divisions/organizer-division-item.tsxRepository: wodsmith/thewodapp
Length of output: 890
🏁 Script executed:
rg "cutoffRank" apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx -A 2 -B 2Repository: wodsmith/thewodapp
Length of output: 1533
This blur handler silently truncates decimal input from the number field.
The <Input type="number"> field allows users to type decimal values (e.g., 1.5), but the blur handler uses parseInt(), which truncates to an integer. If a user types 1.5 and the current cutoffRank is 1, the parsed value equals the current value, so nothing is saved—but the UI still shows 1.5, making it appear the input was ignored. When cutoffRank differs, the truncated value gets saved silently. Use Number() and validate with Number.isInteger() instead:
const newVal =
localCutoffRank.trim() === ""
? null
: Number(localCutoffRank)
if (newVal !== cutoffRank) {
if (
newVal !== null &&
(Number.isNaN(newVal) || !Number.isInteger(newVal) || newVal < 1)
) {
setLocalCutoffRank(cutoffRank?.toString() ?? "")
return
}
onCutoffRankSave(newVal)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/wodsmith-start/src/components/divisions/organizer-division-item.tsx`
around lines 345 - 359, The blur handler for the number input truncates decimals
by using parseInt, causing mismatches between the displayed value and saved
value; replace parseInt(localCutoffRank, 10) with Number(localCutoffRank) and
validate using Number.isInteger so decimals are rejected (or reset) rather than
silently truncated; update the logic in the onBlur block that computes newVal
and the subsequent validation checks around localCutoffRank, cutoffRank,
onCutoffRankSave, and setLocalCutoffRank to use Number(...) +
Number.isInteger(...) and the same newVal < 1 and NaN checks before calling
onCutoffRankSave.
| const handleCutoffRankSave = async ( | ||
| divisionId: string, | ||
| newCutoffRank: number | null, | ||
| ) => { | ||
| const original = initialDivisions.find((d) => d.id === divisionId) | ||
| if (original && original.cutoffRank === newCutoffRank) return | ||
|
|
||
| setDivisions((prev) => | ||
| prev.map((d) => | ||
| d.id === divisionId ? { ...d, cutoffRank: newCutoffRank } : d, | ||
| ), | ||
| ) | ||
|
|
||
| try { | ||
| await updateDivisionCutoffFn({ | ||
| data: { | ||
| teamId, | ||
| competitionId, | ||
| divisionId, | ||
| cutoffRank: newCutoffRank, | ||
| }, | ||
| }) | ||
| toast.success("Division cutoff updated") | ||
| } catch (error) { | ||
| toast.error( | ||
| error instanceof Error ? error.message : "Failed to update cutoff", | ||
| ) | ||
| setDivisions((prev) => | ||
| prev.map((d) => | ||
| d.id === divisionId | ||
| ? { ...d, cutoffRank: original?.cutoffRank ?? newCutoffRank } | ||
| : d, | ||
| ), | ||
| ) | ||
| } |
There was a problem hiding this comment.
Rollback uses a stale/null-hostile source value.
This handler captures original from initialDivisions, then reverts with original?.cutoffRank ?? newCutoffRank. If the prior value was null, the catch path keeps the optimistic value instead of rolling back, and after one successful save initialDivisions is also stale for subsequent failures. Capture the pre-edit value from current state before mutating and restore that exact value on error.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/wodsmith-start/src/components/divisions/organizer-division-manager.tsx`
around lines 262 - 296, The rollback uses a stale/null-hostile value from
initialDivisions; modify handleCutoffRankSave to read and store the pre-edit
cutoff from the live divisions state (use the current divisions lookup before
calling setDivisions) into a local variable (e.g., previousCutoff) and use that
exact value in the catch rollback instead of original?.cutoffRank ??
newCutoffRank; ensure updateDivisionCutoffFn is still awaited and that after a
successful save you also update whatever source-of-truth (if any) you use for
initialDivisions so future errors can rollback correctly.
| table.getRowModel().rows.map((row, rowIdx) => { | ||
| const entry = row.original | ||
| const rows = table.getRowModel().rows | ||
| const nextRow = rows[rowIdx + 1] | ||
| const showCutoff = | ||
| cutoffRank != null && | ||
| !selectedEventId && | ||
| entry.overallRank <= cutoffRank && | ||
| (!nextRow || nextRow.original.overallRank > cutoffRank) | ||
| return ( | ||
| <Fragment key={row.id}> | ||
| <TableRow | ||
| className={cn( | ||
| "table-row", | ||
| row.getIsExpanded() && "border-b-0", | ||
| hasExpandableContent(row.original, selectedEventId) && | ||
| "cursor-pointer", | ||
| )} | ||
| onClick={() => { | ||
| if ( | ||
| hasExpandableContent(row.original, selectedEventId) | ||
| ) { | ||
| row.toggleExpanded() | ||
| } | ||
| }} | ||
| > | ||
| {row.getVisibleCells().map((cell) => ( | ||
| <TableCell key={cell.id} className="table-cell"> | ||
| {flexRender( | ||
| cell.column.columnDef.cell, | ||
| cell.getContext(), | ||
| )} | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| {row.getIsExpanded() && ( | ||
| <ExpandedVideoRow | ||
| row={row} | ||
| selectedEventId={selectedEventId} | ||
| columnsCount={columns.length} | ||
| voteCounts={voteCounts} | ||
| isLoggedIn={isLoggedIn} | ||
| currentUserId={currentUserId} | ||
| /> | ||
| )} | ||
| onClick={() => { | ||
| if (hasExpandableContent(row.original, selectedEventId)) { | ||
| row.toggleExpanded() | ||
| } | ||
| }} | ||
| > | ||
| {row.getVisibleCells().map((cell) => ( | ||
| <TableCell key={cell.id} className="table-cell"> | ||
| {flexRender( | ||
| cell.column.columnDef.cell, | ||
| cell.getContext(), | ||
| )} | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| {row.getIsExpanded() && ( | ||
| <ExpandedVideoRow | ||
| row={row} | ||
| selectedEventId={selectedEventId} | ||
| columnsCount={columns.length} | ||
| voteCounts={voteCounts} | ||
| isLoggedIn={isLoggedIn} | ||
| currentUserId={currentUserId} | ||
| /> | ||
| )} | ||
| </Fragment> | ||
| )) | ||
| {showCutoff && ( | ||
| <tr> | ||
| <td colSpan={columns.length} className="p-0"> | ||
| <div className="h-[3px] bg-orange-500" /> | ||
| </td> | ||
| </tr> | ||
| )} |
There was a problem hiding this comment.
The cutoff line breaks when overall rows are sorted by something else.
showCutoff is derived from the current row order, so sorting overall view by athlete/event or reversing rank order makes the top-N athletes non-contiguous and the separator becomes incorrect. This needs to be gated to ascending overallRank order, or computed from a separate rank-sorted slice.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/wodsmith-start/src/components/online-competition-leaderboard-table.tsx`
around lines 1256 - 1307, Summary: The cutoff separator is computed from the
current visual row order, so when the table is sorted by other columns the top-N
overall athletes are non-contiguous and the separator is wrong. Fix: change the
showCutoff logic to only render when the table is sorted by overallRank
ascending or compute cutoff from a rank-sorted slice. Specifically, in the
component use table.getState().sorting (or table.getState().sorting[0]) to
detect a single active sort whose id matches the overall-rank column id (the
column that provides entry.overallRank) and direction === "asc", and only then
allow the existing showCutoff code to run; alternatively, derive
rowsSortedByRank =
[...table.getRowModel().rows].sort((a,b)=>a.original.overallRank -
b.original.overallRank) and compute whether entry is the last within the top
cutoffRank using that sorted array instead of the visual rows. Apply this change
where showCutoff is computed (referencing showCutoff, cutoffRank,
entry.overallRank, table.getRowModel(), and table.getState().sorting).
| // Cutoff rank for this division (null = use competition default) | ||
| cutoffRank: int(), |
There was a problem hiding this comment.
Apply rank-domain constraints to competition_divisions.cutoffRank as well.
At Line 162, this field currently allows negative values (and 0). Since this is an ordinal rank, persisting invalid values should be prevented consistently with the competition default cutoff.
Suggested schema hardening
- cutoffRank: int(),
+ cutoffRank: int({ unsigned: true }),📝 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.
| // Cutoff rank for this division (null = use competition default) | |
| cutoffRank: int(), | |
| // Cutoff rank for this division (null = use competition default) | |
| cutoffRank: int({ unsigned: true }), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/wodsmith-start/src/db/schemas/commerce.ts` around lines 161 - 162, The
cutoffRank field on competition_divisions currently uses int() and allows
0/negative values; change its schema to enforce the same rank-domain constraints
as the competition default (e.g., replace cutoffRank: int() with cutoffRank:
int().min(1).nullable() or the project’s equivalent validation chain used by
competition.defaultCutoffRank) so only positive ordinal ranks or null are
permitted; update any related tests or callers that assume 0/negative values.
| // Cutoff: default leaderboard cutoff rank per division (null = no cutoff line) | ||
| defaultCutoffRank: int(), |
There was a problem hiding this comment.
Constrain defaultCutoffRank to valid rank values.
At Line 133, int() permits negative values (and 0), which are invalid for rank semantics and can create ambiguous UI behavior. Please enforce a positive domain at persistence boundaries (DB and/or validator).
Suggested schema hardening
- defaultCutoffRank: int(),
+ defaultCutoffRank: int({ unsigned: true }),📝 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.
| // Cutoff: default leaderboard cutoff rank per division (null = no cutoff line) | |
| defaultCutoffRank: int(), | |
| // Cutoff: default leaderboard cutoff rank per division (null = no cutoff line) | |
| defaultCutoffRank: int({ unsigned: true }), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/wodsmith-start/src/db/schemas/competitions.ts` around lines 132 - 133,
The defaultCutoffRank field currently uses int(), which allows 0/negative
values; change the schema for defaultCutoffRank on the competitions schema to
only accept null or positive integers (>0) (e.g., use a validator like
int().min(1) / positiveInt() or equivalent) and add a DB-level CHECK constraint
(DEFAULT_CUTOFF_RANK IS NULL OR DEFAULT_CUTOFF_RANK > 0) so both the application
validator and persistence layer enforce the same domain; update any related
input/DTO validators (e.g., create/update competition inputs) to mirror this
rule.
| const parsedCutoff = cutoffRank.trim() === "" ? null : parseInt(cutoffRank, 10) | ||
|
|
||
| if ( | ||
| parsedCutoff !== null && | ||
| (Number.isNaN(parsedCutoff) || parsedCutoff < 1) | ||
| ) { | ||
| toast.error("Cutoff rank must be 1 or higher") | ||
| setIsSubmitting(false) | ||
| return | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
node - <<'NODE'
for (const raw of ["10.5", "1e2", "08"]) {
const parsedInt = parseInt(raw, 10)
const numeric = Number(raw)
console.log({ raw, parsedInt, numeric, isInteger: Number.isInteger(numeric) })
}
NODERepository: wodsmith/thewodapp
Length of output: 242
🏁 Script executed:
fd -type f "capacity-settings-form.tsx"Repository: wodsmith/thewodapp
Length of output: 232
🏁 Script executed:
fd "capacity-settings-form.tsx"Repository: wodsmith/thewodapp
Length of output: 164
🏁 Script executed:
wc -l "apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/capacity-settings-form.tsx"Repository: wodsmith/thewodapp
Length of output: 181
🏁 Script executed:
git ls-files | grep "capacity-settings-form.tsx"Repository: wodsmith/thewodapp
Length of output: 164
🏁 Script executed:
cat -n 'apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/capacity-settings-form.tsx' | sed -n '65,115p'Repository: wodsmith/thewodapp
Length of output: 2114
Reject decimal/exponent cutoff inputs instead of truncating them.
parseInt() silently coerces 10.5 to 10 and 1e2 to 1, allowing invalid cutoff values to save. The same truncation also leaks into hasChanges, masking whether the input actually changed.
Note: maxTotal already uses Number() with Number.isFinite() and Number.isInteger() checks (lines 102–103)—apply the same pattern to cutoffRank for consistency.
Suggested fix
- const parsedCutoff = cutoffRank.trim() === "" ? null : parseInt(cutoffRank, 10)
+ const parsedCutoff = cutoffRank.trim() === "" ? null : Number(cutoffRank)
if (
parsedCutoff !== null &&
- (Number.isNaN(parsedCutoff) || parsedCutoff < 1)
+ (!Number.isFinite(parsedCutoff) ||
+ !Number.isInteger(parsedCutoff) ||
+ parsedCutoff < 1)
) {
toast.error("Cutoff rank must be 1 or higher")
setIsSubmitting(false)
return
}
@@
- const parsedCutoff = cutoffRank.trim() === "" ? null : parseInt(cutoffRank, 10)
- if (parsedCutoff !== null && Number.isNaN(parsedCutoff)) return false
+ const parsedCutoff = cutoffRank.trim() === "" ? null : Number(cutoffRank)
+ if (
+ parsedCutoff !== null &&
+ (!Number.isFinite(parsedCutoff) || !Number.isInteger(parsedCutoff))
+ ) {
+ return false
+ }Also applies to: 104–109
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-components/capacity-settings-form.tsx
around lines 70 - 79, Replace parseInt-based parsing of cutoffRank with numeric
validation using Number() and Number.isFinite/Number.isInteger checks: treat an
empty cutoffRank string as null, otherwise call Number(cutoffRank) and ensure
Number.isFinite(parsed) && Number.isInteger(parsed) && parsed >= 1; if
validation fails show the same toast and abort. Mirror this same
parsing/validation for any hasChanges comparison so the change detection
compares the canonical parsedCutoff (or null) rather than the truncated parseInt
result; follow the same pattern already used for maxTotal (Number +
Number.isFinite + Number.isInteger).
| Divisions segment athletes within a competition (e.g., "RX Male", "Scaled Female", "Masters 40+"). | ||
|
|
||
| Stored in competition `settings` JSON. Athletes register into a specific division. Division-specific registration fees can override the competition default. | ||
| Stored in competition `settings` JSON. Athletes register into a specific division. Division-specific registration fees, capacity (max spots), and leaderboard cutoff rank can each override the competition default. |
There was a problem hiding this comment.
Division storage detail is inaccurate after this feature change.
Line 48 says division config is stored in competition settings JSON, but fee/capacity/cutoff now live in competition_divisions. Please update wording so docs match runtime data model.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lat.md/domain.md` at line 48, The doc text incorrectly states that division
config is stored in the competition `settings` JSON; update the sentence in
lat.md/domain.md to reflect that division-specific registration fee, capacity
(max spots), and leaderboard cutoff rank are stored on the competition_divisions
data model/ table (e.g., `competition_divisions`) rather than in the competition
`settings` JSON, while keeping that athletes register into a specific division
and noting that these fields override the competition defaults.
| ### Cutoff Rank | ||
|
|
||
| Configures a leaderboard cutoff line — a bold orange line rendered after the Nth-ranked athlete. | ||
|
|
||
| Uses the same two-level fallback as capacity: `competitionsTable.defaultCutoffRank` is the competition-wide default, `competitionDivisionsTable.cutoffRank` is the per-division override (null = use default). The effective cutoff flows to the public leaderboard via `getPublicCompetitionDivisionsFn` and renders in both `CompetitionLeaderboardTable` and `OnlineCompetitionLeaderboardTable` (desktop + mobile). Only shown on overall view, hidden in single-event view. |
There was a problem hiding this comment.
Use wiki links for the new cutoff references.
This new lat.md section introduces several raw code references instead of [[...]] links, which breaks the knowledge-graph cross-linking for the added feature. As per coding guidelines, "Use wiki links ([[target]] or [[target|alias]]) to cross-reference sections and source code in lat.md/ files".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lat.md/organizer-dashboard.md` around lines 29 - 33, Replace the raw code
references in this section with wiki links so the knowledge graph picks them up:
convert "competitionsTable.defaultCutoffRank" and
"competitionDivisionsTable.cutoffRank" to wiki links (e.g.
[[competitionsTable.defaultCutoffRank]] /
[[competitionDivisionsTable.cutoffRank]]), change
"getPublicCompetitionDivisionsFn" to [[getPublicCompetitionDivisionsFn]] and the
component names "CompetitionLeaderboardTable" and
"OnlineCompetitionLeaderboardTable" to [[CompetitionLeaderboardTable]] and
[[OnlineCompetitionLeaderboardTable]] (use aliasing if you want different
display text); keep the surrounding explanatory text intact and ensure the
overall/single-event view wording remains unchanged.
Summary
competitionsTable.defaultCutoffRankandcompetitionDivisionsTable.cutoffRankChanges
Schema:
defaultCutoffRank: int()oncompetitionsTable— competition-wide defaultcutoffRank: int()oncompetitionDivisionsTable— per-division override (null = use default)Server functions:
updateCompetitionDefaultCapacityFnto acceptdefaultCutoffRankupdateDivisionCutoffFnfor per-division cutoff overridesgetCompetitionDivisionsWithCountsFnandgetPublicCompetitionDivisionsFnto return cutoff dataOrganizer UI:
CapacitySettingsFormOrganizerDivisionItemcollapsible sectionLeaderboard:
CompetitionLeaderboardTableandOnlineCompetitionLeaderboardTableTest plan
pnpm db:pushto apply schema changes🤖 Generated with Claude Code
Summary by cubic
Adds a configurable leaderboard cutoff rank that draws a bold orange line after the Nth-ranked athlete. Uses a competition default with per-division overrides, shows only on overall view, and handles ties.
New Features
competitionsTable.defaultCutoffRank,competitionDivisionsTable.cutoffRank.updateCompetitionDefaultCapacityFnto acceptdefaultCutoffRank; addedupdateDivisionCutoffFn; cutoff returned bygetCompetitionDivisionsWithCountsFnandgetPublicCompetitionDivisionsFn.CapacitySettingsForm; per-division cutoff input inOrganizerDivisionItem(wired viaOrganizerDivisionManager).CompetitionLeaderboardTableandOnlineCompetitionLeaderboardTable(desktop + mobile). Only on overall view; line appears after all athletes at the cutoff to account for ties.Migration
pnpm db:pushto add new columns.Written for commit 0634494. Summary will update on new commits.
Summary by CodeRabbit