[climbing] Add grade system switcher - #2484
Conversation
* chore: configure Paseo workspace workflows * fix: allow alertable in spell check * fix: scope mutation checks to pull request changes * test: align mobile tRPC mocks * test: align web tRPC mocks * fix: sync mobile Expo dependencies * docs: record CI incident findings
* docs: specify body fat percentage chart * docs: plan body fat percentage chart * feat: expose mobile body fat history * feat: add web body fat chart * feat: show body fat chart on web body page * feat: show body fat trend on mobile recovery * test: cover mobile recovery body fat response * test: type recovery body fat fixture * fix: close body fat chart review findings * fix: preserve body fat chart layout * fix: restore CI checks * test: strengthen processing mutations
Pull Request Review SummaryOverall, this is a clean and well-structured implementation integrating Summary of Findings & Issues:
Inline comments with suggested fixes have been posted on the diff. 🤖 Reviewed by codereviewbot.ai - Catch bugs before your team does. |
SummaryOverall, this is a well-structured PR integrating Key Highlights & Observations:
🤖 Reviewed by codereviewbot.ai - Catch bugs before your team does. |
There was a problem hiding this comment.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/server/src/repositories/climbing-repository.test.ts (1)
132-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for a non-default
gradePreference.
makeRepositoryalways constructsClimbingRepositorywith the default preference, so every assertion in this file exercises the identity path where the source system equals the display system. The conversion branch of#displayGrade, the merge of two source grades onto one display grade ingetVolumeByGrade, and the unparseable-grade fallback stay untested. AGENTS.md requires tests for changed behavior.💚 Proposed helper change
- function makeRepository(rows: Record<string, unknown>[] = []) { + function makeRepository( + rows: Record<string, unknown>[] = [], + gradePreference?: ClimbingGradePreference, + ) { const execute = vi.fn().mockResolvedValue(rows); - const repo = new ClimbingRepository(executeDb(execute), "user-1", "America/Los_Angeles"); + const repo = new ClimbingRepository( + executeDb(execute), + "user-1", + "America/Los_Angeles", + undefined, + gradePreference, + ); return { repo, execute }; }Add at least one case with
{ boulder: "font", route: "french" }that asserts the converted display grade and system.🤖 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 `@packages/server/src/repositories/climbing-repository.test.ts` around lines 132 - 136, Update makeRepository to accept an optional gradePreference override while preserving the current default, then add coverage using { boulder: "font", route: "french" } that verifies converted display grades and their systems. Include assertions covering getVolumeByGrade’s merging of source grades and the unparseable-grade fallback.Source: Path instructions
packages/server/src/contracts/mobile-dashboard-contracts.ts (1)
352-389: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winValidate
gradeSystemagainstclimbType.The schemas accept
fontfor a route andfrenchfor a boulder. This contradicts the boulder-only and route-only systems inpackages/server/src/climbing-grade-preferences.tsLines 12-15.Use a discriminated union or a refinement that rejects invalid
climbTypeandgradeSystempairs. Add output-contract tests for both invalid pair types.As per coding guidelines, “Use Zod to parse data crossing runtime boundaries.”
🤖 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 `@packages/server/src/contracts/mobile-dashboard-contracts.ts` around lines 352 - 389, Update the gradeProgression and volumeByGrade schemas to reject invalid climbType/gradeSystem combinations, using a discriminated union or Zod refinement aligned with the boulder-only and route-only systems defined in climbing-grade-preferences.ts. Preserve all existing field validation, and add output-contract tests covering both a route with font and a boulder with french.Source: Coding guidelines
🤖 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 `@docs/superpowers/plans/2026-08-11-climbing-grade-systems.md`:
- Around line 40-41: Update the expected displayGrade in the
convertClimbingGrade example to "6a+/6b+" so it matches the regression test in
climbing-grades.test.ts and avoids contradictory expectations.
- Around line 72-77: Update the plan’s commit and push steps, including Step 5
and the referenced repeated sections, to require explicit user or workflow
approval before performing those operations; otherwise leave commit and push
execution to the operator. Do not instruct the agent to switch branches, rebase,
commit, or push unconditionally.
In `@drizzle/0074_climbing_grade_systems.sql`:
- Around line 1-7: Update the migration that defines
fitness.climbing_grade_system so new values are inserted in the same order as
declared in src/db/schema/enums.ts, placing font after v_scale rather than
appending it after yds; use PostgreSQL enum insertion positioning for the
remaining values as needed to preserve the full declared order.
In `@packages/mobile/components/ClimbingAttemptLog.tsx`:
- Around line 98-102: Update ClimbingAttemptLog’s grade field to use a
selectable control populated from gradeOptionsForSystem(gradeSystem), matching
the web implementation instead of accepting arbitrary text. Clear grade whenever
the climb type changes, and add coverage that switches climb type then submits a
valid route grade, ensuring the mobile behavior remains equivalent to web.
In `@packages/server/src/repositories/climbing-repository.ts`:
- Around line 332-345: Update the summary aggregation around the summaries Map
so locationName is populated from a later non-null row when the existing summary
value is null. Preserve the first non-null location value and the existing
behavior for activities whose location remains null.
- Around line 83-94: Use the shared climbing grade definitions instead of
duplicating the nine-value list: in
packages/server/src/repositories/climbing-repository.ts:83-94, import and pass
CLIMBING_GRADE_SYSTEMS to z.enum for gradeSystemSchema; in
packages/server/src/routers/climbing.test.ts:117-126, import ClimbingGradeSystem
and type the optional gradeSystem field as gradeSystem?: ClimbingGradeSystem,
removing the inline union.
- Around line 413-427: Update the fallback display object in the mapping before
the sort to use a finite gradeSortValue sentinel instead of -Infinity, or adjust
the comparator to explicitly compare values without subtracting two non-finite
values. Keep failed-display rows consistently ordered and preserve descending
gradeSortValue ordering for valid grades.
In `@packages/server/src/routers/climbing.test.ts`:
- Around line 639-644: Update the test around
validFont.caller.logClimbingSession to mock or spy on validFont.execute and
assert it was called, rather than asserting an INTERNAL_SERVER_ERROR rejection.
Preserve the Font “6a” boulder input and use the execute-call assertion to
verify it passed input validation and reached the repository.
- Around line 117-126: Replace the inline gradeSystem union in the relevant test
type with the exported ClimbingGradeSystem type from
`@dofek/training/climbing-grades`, adding the necessary import and preserving the
optional property.
- Around line 129-135: Update makeClimbingSessionInput so its fallback grade is
selected from gradeSystem, using valid defaults for each supported bouldering
and route grading system instead of only climbType. Add test cases for
non-default systems such as font and french that omit grade, ensuring the
generated input uses a valid matching-system grade.
In `@packages/server/src/routers/climbing.ts`:
- Around line 208-266: Extract the repeated loadClimbingGradePreference and
ClimbingRepository construction into a shared helper near the router procedures.
Update the affected procedures, including gradeProgression, volumeByGrade, and
sessionSummary, to call that helper while preserving their existing query
methods and mapping behavior.
- Around line 205-219: Update the grade-preference change handling in
settings.set to invalidate the climbing query caches activityEntries,
gradeProgression, volumeByGrade, and sessionSummary whenever the preference
changes, while preserving the existing settings. invalidation.
In `@packages/server/src/routers/settings.ts`:
- Around line 28-31: Update the settings mutation handling for
CLIMBING_GRADE_PREFERENCE_SETTINGS_KEY to invalidate both
`${ctx.userId}:climbing.` and `${ctx.userId}:mobileDashboard.training` after the
preference changes, using the existing cache invalidation mechanism and
preserving other settings behavior.
In `@packages/training/src/climbing-grades.test.ts`:
- Around line 12-42: Extend the climbing-grade tests around
gradeSystemsForClimbType and related helpers with table-driven cases covering
French, UIAA, Ewbank, Saxon, Norwegian, and Brazilian Crux. For each route
system, assert a representative valid grade is included in
gradeOptionsForSystem, accepted by isValidClimbingGrade, ordered correctly, and
converted to the expected display result; retain the existing Font coverage and
use the established Sandbag mappings.
In `@packages/training/src/climbing-grades.ts`:
- Around line 56-66: Update the UIAA entry in SYSTEM_LABELS, which feeds
gradeSystemLabel(), to use a readable expanded label such as “International
Climbing and Mountaineering Federation (UIAA)” while leaving the other
grade-system labels unchanged.
In `@packages/web/src/components/ClimbingGradeSystemToggle.stories.tsx`:
- Around line 57-59: Expand the ClimbingGradeSystemToggle stories beyond
FontAndFrench to cover the default preference, loading, no stored preference,
and read/write error states. Use the existing ClimbingGradeSystemStoryFrame and
available preference-state controls or fixtures, preserving FontAndFrench as the
meaningful populated variant.
In `@packages/web/src/components/ClimbingGradeSystemToggle.tsx`:
- Around line 59-73: Prevent overlapping preference writes in setPreference by
disabling both selectors while mutation.isPending, or otherwise queueing writes
so they execute serially. Ensure rollback in the mutation onError handler cannot
restore an older previous value over a newer preference, and preserve optimistic
updates and settled cache invalidation.
- Around line 24-42: Replace the duplicated preferenceFrom validation with one
exported Zod schema or parser from the training package’s
ClimbingGradePreference module, adding the training package dependency if
needed. Update the settings-query boundaries in
packages/web/src/components/ClimbingGradeSystemToggle.tsx (24-42),
packages/web/src/routes/training/climbing.tsx (38-56),
packages/mobile/app/climbing-log.tsx (94-112), and
packages/mobile/app/settings.tsx (997-1015) to use that shared parser while
preserving the DEFAULT_CLIMBING_GRADE_PREFERENCE fallback behavior.
- Around line 45-50: Update ClimbingGradeSystemToggle.tsx:45-50,
training/climbing.tsx:124-125, and both settings.tsx ranges 255-279 and 527-581
to distinguish loading, error, and successful absence from preference
resolution; preserve cached data during background refetches, disable controls
and writes until settings.get succeeds, and use QueryStatePanel for applicable
web sections with explicit loading/error UI. Update climbing-log.tsx:22-24
likewise so it never derives or writes DEFAULT_CLIMBING_GRADE_PREFERENCE from an
unresolved query, while retaining the default only after a successful query with
no saved preference.
---
Outside diff comments:
In `@packages/server/src/contracts/mobile-dashboard-contracts.ts`:
- Around line 352-389: Update the gradeProgression and volumeByGrade schemas to
reject invalid climbType/gradeSystem combinations, using a discriminated union
or Zod refinement aligned with the boulder-only and route-only systems defined
in climbing-grade-preferences.ts. Preserve all existing field validation, and
add output-contract tests covering both a route with font and a boulder with
french.
In `@packages/server/src/repositories/climbing-repository.test.ts`:
- Around line 132-136: Update makeRepository to accept an optional
gradePreference override while preserving the current default, then add coverage
using { boulder: "font", route: "french" } that verifies converted display
grades and their systems. Include assertions covering getVolumeByGrade’s merging
of source grades and the unparseable-grade fallback.
🪄 Autofix
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: ASSERTIVE
Plan: Pro Plus
Run ID: dfec9b0b-d23a-4a42-9a58-2f5cc65f92ae
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
cspell.jsondocs/schema.dbmldocs/schema.pumldocs/superpowers/plans/2026-08-11-climbing-grade-systems.mddocs/superpowers/specs/2026-08-11-climbing-grade-systems-design.mddrizzle/0074_climbing_grade_systems.sqldrizzle/meta/_journal.jsonpackages/mobile/app/climbing-log.tsxpackages/mobile/app/settings.tsxpackages/mobile/components/ClimbingAttemptLog.test.tsxpackages/mobile/components/ClimbingAttemptLog.tsxpackages/server/src/climbing-grade-preferences.test.tspackages/server/src/climbing-grade-preferences.tspackages/server/src/contracts/mobile-dashboard-contracts.tspackages/server/src/repositories/climbing-repository.test.tspackages/server/src/repositories/climbing-repository.tspackages/server/src/repositories/climbing-training-log-repository.tspackages/server/src/routers/climbing.test.tspackages/server/src/routers/climbing.tspackages/server/src/routers/settings.tspackages/server/src/services/mobile-training-tab.tspackages/training/README.mdpackages/training/package.jsonpackages/training/src/climbing-grades.test.tspackages/training/src/climbing-grades.tspackages/training/src/openbeta-sandbag.d.tspackages/web/src/components/ClimbingAttemptLog.test.tsxpackages/web/src/components/ClimbingAttemptLog.tsxpackages/web/src/components/ClimbingGradeSystemToggle.stories.tsxpackages/web/src/components/ClimbingGradeSystemToggle.tsxpackages/web/src/pages/SettingsPage.tsxpackages/web/src/routes/training/climbing.test.tsxpackages/web/src/routes/training/climbing.tsxsrc/db/drizzle-schema.test.tssrc/db/schema/enums.ts
|
🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews. |
|
🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews. |
|
🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews. |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/superpowers/plans/2026-08-11-climbing-grade-systems.md (2)
24-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winKeep the Task 1 file list and commit complete.
The stack lists
cspell.jsonin this grade-domain cohort, but Lines 24-29 and 72-76 omit it. If Task 1 updates spelling entries for the new grade-system names, addcspell.jsonto both lists. Otherwise, state that no cspell change is required.Also applies to: 72-76
🤖 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 `@docs/superpowers/plans/2026-08-11-climbing-grade-systems.md` around lines 24 - 29, Update the Task 1 file lists in the climbing-grade plan to include cspell.json wherever spelling entries for the new grade-system names are changed; if no spelling update is needed, explicitly state that no cspell change is required in both lists.
33-43: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd regression coverage for
sortValueand grade ordering.The interface promises
sortValue, and Line 64 defines it from Sandbag score ranges. The planned test checks onlydisplaySystemanddisplayGrade. Assert the expectedsortValueand compare at least two valid grades, including a boundary case, so incorrect range averaging cannot pass while display conversion still succeeds.As per path instructions:
**/*.{test,integration.test}.{ts,tsx}requires tests for changed behavior.Also applies to: 64-64
🤖 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 `@docs/superpowers/plans/2026-08-11-climbing-grade-systems.md` around lines 33 - 43, Expand the Step 1 domain tests around convertClimbingGrade to assert the expected sortValue for converted grades and verify ordering by comparing at least two valid grades, including a Sandbag range boundary case. Keep the existing display-system, display-grade, and cross-discipline assertions, and ensure the tests detect incorrect range averaging.Source: Path instructions
packages/mobile/app/settings.tsx (1)
529-529: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftSplit
SettingsScreenbefore merge.This addition brings
packages/mobile/app/settings.tsxto 1,003 lines. Extract a focused settings section and its handlers so the source module stays within the limit. As per coding guidelines: “No TypeScript file may exceed 1000 lines; split large files into focused modules.”🤖 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 `@packages/mobile/app/settings.tsx` at line 529, Split SettingsScreen into focused modules before merging, extracting the goals-models settings section and its associated handlers from settings.tsx while preserving existing behavior and wiring. Keep the resulting TypeScript files under the 1,000-line limit, and update imports/exports so SettingsScreen continues rendering the extracted section correctly.Source: Coding guidelines
🤖 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 `@packages/mobile/app/settings.tsx`:
- Around line 541-588: Update the grade-system control containers around the
BOULDER_GRADE_SYSTEMS and ROUTE_GRADE_SYSTEMS maps to use a wrapping layout or
vertical list instead of the single-row styles.unitRow. Ensure styles.unitButton
has a usable minimum width and does not shrink each choice to an unusable size
on narrow screens, while preserving the existing selection and press behavior.
In `@packages/training/src/climbing-grades.ts`:
- Around line 135-148: Define a Zod schema in the shared climbing-grades module
for the boulder and route preference values, using the existing grade-system
validators; update resolveClimbingGradePreference() to validate value via
safeParse() and return the parsed data on success. Return
DEFAULT_CLIMBING_GRADE_PREFERENCE when parsing fails, replacing the manual
unknown narrowing while preserving the current fallback behavior.
In `@packages/web/src/components/ClimbingGradeSystemToggle.stories.tsx`:
- Around line 71-85: The ClimbingGradeSystemToggle stories are missing coverage
for the pending-save state. Add a Saving story using
ClimbingGradeSystemStoryFrame that resolves settings.get while keeping
settings.set pending, then verify that selecting a grade disables both selectors
while the save is in progress.
---
Outside diff comments:
In `@docs/superpowers/plans/2026-08-11-climbing-grade-systems.md`:
- Around line 24-29: Update the Task 1 file lists in the climbing-grade plan to
include cspell.json wherever spelling entries for the new grade-system names are
changed; if no spelling update is needed, explicitly state that no cspell change
is required in both lists.
- Around line 33-43: Expand the Step 1 domain tests around convertClimbingGrade
to assert the expected sortValue for converted grades and verify ordering by
comparing at least two valid grades, including a Sandbag range boundary case.
Keep the existing display-system, display-grade, and cross-discipline
assertions, and ensure the tests detect incorrect range averaging.
In `@packages/mobile/app/settings.tsx`:
- Line 529: Split SettingsScreen into focused modules before merging, extracting
the goals-models settings section and its associated handlers from settings.tsx
while preserving existing behavior and wiring. Keep the resulting TypeScript
files under the 1,000-line limit, and update imports/exports so SettingsScreen
continues rendering the extracted section correctly.
🪄 Autofix
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: ASSERTIVE
Plan: Pro Plus
Run ID: cc98da6d-dbbc-44e3-81a4-81c24f777182
📒 Files selected for processing (21)
docs/superpowers/plans/2026-08-11-climbing-grade-systems.mddrizzle/0074_climbing_grade_systems.sqlpackages/mobile/app/climbing-log.tsxpackages/mobile/app/settings.styles.tspackages/mobile/app/settings.tsxpackages/mobile/components/ClimbingAttemptLog.test.tsxpackages/mobile/components/ClimbingAttemptLog.tsxpackages/server/src/contracts/mobile-dashboard-contracts.test.tspackages/server/src/contracts/mobile-dashboard-contracts.tspackages/server/src/repositories/climbing-repository.test.tspackages/server/src/repositories/climbing-repository.tspackages/server/src/routers/climbing.integration.test.tspackages/server/src/routers/climbing.test.tspackages/server/src/routers/climbing.tspackages/server/src/routers/recovery-settings-sleep-need-sport-settings.test.tspackages/server/src/routers/settings.tspackages/training/src/climbing-grades.test.tspackages/training/src/climbing-grades.tspackages/web/src/components/ClimbingGradeSystemToggle.stories.tsxpackages/web/src/components/ClimbingGradeSystemToggle.tsxpackages/web/src/routes/training/climbing.tsx
|
🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews. |
|
🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews. |
|
Addressed the remaining CodeRabbit PR-level feedback in b8224ce.
|
|
🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews. |
|
🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews. |
What changed
Why
Climbers can now view and log boulder and route grades in their preferred relevant grading systems, analogous to the existing km/miles setting.
Validation
pnpm lintpnpm typecheckSummary by Sourcery
Add Sandbag-backed multi-system climbing grade preferences, Hangboarding analytics and detail surfaces, and body-fat history visuals while tightening processing status alerts, Apple Health interval persistence, and telemetry/noise handling across server, web, and mobile.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Tests:
Summary by cubic
Adds a grade-system switcher with persisted boulder and route preferences. Validates, converts, and orders grades server-side using
@openbeta/sandbag, so all climbing views show grades in the user’s chosen systems.New Features
climbingGradeSystemswith defaults and schema validation; resolve viaresolveClimbingGradePreference.gradeSortValuewith Sandbag scores.@dofek/training/climbing-gradesfor validation, conversion, grade lists, and labels; load preference server-side for conversions.ClimbingGradeSystemToggle(with Storybook) and apply preference in manual logging. Mobile: addClimbingGradeSystemSettingsand apply preference in logging; displays use server-converted grades. Invalidatesettingsandclimbingcaches after changes.Dependencies
@openbeta/sandbag@0.0.55andzod@4.4.3to@dofek/training.fitness.climbing_grade_systemwithfont,french,uiaa,ewbank,saxon,norwegian,brazilian_crux.Written for commit 3f44f08. Summary will update on new commits.
Summary by CodeRabbit