Skip to content

Show active boost counts on the tribe leaderboard - #4796

Merged
evanpelle merged 2 commits into
mainfrom
feat/tribe-leaderboard-boosts
Jul 30, 2026
Merged

Show active boost counts on the tribe leaderboard#4796
evanpelle merged 2 commits into
mainfrom
feat/tribe-leaderboard-boosts

Conversation

@evanpelle

@evanpelle evanpelle commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

The tribe leaderboard API (infra#486) now serves activeBoosts per 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:

  • Boosted names show an amber multiplier — activeBoosts + 1, matching the in-game draw weight (1 + activeBoosts) and the convention the store UI uses (2 boosts = appears 3× as often).
  • Unboosted names show a dim dash.
  • Header and cell tooltips explain the semantics ("Boosted names appear more often in other players' games" / "Appears N× as often in other players' games").

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

  • activeBoosts is required in TribeLeaderboardEntrySchema, 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.
  • It's a "now" snapshot behind the board's 1h cache, not a 30-day aggregate — it can lag a purchase by up to an hour.
  • Ranking is unchanged: boosts affect in-game sampling weight only, not board order.

Test plan

  • Schema tests: entry with activeBoosts, rejection when it's missing, and a stringified count (coercion tolerance matching TribeNameSchema).
  • jsdom rendering tests: multiplier is count + 1, column header renders, no multiplier at zero boosts, mixed board marks only the boosted rows.
  • Full test suite, tsc --noEmit, eslint, prettier all green.

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The tribe leaderboard schema now requires a coerced activeBoosts value. The table adds a translated Boost column showing activeBoosts + 1 for boosted tribes and a dim dash for zero boosts, with updated parsing and rendering tests.

Changes

Tribe leaderboard boosts

Layer / File(s) Summary
Boost response contract
src/core/ApiSchemas.ts, tests/ApiSchemas.test.ts, tests/client/ApiTribeLeaderboard.test.ts
TribeLeaderboardEntry now requires numeric activeBoosts, including coercion from strings; missing values are rejected and fixtures provide zero values.
Boost table rendering
src/client/components/leaderboard/LeaderboardTribeTable.ts, resources/lang/en.json, tests/client/LeaderboardTribeTable.test.ts
The table adds a translated Boost column, displays activeBoosts + 1 for boosted tribes, shows a dim dash for zero boosts, and tests header, tooltip, and mixed-row rendering.

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
Loading

Possibly related PRs

Suggested labels: UI/UX, Translation

Suggested reviewers: celant, ryanbarlow97, developingtom

Poem

Boosts bloom beside each tribe,
Multipliers come alive.
Zero counts become a dash,
Tooltips softly flash.
Leaderboard rows now thrive.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change: exposing active boost counts in the tribe leaderboard.
Description check ✅ Passed The description matches the code changes and explains the new leaderboard boost column and schema update.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7403717 and c1bd6ab.

📒 Files selected for processing (5)
  • resources/lang/en.json
  • src/client/components/leaderboard/LeaderboardTribeTable.ts
  • src/core/ApiSchemas.ts
  • tests/ApiSchemas.test.ts
  • tests/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>`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Comment thread src/core/ApiSchemas.ts Outdated
// 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(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.ts

Repository: 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 || true

Repository: 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:


🌐 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:


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.

Suggested change
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.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jul 30, 2026
@evanpelle evanpelle added this to the v33 milestone Jul 30, 2026
@evanpelle
evanpelle merged commit 3e845e3 into main Jul 30, 2026
12 of 15 checks passed
@evanpelle
evanpelle deleted the feat/tribe-leaderboard-boosts branch July 30, 2026 19:00
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/core/ApiSchemas.ts (1)

343-349: ⚠️ Potential issue | 🟡 Minor

Reject invalid boost counts at the schema boundary.

z.coerce.number() still accepts or converts values that are not valid counts, including null, booleans, negative numbers, and fractions. The UI can then render invalid multipliers such as or 2.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

📥 Commits

Reviewing files that changed from the base of the PR and between c1bd6ab and 82d070b.

📒 Files selected for processing (5)
  • src/client/components/leaderboard/LeaderboardTribeTable.ts
  • src/core/ApiSchemas.ts
  • tests/ApiSchemas.test.ts
  • tests/client/ApiTribeLeaderboard.test.ts
  • tests/client/LeaderboardTribeTable.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant