Show active boost counts on the tribe leaderboard - #4796
Conversation
The tribe leaderboard API (infra#486) now serves activeBoosts per entry — the same unexpired-boost count the owner sees on their names list. Surface it as a "Boosts" column between Name and Games: an amber "N×" multiplier (activeBoosts + 1, the draw-weight convention the store UI uses) when boosted, a dim dash otherwise. The field is optional in the schema: the board is cached for an hour, so responses from before the API deploy simply lack it and the column shows dashes. Ranking is unchanged — boosts affect in-game sampling weight, not board order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkthroughThe tribe leaderboard schema now requires a coerced ChangesTribe leaderboard boosts
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant fetchTribeLeaderboard
participant TribeLeaderboardResponseSchema
participant LeaderboardTribeTable
fetchTribeLeaderboard->>TribeLeaderboardResponseSchema: parse required activeBoosts
TribeLeaderboardResponseSchema-->>LeaderboardTribeTable: return validated tribe entries
LeaderboardTribeTable->>LeaderboardTribeTable: render activeBoosts + 1 or a dash
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
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 |
Drop the optional() — the client now deploys after infra#486, whose v2 cache key guarantees every response carries the field. A missing count fails the parse (surfacing the error state) instead of silently rendering an unboosted board. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@src/client/components/leaderboard/LeaderboardTribeTable.ts`:
- Line 188: Add a leaderboard_modal translation key for the empty boost marker
in resources/lang/en.json, then update the rendering branch in
LeaderboardTribeTable to pass that key through translateText() instead of
displaying the literal em dash. Preserve the existing span styling and
surrounding behavior.
In `@src/core/ApiSchemas.ts`:
- Line 348: Update the activeBoosts schema in ApiSchemas to coerce only expected
string-number inputs and validate the result as a non-negative integer,
rejecting null, booleans, decimals, and negative values while preserving
optional-field behavior.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: df20ba6c-5292-4327-9185-b968427c33e9
📒 Files selected for processing (5)
resources/lang/en.jsonsrc/client/components/leaderboard/LeaderboardTribeTable.tssrc/core/ApiSchemas.tstests/ApiSchemas.test.tstests/client/LeaderboardTribeTable.test.ts
| private renderBoostCell(tribe: TribeLeaderboardEntry): TemplateResult { | ||
| const boosts = tribe.activeBoosts ?? 0; | ||
| if (boosts === 0) { | ||
| return html`<span class="text-white/20">—</span>`; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Translate the empty boost marker.
Line 188 renders a user-visible literal —. Add a leaderboard_modal translation key in resources/lang/en.json and render it through translateText().
As per coding guidelines, “All user-visible text must go through translateText() function with corresponding entries in resources/lang/en.json.”
🤖 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 `@src/client/components/leaderboard/LeaderboardTribeTable.ts` at line 188, Add
a leaderboard_modal translation key for the empty boost marker in
resources/lang/en.json, then update the rendering branch in
LeaderboardTribeTable to pass that key through translateText() instead of
displaying the literal em dash. Preserve the existing span styling and
surrounding behavior.
Source: Coding guidelines
| // sees (display multiplier = this + 1). A "now" snapshot behind the board's | ||
| // 1h cache, not a window aggregate. Optional: cached responses from before | ||
| // the API served it lack the field. | ||
| activeBoosts: z.coerce.number().optional(), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
# Inspect the declared Zod version and the schema/test coverage before
# confirming the exact coercion and pipeline API used by this repository.
rg -n -C2 '"zod"' package.json pnpm-lock.yaml yarn.lock 2>/dev/null || true
sed -n '340,350p' src/core/ApiSchemas.ts
sed -n '920,955p' tests/ApiSchemas.test.tsRepository: openfrontio/OpenFrontIO
Length of output: 2230
🏁 Script executed:
#!/bin/bash
set -eu
# Inspect relevant schema locations and related activeBoosts usages/tests.
rg -n -C4 "activeBoosts|TribeLeaderboardResponseSchema|TribeNameSchema" src/core tests 2>/dev/null || true
# If local package files exist, show package manager lock entries for zod 4.x.
rg -n -A2 -B1 'zod@|^/zod|version.*4\.4\.2|4\.4\.2' pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || trueRepository: openfrontio/OpenFrontIO
Length of output: 19354
🌐 Web query:
Zod 4 coerce number behavior parse null undefined true false intnonnegative documentation
💡 Result:
In Zod 4, coercion relies on JavaScript's built-in constructors to convert input data [1][2]. The z.coerce.number() schema uses the Number() constructor [3][2], which leads to the following specific behaviors for non-numeric inputs: - null: Coerces to 0 [3][4][5]. - undefined: Throws a ZodError (validation fails) [3][4]. - true: Coerces to 1 [3][4][5]. - false: Coerces to 0 [3][4][5]. Note: While older versions of Zod might have allowed certain behaviors, in Zod 4, undefined is not accepted by z.coerce.number() and will cause parsing to fail [3][4]. Furthermore, NaN is not allowed by z.coerce.number() and will trigger a validation error [3][4]. Regarding intnonnegative, Zod 4 uses .nonnegative() as a chainable validation method on number schemas [6][7]. The correct usage is to chain these methods on a number schema, such as z.number().int().nonnegative() or z.coerce.number().int().nonnegative() [6][8][7]. If you encounter issues using these methods (e.g., TypeError:... is not a function), ensure you are using the latest version of Zod, as some issues related to chaining validation methods on number schemas were addressed in recent updates [9]. For scenarios involving objects, remember that Zod 4 performs validation of required fields before executing field-level coercion [10]; if a field is missing, you may need to use .default() to handle undefined inputs explicitly [10].
Citations:
- 1: https://zod.dev/api
- 2: https://github.com/colinhacks/zod/blob/v4.4.3/packages/docs/content/api.mdx
- 3: https://mintlify.com/colinhacks/zod/api/utilities/coerce
- 4: https://cdn.jsdelivr.net/npm/zod@4.3.6/src/v4/classic/tests/coerce.test.ts
- 5: https://github.com/andrewstellman/quality-playbook/blob/main/repos/docs_gathered/zod/08_behavioral_contracts_and_edge_cases.md
- 6: https://github.com/colinhacks/zod/blob/c7805073/packages/docs/content/api.mdx
- 7: https://github.com/colinhacks/zod/blob/912f0f51/packages/docs/content/api.mdx
- 8: https://mintlify.wiki/colinhacks/zod/api/primitives/number
- 9: v4: z.int().nonnegative is not a function colinhacks/zod#4162
- 10: v4.4 now errors on z.coerce.boolean parsing undefined colinhacks/zod#5957
🌐 Web query:
Zod 4.4.2 int() nonnegative chain on z.coerce.number() coerce.number.int API
💡 Result:
In Zod 4.4.2, you can chain integer and non-negative validations onto a coerced number schema. To ensure an input is coerced to a non-negative integer, you would use z.coerce.number().int().nonnegative(). Key details for Zod 4.4.x: 1. Chaining: You can chain .int() and .nonnegative() directly after z.coerce.number() [1]. const schema = z.coerce.number.int.nonnegative; schema.parse("10"); // => 10 schema.parse("-1"); // => Throws validation error 2. TypeScript and Input Types: In Zod v4, the input type of all z.coerce schemas defaults to unknown [2]. If you are using these within a .pipe() or another chain where strict type checking is required, you may encounter TypeScript errors [2][3]. You can resolve this by providing a generic type parameter to specify the expected input, such as z.coerce.number<string | number>() [2]. 3. API Changes: While older Zod documentation or legacy code may suggest .int() is deprecated or point to z.number({ coerce: true }) (which is no longer supported), the modern approach in Zod 4 is to use top-level coercion functions like z.coerce.number() followed by validation methods [4]. Additionally, Zod 4 introduces z.int() as a standalone schema constructor for integer validation, which is preferred over the method-style .int() in some contexts [5]. 4. Behavior: .nonnegative() validates that the number is greater than or equal to 0 [6][7]. The .int() method restricts values to safe integers [7]. Sources: [1], [6], [2], [5], [7]
Citations:
- 1: https://mintlify.com/colinhacks/zod/api/utilities/coerce
- 2: v4: Coerce API seems incompatible with pipes in regards to TypeScript colinhacks/zod#5124
- 3: v4: Type violation upon piping into coerced types colinhacks/zod#4236
- 4: v4: Can't properly coerce int number (.int() marked as deprecated) colinhacks/zod#4212
- 5: https://github.com/colinhacks/zod/blob/v4.4.3/packages/docs/content/api.mdx
- 6: https://github.com/colinhacks/zod/blob/9575cfecbabeb149a4991f931cf24f685ea8c55e/README.md
- 7: https://mintlify.wiki/colinhacks/zod/api/primitives/number
Validate activeBoosts as a non-negative integer.
z.coerce.number() uses JavaScript numeric conversion, so null, booleans, and negative values can pass as 0 or 1; negatives can also create invalid boost multipliers. Check a real count at runtime and keep string-number tolerance only where it is already expected.
Suggested schema shape
- activeBoosts: z.coerce.number().optional(),
+ activeBoosts: z
+ .union([z.number(), z.string().trim().min(1)])
+ .pipe(z.coerce.number().int().nonnegative())
+ .optional(),📝 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.
| activeBoosts: z.coerce.number().optional(), | |
| activeBoosts: z | |
| .union([z.number(), z.string().trim().min(1)]) | |
| .pipe(z.coerce.number().int().nonnegative()) | |
| .optional(), |
🤖 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 `@src/core/ApiSchemas.ts` at line 348, Update the activeBoosts schema in
ApiSchemas to coerce only expected string-number inputs and validate the result
as a non-negative integer, rejecting null, booleans, decimals, and negative
values while preserving optional-field behavior.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/core/ApiSchemas.ts (1)
343-349:⚠️ Potential issue | 🟡 MinorReject invalid boost counts at the schema boundary.
z.coerce.number()still accepts or converts values that are not valid counts, includingnull, booleans, negative numbers, and fractions. The UI can then render invalid multipliers such as0×or2.5×. Preserve numeric-string support, but require an integer and non-negative value after rejecting non-number inputs.This is the same unresolved concern from the previous review.
Suggested schema shape
- activeBoosts: z.coerce.number(), + activeBoosts: z + .union([z.number(), z.string().trim().min(1)]) + .pipe(z.coerce.number().int().nonnegative()),🤖 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 `@src/core/ApiSchemas.ts` around lines 343 - 349, Update the activeBoosts schema in ApiSchemas to reject null, boolean, negative, fractional, and other non-number inputs while preserving numeric-string support. Validate the coerced value as a non-negative integer so only valid boost counts reach the UI.
🤖 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.
Duplicate comments:
In `@src/core/ApiSchemas.ts`:
- Around line 343-349: Update the activeBoosts schema in ApiSchemas to reject
null, boolean, negative, fractional, and other non-number inputs while
preserving numeric-string support. Validate the coerced value as a non-negative
integer so only valid boost counts reach the UI.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8e5cb204-02c2-445b-98e0-1cea7a883c27
📒 Files selected for processing (5)
src/client/components/leaderboard/LeaderboardTribeTable.tssrc/core/ApiSchemas.tstests/ApiSchemas.test.tstests/client/ApiTribeLeaderboard.test.tstests/client/LeaderboardTribeTable.test.ts
Summary
The tribe leaderboard API (infra#486) now serves
activeBoostsper entry — the count of unexpired boosts on the name, the same field the owner sees on their names list. This surfaces it on the leaderboard's Tribes tab as a new Boosts column between Name and Games:N×multiplier —activeBoosts + 1, matching the in-game draw weight (1 + activeBoosts) and the convention the store UI uses (2 boosts = appears 3× as often).Column widths were re-tuned (name 8rem → 7rem, boosts 4rem) so the 26rem total still keeps Player Reach — the column the board is ranked by — on screen at mobile widths without horizontal scroll.
Notes
activeBoostsis required inTribeLeaderboardEntrySchema, so this must deploy after infra#486. Until then the board parse fails and the tab shows its error state. Infra bumped its cache key to v2, so every response from that deploy carries the field — there's no stale-cache window to tolerate.Test plan
activeBoosts, rejection when it's missing, and a stringified count (coercion tolerance matchingTribeNameSchema).tsc --noEmit, eslint, prettier all green.🤖 Generated with Claude Code