Skip to content

BaseballHelm finish: #379 stats reconciliation, heavy mobile pass, CI gates, tests — 45-PR overnight batch - #856

Merged
njrini99-code merged 45 commits into
mainfrom
batch/bbh-finish-0714
Jul 15, 2026
Merged

BaseballHelm finish: #379 stats reconciliation, heavy mobile pass, CI gates, tests — 45-PR overnight batch#856
njrini99-code merged 45 commits into
mainfrom
batch/bbh-finish-0714

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

What this is

The 2026-07-14→15 overnight mission batch: 45 PRs, each individually built by a Sonnet worker, adversarially reviewed (every finding either fixed+re-verified or documented), and merged into this integration branch. Every PR body carries its own gate evidence.

Assembled-branch gates (run on this exact tip, 66ec7ee)

  • typecheck: exit 0 · lint-ratchet: exit 0 · import-cycles: 33 known, none new
  • unit: 5159 passed, 0 failed · business contracts: 5235 passed, 0 failed

Contents by lane

Deploy note

Merging this deploys prod (main auto-deploys). CI on this PR runs the full required set including the new hard gates.

Held for Nick (documented in the morning report)

Journey/pipeline vocabulary decision (memo ready) · #379 legacy backfill question · marketing root · dual upload-wizard consolidation · velocity eventDerived wiring follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

njrini99-code and others added 30 commits July 14, 2026 23:43
The app write path (createCamp/updateCamp in
src/app/baseball/actions/camps.ts) always writes status:'published'
because the baseball_camps_select RLS policy only exposes non-owner
(player) reads when status = 'published'. The E2E seed fixture still
inserted the seeded "E2E Prospect Camp" with status:'active', making
it invisible to the player-facing camps browse query and silently
failing the "Camps - Player Flow" specs in e2e/camps.spec.ts.

No schema/migration change needed — baseball_camps.status is a free
text column with no CHECK constraint, and 'published' is already the
value the real app writes.


Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…gREST cap (#813)

getEliteStatEvents built its 4 event-grain queries (baseball_pitch_events,
baseball_batted_ball_events, baseball_swing_events,
baseball_plate_appearances) with no .order()/.range() at all, so any team
with more than 1000 rows in a table (a single season of pitch-by-pitch data
easily exceeds this) silently had derived metrics — CSW%, chase/whiff rate,
velo-decay, pitch mix, hard-hit rate, command heatmap — computed from an
arbitrary 1000-row slice, not even a stable "most recent 1000".

Convert all 4 buildQuery(...) call sites to fetchAllRowsResult with a stable
.order('id', {ascending:true}) + .range(from,to), mirroring the identical,
already-proven pattern coachhelm/engine-run.ts uses against these same
tables. The existing team/staff-gate envelope and error-branch shape are
unchanged; only the data queries are now paginated.

Adds a focused pagination regression test (the file's own existing test
suite is deliberately pure-math-only per its header) that simulates the
real PostgREST 1000-row-per-response cap in a mock query builder and
asserts getEliteStatEvents returns all 1500 rows across 2 pages, issues a
stable .order('id') before ranging, and still surfaces an honest error
(no throw) on a failed page read.


Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…) (#814)

PrimaryCtaRow rendered up to three peer pill buttons (Check In,
Acknowledge, View Today Plan/Schedule) with no priority collapse. Now
resolves a single filled primary CTA by priority (pending ack > due
check-in > view plan/schedule per the issue's acceptance criteria) and
demotes any remaining applicable action(s) to inline text links —
visible, not hidden, just secondary.

Also reorders NextEventHero ahead of the one-time "Activate Recruiting"
nudge card so real schedule content reaches the first viewport before
a promotional card, addressing the issue's third acceptance criterion
without touching unrelated component internals.


Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…port + scout-packet (#815)

player-passport.ts and scout-packet.ts still summed innings-pitched with
naive base-10 `+=` and divided ERA/WHIP by that raw notation sum, so
6.1 + 6.2 yielded 12.3 instead of the correct 13.0 — the exact bug #434
already fixed in stats-center.ts and BoxScoreView.tsx via the shared
src/lib/baseball/innings.ts helper (sumInningsPitched/ipToInnings), but
never wired into these two read models.

Extracted the season pitching-line computation into a small pure,
exported `summarizePitchingSeason(games, ipRows, totals)` in each file
(same shape as stats-center.ts's addPitching/finalizePitching split) so
the fix is directly unit-testable without mocking Supabase. Both files
now sum IP via outs and derive era/whip from ipToInnings(ip) (true
innings), never the notation value — mirroring the proven pattern
exactly, no shape/signature changes to either read model's public
output.

Added src/lib/baseball/read-models/__tests__/{player-passport,scout-packet}-innings.test.ts
proving the 5.2 + 3.2 -> 9.1 thirds case (not 8.4), the classic 6.1 + 6.2
-> 13.0 case (not 12.3), and that era/whip divide by true innings (28/3)
rather than the 9.1 notation value.

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…offsets (#485) (#816)

CalendarFairway's outer shell derived its full-bleed height from a flat,
hand-guessed `h-[calc(100vh-5.5rem-...)]` that predated the current AppShell
(sticky glass top bar + a coach-only Team-hub sub-nav strip) and never
matched either, producing double-scroll/dead-space on mobile.

Replace it with two literal shell strings (coach vs. player) that subtract
the shell's REAL offsets: the shared `--golf-mobile-header-offset` CSS var
(top bar), the Team-hub HubSubNav row (45px, coach-only — Calendar has no
sub-nav for players, confirmed via resolve-active-hub.ts's player branch),
and AppShell's own mirrored bottom-nav clearance — using 100dvh instead of
100vh so iOS Safari's dynamic toolbar can't leave a sliver. Kept the
determinate-height ancestor PremiumCalendarClient's internal h-full/flex-1
scroll region needs (verified it collapses to 0 without one) rather than
going fully in-flow like golf's native FairwayCalendar, since baseball's
calendar still reuses that shared component verbatim.

Added CalendarFairway.shell-height.test.tsx pinning the corrected class
strings so the flat 100vh guess can't silently return.

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…allAction (#394) (#819)

All 10 exports across discover.ts (4) and insights.ts (6) ran only through
the guard-free withAdminObserved observability decorator — no auth/context/
capability enforcement, no demo-write guard, no Sentry scoping.

- discover.ts: wrap all 4 read-only exports (getDiscoverPlayers,
  getDiscoverTeams, getWatchlistIds, getStateCounts) with withBaseballAction
  (requireActiveContext:false, demoSafe:true, no requiredCapability — Discover
  is deliberately cross-team by product design). Impl bodies unchanged.
- insights.ts: deleted generateTeamInsights/getTeamInsights as dead code —
  zero callers repo-wide, superseded by engine-run.ts's shared reconciliation
  core, and generateTeamInsightsImpl carried a live #472-class coachId/user.id
  domain-confusion bug that was never exercised because nothing called it.
- insights.ts: migrated dismissInsight/markInsightAddressed/
  submitInsightFeedback onto withBaseballAction (ownership-gated in-body, no
  requiredCapability, requireActiveContext:false — mirrors dev-plans.ts's
  precedent). Each export is now a thin try/catch wrapper around the guarded
  action so a thrown BaseballUnauthorizedError/BaseballDemoReadOnlyError/
  BaseballActionError is translated back into the existing
  {success:false,error} envelope — PlayerInsightsPanel.tsx calls these inside
  an un-try/catch'd startTransition and depends on always getting a resolved
  value.
- Relocated resolveCallerCoachId out of the 'use server' insights.ts into a
  plain module (src/lib/baseball/insights/resolve-coach-id.ts) so it stops
  being an accidental public server-action endpoint.
- Updated feature-registry.ts's baseball_insights action-name manifest to
  drop the deleted/relocated names.
- Added the @sentry/nextjs + @/lib/demo/baseball-config.server mocks
  discover-privacy.test.ts and insight-lifecycle.test.ts now need (the real
  withBaseballAction wrapper touches both); updated insight-lifecycle.test.ts's
  resolveCallerCoachId import path.

Known accepted risk (per spec): withBaseballAction's AUTH step throws
unconditionally on no session, vs. discover.ts's prior graceful
empty-shape-on-no-session return. DiscoverClient.tsx already try/catches
every call site and falls back to a visible error state, so this is a
visible-but-handled edge case on an already-authenticated dashboard route,
not a crash.

Gates: typecheck clean; eslint --max-warnings 0 clean on all 6 touched
files; targeted vitest (discover-privacy, insight-lifecycle,
coverage-contract.observability) 60/60 passing across all 4 vitest projects.


Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e-menu-button (#820)

Batch-H cleanup, re-verified zero importers before deleting:

- PlayerPassportCard.tsx + its index.ts barrel export: only self-references
  and stale "legacy" doc-comment mentions remained; PlayerPassportFairway
  replaced it everywhere. Deleted both.
- header.tsx: repo-wide grep (import path + <Header usage) confirmed zero
  real importers left (the only <Header> JSX hits are an unrelated golf
  FairwayDrivingSpray component). isRedesignEnabled()'s always-true flag had
  made its legacy-chrome branch permanently dead. Deleted the whole file
  rather than collapsing the fork, per fleet-verifier guidance.
- mobile-menu-button.tsx: header.tsx was its only importer (page-header.tsx's
  MobileMenuButton resolves to the unrelated components/golf/MobileMenuButton).
  Now orphaned, deleted.
- mode-toggle.tsx: kept — it has a real importer (baseball/coach/ModeToggle.tsx,
  JUCOModeToggle), so it does not qualify as a dead sibling here even though
  that importer's own only caller was the now-deleted header.tsx. Left as a
  follow-up note rather than expanding scope.
- flag.ts: corrected the now-stale "NOT dead: header.tsx" caveat on
  isRedesignEnabled() — the function is still very much alive via dozens of
  live src/components/fairway/** call sites, just no longer via header.tsx.
- MessagesFairway.tsx: left untouched per task ownership (another task owns
  its stale doc-comment fix).

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…bels (#821)

Chunk 1 (dead query): the public player profile's recruiting-interests
select listed a nonexistent `school_name` column, which PostgREST rejects
with 42703. The `{ error }` was never checked, so `recruitingInterests`
silently resolved to `[]` and the Dream Schools section (gated on
`recruitingInterests.length > 0`) never rendered. Drop the bad column from
the select (the Row shape is id/interest_level/organization via the
`organizations` join) and log the error so a future regression is visible.

Chunk 2 (label/enum dedup): the 5 pipeline stage ids were hand-copied in 4
places that had already drifted apart in production — `getPipelineStageLabel`
(utils.ts) called the `watchlist` stage "Prospects" while `PIPELINE_STAGES`
(stages.ts) calls the same stage "Watchlist", both live on the same Pipeline
board. Make `PIPELINE_STAGES` the single source: `getPipelineStageLabel` is
now a lookup into it, and `WatchlistSchemas.updateStatus` derives its zod
enum from `PIPELINE_STAGES.map(s => s.id)` instead of a separate literal
array. Resolves the collision in favor of stages.ts's "Watchlist" (no code
comment defended "Prospects").

Found but out of scope: `PipelineStatusDot` in the frozen
src/components/ui/status-dot.tsx carries a 5th copy of the same label map
(also says "Prospects" for `watchlist`) but appears unused anywhere in the
app; left untouched per the frozen-file rule.

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…xpense identity + denial paths) (#823)

src/app/baseball/actions/travel.ts had zero test coverage (BASEBALLHELM_FEATURE_READINESS_MATRIX.md:73). Adds src/app/baseball/actions/__tests__/travel.test.ts pinning:
- createItinerary writes created_by = ctx.activeCoachId; addExpense writes created_by = ctx.user.id — deliberately different identity spaces, asserted with distinct fixture values so a future mocking mistake can't silently collapse them.
- the two-layer team-capability guard: withBaseballAction's own pre-check only validates ctx.activeTeamId; createItinerary/addExpense both carry an explicit re-check when the caller-supplied teamId differs, which this locks in as a real gate (not just the wrapper).
- not-found + capability-denial paths for updateItinerary/deleteItinerary/deleteExpense, which resolve their team from the EXISTING row rather than caller input.
- a discovered quirk: invalid create-itinerary input degrades to the generic BaseballActionError message, not the raw zod message, because schema.parse() runs inside the withBaseballAction-wrapped body and ZodError isn't in the wrapper's rethrow whitelist — the ZodError branch in createItinerary's own try/catch is presently unreachable. Test pins actual behavior; no production code changed.


Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…gated writes (#824)

Adds src/app/baseball/actions/__tests__/documents-write-capability.test.ts,
a new file exercising what documents-capability.test.ts (one assertion) and
documents.test.ts (read-path uploader-embed regression only) leave
untouched: the six write actions in src/app/baseball/actions/documents.ts.

Seeds a real baseball_team_coach_staff row so requireBaseballCapability's
actual resolution logic runs (not stubbed), mirroring
dev-plans-coach-gating.test.ts's idiom. Covers:
  - capability-less rejection / capability-holder success on all six writes
    (upload/create/update/delete/uploadNewVersion/revert)
  - team-id forging is blocked: upload/create re-check capability against an
    explicit teamId/team_id that differs from the active team; update/delete
    resolve the gated team from the EXISTING document row, never the
    caller's active team; uploadNewVersion's optional teamId argument is
    proven to never feed the capability gate at all (only document.team_id
    does — the arg only shapes the storage path)
  - signed-URL call sites (getPreviewUrl, getVersionHistory, getTeamDocuments/
    withFreshDocumentUrls, upload, uploadNewVersion, revertToVersion) assert
    the exact storage_path + 3600s TTL, including that revertToVersion
    re-signs the ORIGINAL reverted-to path rather than uploading a new file
  - deleteBaseballDocument's storage.remove is scoped to exactly one
    document's version paths, never a sibling document's

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#825)

savePractice/publishPractice/recordPracticeAttendance/getPlayerPractices and
saveObjective/savePracticeRecap/runPracticeEffectiveness/setReviewDisposition
had zero action-level tests (BASEBALLHELM_FEATURE_READINESS_MATRIX.md:65-66,142).
Adds three new test files locking down the stage-then-swap block replacement,
server-side publish validation gate, publish-only calendar attach + per-player
notify, the attendance upsert's no-delete contract, player-view block
visibility + anchor-dependent class-conflict detection, target_metric registry
validation, recap reps clamping, the effectiveness engine's disposition-
preserving upsert + fail-closed read guard + hardcoded staff_only visibility,
and the pure practice-validation issue codes (no direct test existed before).


Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…y, Signal Inbox, Video Library (#826)

Extends src/contracts/baseball/ (unit-project lane) with 6 new pinning test
files covering the 3 surfaces #377 named but the existing contract suite
never exercised directly (every prior reference to getPlayerToday/
getSignalInbox mocked the read model itself instead of running it):

- product-trust/player-today-honest-loop.test.ts — getPlayerToday never
  fabricates a green readiness band (no check-in -> available:false; a
  submitted illness check-in surfaces the real red band/reasons via the
  UNMOCKED computeReadiness); assignments/coachActions/tasks/coachNotes
  degrade a sub-read failure to available:true+items:[]+a distinguishing
  `error` string, never conflated with genuine emptiness; a hand-entered
  recent-stat row never gets a fabricated trust/provenance object.
- access/player-today-self-scope.test.ts — self-only authorization envelope
  (no baseball_players row, or membership on a different team ->
  authorized:false, never a caller-suppliable player id); coachNotes scope
  isolation (staff_only, archived, and a different player's note never leak
  into the player-visible feed).
- coachhelm/signal-inbox-evidence.test.ts — getSignalInbox's OWN honesty
  invariants (distinct from signal-evidence.test.ts's signalFromInsight
  promotion-gate pin): sampleTooSmall catches a sub-threshold sample_n even
  when a generator left disposition at 'new'; confidence is never fabricated
  (null stays null, a legacy 0-100 value is honestly normalized); sourceRefs
  reflect real persisted refs; sub-read failures are distinguishable from
  genuine emptiness.
- access/signal-inbox-staff-scope.test.ts — staff-only envelope + cross-team
  signal isolation in the same table.
- product-trust/video-honest-empty-state.test.ts — Event/Tagged/Evidence
  views' `hasVideoEvents` gate distinguishes "no film at all" from "film
  exists but none qualifies for this view."
- access/video-visibility-scope.test.ts — Library/Player views scoped
  through the requested team's roster; Event/Tagged/Evidence scoped by
  team_id; Evidence's signal-metadata enrichment never leaks a different
  team's signal title even if linked_signal_id ever pointed cross-team.

New helper: src/test/fixtures/fake-supabase-fail-select.ts (NOT an edit to
the shared createFakeSupabase fixture) — makes one table's SELECT chain fail
with a given message, generalizing the ad hoc monkey-patched-`fake.from`
idiom already used in golf's recurring-events.test.ts, needed to prove a
sub-read failure is distinguishable from a genuinely empty table.

All 6 files are plain `*.test.ts` (not `*.contract.test.ts`) so they run in
vitest's `unit` project, not the 4-file `business` CI lane, per the spec's
explicit naming risk.

Gates: typecheck clean (tsc --noEmit, 0 errors repo-wide); eslint --max-warnings 0
clean on all 7 touched files; `vitest run --project unit src/contracts/baseball/`
— 14 files, 93 tests, all passing (43 from the 6 new files + all 8 pre-existing
files unaffected).

No product-code changes — every assertion pins CURRENT behavior; no
product-truth violation was discovered while writing these.

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#481) (#817)

* fix(baseball): account for FairwayBottomNav clearance in Messages viewport math (#481)

MessagesFairway's SHELL and ConversationClient's two height calcs only
subtracted the AppShell top bar (4rem), never the fixed bottom-tab-bar
clearance AppShell's content wrapper adds as extra padding-bottom whenever
`bottomNav` is present. Since that ancestor is `min-h-dvh` (grows, not
clips), the document became taller than the viewport and the composer's
bottom portion rendered under the opaque FairwayBottomNav at the default
scroll position on phones.

Fix: subtract the existing global `--golf-mobile-bottom-nav-offset` var
(globals.css `:root`; already the repo's shared idiom for this exact
clearance — see KeyboardShortcutHint.tsx / InsightBulkActions.tsx / the
golf round-review page) from both surfaces' height budgets. It resolves to
`56px + env(safe-area-inset-bottom)` below `md` and `0px` at `md`+, i.e.
it already matches AppShell's own bottomNav padding branch term-for-term
and zeroes automatically once the bar stops rendering — no AppShell.tsx
change needed.

Also fixed: ConversationClient's two `100dvh-4rem` calcs were additionally
missing the safe-area-inset-top term MessagesFairway's SHELL already had.
Rewrote messages/[id]/loading.tsx's Suspense fallback (still on legacy
warm-200/cream-50 tokens and the bare `100dvh-4rem` calc) to match
ConversationClient's actual Living-Annual chrome and the fixed height
formula. Scrubbed MessagesFairway.tsx's stale doc-comment claiming an
`isRedesignEnabled()` page fork gates this component — the flag is
hardcoded true (Wave W1) and MessagesClient.tsx always renders this shell
unconditionally.

Deviation from spec: the spec's approach proposed exposing a NEW CSS var
on AppShell.tsx (shared with golf). A repo-wide grep found
`--golf-mobile-bottom-nav-offset` already exists globally in globals.css
for exactly this clearance, already consumed by golf components — so no
AppShell.tsx or BaseballFairwayShell.tsx (frozen) edit was needed at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): account for coach HubSubNav height in Messages viewport (#481)

Review found the prior fix still clipped the composer behind
FairwayBottomNav for coaches: on /baseball/dashboard/messages(/[id]),
resolveActiveHub() mounts a real HubSubNav ("Messages · Announcements")
above these panes for coach role, but the height formulas never
subtracted its height, so the pane's top (and thus its bottom edge)
was pushed ~45px into the bottom nav's fixed region.

HubSubNav (hub-sub-nav.tsx) now measures its own rendered height via
a ResizeObserver-backed callback ref and publishes it to a new global
CSS var, --baseball-hub-subnav-offset (0px default in globals.css,
reset to 0px on unmount so subnav-less routes never inherit a stale
value). MessagesFairway.tsx, ConversationClient.tsx (both branches),
and messages/[id]/loading.tsx all subtract this var alongside the
existing top-bar/safe-area/bottom-nav terms, so the box's own bottom
edge clears FairwayBottomNav regardless of whether a sub-nav strip is
mounted above it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…nk nav (#482) (#818)

* fix(baseball): compact public player-profile mobile hero, fix back-link nav (#482)

The public player profile's mobile hero was a tall (208px) decorative
banner with absolute controls that ignored safe-area insets, a back
control hardcoded to /baseball/dashboard/discover regardless of who
opened the shared link, and two competing coach CTAs (Watchlist +
Message) crammed into the overlay at once.

- Hero banner: h-52 md:h-64 -> h-28 sm:h-36 md:h-64 (112px on phones,
  under the ~120px acceptance bar; desktop unchanged).
- Passport-card overlap and mobile avatar re-tuned to match
  (-mt-28 -> -mt-10; w-32 h-32 -> w-20 h-20 avatar; verification badge
  scaled down proportionally) so identity clears the fold at 390x844
  without colliding with the shrunk hero's controls.
- Back control and coach-action bar now use
  `max(1rem, calc(env(safe-area-inset-top) + 0.5rem))` instead of a
  fixed `top-4`, clearing notch/Dynamic Island devices.
- Back control is a history-aware button (mirrors BackChevron in
  src/components/ui/page-header.tsx): router.back() when history
  exists, else a contextual fallback (Discover for coaches, Player
  Today for the self-view case, home otherwise) instead of a single
  hardcoded Discover link.
- Coach actions collapse to one primary CTA (Watchlist toggle) on
  <md; Message stays a full CTA at md+ and tucks into a small
  overflow menu on phones (dismissed via Escape or blur-outside).

Deviation from the spec's suggested approach: used a small
hand-rolled overflow menu (blur/Escape-dismiss, ink/paper styling)
instead of src/components/ui/dropdown-menu.tsx — that component is
unused anywhere in baseball and carries the cream/warm/primary token
set from a different visual language than this page's paper/ink
Living-Annual surface; a plain paper-card menu keeps the page's
existing "one dark surface, rest is paper" rule intact.

Coordination: left the dead recruiting-interests query in
page.tsx (~lines 119-133) untouched per the other in-flight task
that owns it.

Gates:
- npm run typecheck: clean, no output.
- npx eslint --max-warnings 0 on the touched file: clean (0
  problems) after adding tabIndex={-1} to the role="menu" panel and
  two precedented eslint-disable-next-line comments (helm/no-raw-button
  for the file's already-established raw-button idiom; jsx-a11y/no-static-
  element-interactions for the blur-only dismiss wrapper, matching the
  same disable pattern already used across
  src/app/golf/admin/crm/components/*).
- No existing test file imports PlayerProfileClient.tsx (confirmed via
  repo-wide grep) and the spec names no test file to add; ran the
  adjacent src/lib/baseball/__tests__/public-profile-anon-gating.test.ts
  as a sanity check (80/80 passed, unaffected by this change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(baseball): clear coach-action row on notched-iPhone worst case (#818 review)

The mobile hero-height/passport-overlap pairing (h-28/112px hero, -mt-10/40px
overlap) collided with the safe-area fix on real notched/Dynamic-Island
iPhones: env(safe-area-inset-top) is a live value (viewportFit:'cover' in
layout.tsx), and at its ~59px worst case (14/15/16 Pro Dynamic Island,
portrait) the coach action row's bottom edge landed at ~111px while the
opaque passport card's top edge stayed a fixed 72px — a 24-39px band where
the card visually covered and intercepted taps on the Watchlist toggle /
overflow-menu trigger.

Re-derive the pairing so the card clears the row even at that worst case:
mobile hero h-28 -> h-32 (128px), overlap -mt-10 -> -mt-2 (8px), giving the
passport card a top edge of 120px against a worst-case row bottom of 111px
(9px of real clearance). sm:/md: breakpoints are untouched (explicit
sm:-mt-10 preserves the prior sm-range look now that the base value changed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… (#822)

* test(baseball): add pgTAP suite for scope_player_ids staff isolation (#406)

can_view_baseball_player() (20260630180000_baseball_scope_player_ids_rls.sql)
had no behavioral test proving its scope_player_ids allowlist semantics.
Add supabase/tests/rls/baseball_scope_player_ids_isolation.sql, mirroring the
seed-then-impersonate pattern already proven in
baseball_recalc_body_guards.sql / golf_coach_insights_cross_tenant_select.sql:
seeds one team + one scoped (non-head, non-primary, active) staff coach,
impersonates via SET LOCAL request.jwt.claims, and asserts empty/NULL scope
(fallthrough visibility), a one-player allowlist, a multi-player allowlist,
and a suspended-status short-circuit ahead of the scope check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* test(baseball): add cross-staff isolation case to scope_player_ids pgTAP suite

Review flagged that the "isolation" suite only ever seeded one staff-coach
row per team, so a regression dropping/weakening the coach_id predicate in
can_view_baseball_player()'s row lookup (WHERE tcs.team_id = p_team_id AND
tcs.coach_id = v_coach_id LIMIT 1) would be structurally invisible to it.

Adds Case D: a second staff-coach row on the same team with a wider
scope_player_ids allowlist, then asserts (impersonating the first coach)
that its results are unaffected by the second row's existence or scope.
plan(13) -> plan(15) for the two new assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ests (#379 Phase 0) (#827)

* fix(baseball): reconcile seed stats with Stats Center + add drift/smoke tests (#379 Phase 0)

scripts/seed-baseball-stats.mjs now writes BOTH stats layers for every
stat_type:'game' session it seeds: the legacy flat/aggregate rows
(unchanged, for grandfathered consumers) AND matching
baseball_box_score_batting/_pitching rows for a synthetic completed game,
followed by the same recalculate_baseball_season_stats RPC games.ts's
box-score save flow calls. Every write is an upsert (game id / (game_id,
player_id) keys) — never delete-then-reinsert. seed-baseball-demo.ts's
stale doc comment (claiming Stats Center should stay empty) now points at
its stats-seeding companion script instead.

Adds the two tests #379's acceptance criteria ask for:
- seeded-stats-non-empty.smoke.test.ts runs the seed script's own
  seedTeamStats() against a fake Supabase client and asserts
  getStatsCenter() is non-empty for a "seeded" team.
- command-center-stats-center-drift.test.ts asserts Command Center's
  legacy-layer game average and Stats Center's box-score-layer average
  agree for the same player/team/season.

Adds manifest entries for the two new test files (they reference the
deprecated table names as fixture/doc data) so the existing stat-layer
contract test stays green, and updates stats-architecture.md's status
notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): review fixes for #379 Phase 0 — shared game rows + honest drift test

mustFix #1: buildBoxScoreRowsForSessions minted a PRIVATE baseball_games row
per player per session (id baked in player_id), so seeding a normal roster
inserted 150-350 fabricated single-player "completed" games with no scores —
flooding Stats Center's game counts, the Decision Room game-results rail, and
the Postgame review game-picker. Fixed by introducing a shared team-level
game schedule (buildTeamGameSchedule) that every player's game-type sessions
attach to, and regrouping buildBoxScoreRowsForSessions/seedTeamStats to
derive ONE shared, upserted game row per distinct team+date (never
player-scoped), with every attending player's batting/pitching lines
referencing that same game_id — mirroring the existing correct pattern in
scripts/seed-rini-baseball-demo.ts. Verified empirically: 20 players now
produce 23 shared team games (not ~150-350 private ones), each player
appearing in ~9-13 of them.

mustFix #2: the drift test's "passing" case hand-built a zero-practice-
session baseball_player_aggregates fixture that bypassed seedTeamStats()
entirely, overclaiming that Command Center's blended average and Stats
Center's game-only average reconcile for realistic (~40% practice) players —
they don't; that's a later #379 phase. Rewrote the test to run the REAL
seedTeamStats() path with a deterministic (mulberry32-seeded) realistic
game+practice mix and pin the honest picture: (1) the GAME-CONTEXT numbers
genuinely agree between the two layers (no drift in the underlying game
data — the actual Phase 0 reconciliation), and (2) Command Center's blended
average still legitimately diverges from Stats Center's game-only average
for a realistic player, asserted explicitly as an open Phase 0 gap rather
than masked by a cherry-picked fixture. Updated docs/baseball/
stats-architecture.md's Phase 0 status note and the stat-layer-manifest.ts
note to match.

Gates: npm run typecheck (clean), eslint --max-warnings 0 on all touched
files (clean), vitest run on both #379 test files + stat-layer-contract.test
(11 files / 25 tests passed), plus the full src/contracts/baseball suite
(52 files / 273 tests passed, no regressions) and the seed script's node
--test safety suite (3/3 passed, still upsert-only/no .delete()).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(baseball): add shared legacy-flat stat adapter module (#379)

Generalizes roster-aggregates-merge.ts's box-score-over-legacy merge into
src/lib/baseball/read-models/legacy-stat-adapters.ts — the one shared
precedence rule (box-score > legacy-fallback > no-data, permanent practice
carve-out, null-safe event-derived fields, sourceLayer provenance tag) that
every later stats-layer-reconciliation migration will build on.

roster-aggregates-merge.ts becomes a thin wrapper delegating to the new
module; its exported mergeSeasonStatsIntoAggregates signature and behavior
are unchanged so roster.ts and RosterClient.tsx need no changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): restore per-field legacy fallback for rate stats in roster merge

Review of #828 flagged that toLegacyAggregateShape() switched from a
per-field legacy fallback to taking the box-score row's rate fields
verbatim, dropping the pre-#379 `row.avg ?? existing?.career_avg ?? null`
behavior for career_avg/career_obp/career_slg/career_ops/game_avg/
season_avg. Any pure pitcher/DH gets a baseball_player_season_stats row
with g=0 and every rate field null (recalculate_baseball_season_stats
nulls them when v_ab/v_pa = 0), which silently blanked their real,
existing legacy figures to em-dash on the live Roster Wall.

Restore the per-field fallback for exactly those six fields, mirroring
the avg/max_pitch_velocity handling already in this file: box-score
truth (including a legitimate 0) still wins outright whenever present;
the legacy value is only read back in when the box-score-derived field
is null/undefined. Add a test case for the pure-pitcher/DH shape (legacy
row has real values, season-stats row has g=0 and null rates) plus a
companion case proving a genuine box-score 0 still wins over a nonzero
legacy figure, encoding the exact precedence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ess lonely 1-tab hub strips (#829)

Overnight mobile UI/UX audit — shared-shell group + primitive groundwork
requested by other in-flight fixer agents:

- PopoverPanel.Item: 36px -> 44px tap-target floor (min-h-11), app-wide.
- Modal (ui/modal.tsx): new opt-in `sheetOnMobile` prop — bottom sheet
  (rounded-t, drag bar, safe-area padding, dvh-safe max-height) below `md`,
  centered dialog unchanged at md+. Absent prop renders byte-identical.
- Fairway Sheet: new opt-in `mobileSide` prop — resolves to `mobileSide`
  below `md`, `side` at md+, so a desktop docked `side="right"` panel can
  become a bottom sheet on phone without every consumer hand-rolling its
  own useMediaQuery + ternary (the pattern FairwayNewMessageSheet/
  InsightPanel already duplicate by hand).
- Segmented: internal horizontal scroll + scroll-fade (useScrollFade) so a
  segment row wider than its container scrolls instead of silently
  clipping under the app's mobile overflow-x:clip guard. No-op when
  content already fits (fadeStyle is `{}`).
- BaseballHelm HubSubNav / resolveActiveHub: suppress the sub-nav strip
  entirely when a hub resolves to < 2 visible tabs (was only 0) — kills
  the pointless single-tab sticky row on /baseball/dashboard/my-stats and
  /academics. Updated the one test that asserted the old single-tab
  resolve behavior.
- FairwayHubSubNav (golf): 44px tap-target floor via an invisible vertical
  hit-slop pseudo-element, NOT a visible size change — golf's sub-nav row
  height (and LargeTitleContext's offset math) stays pixel-identical at
  every breakpoint.
- Cross-referencing doc comments at both z-index token definition sites
  (design-tokens.css --fw-z-*, tokens.css --z-*) flagging the two
  differently-numbered ladders that share tier names. Unification is
  intentionally NOT done here (see PR body follow-up).

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all touched files
clean; vitest run on nav-manifest.test.ts, resolve-active-hub.test.ts, and
every nav|hub-matching suite (60 files / 1556 tests) green.


Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…830)

* fix(mobile): shared-primitive tap-target/overflow/sheet fixes + suppress lonely 1-tab hub strips

Overnight mobile UI/UX audit — shared-shell group + primitive groundwork
requested by other in-flight fixer agents:

- PopoverPanel.Item: 36px -> 44px tap-target floor (min-h-11), app-wide.
- Modal (ui/modal.tsx): new opt-in `sheetOnMobile` prop — bottom sheet
  (rounded-t, drag bar, safe-area padding, dvh-safe max-height) below `md`,
  centered dialog unchanged at md+. Absent prop renders byte-identical.
- Fairway Sheet: new opt-in `mobileSide` prop — resolves to `mobileSide`
  below `md`, `side` at md+, so a desktop docked `side="right"` panel can
  become a bottom sheet on phone without every consumer hand-rolling its
  own useMediaQuery + ternary (the pattern FairwayNewMessageSheet/
  InsightPanel already duplicate by hand).
- Segmented: internal horizontal scroll + scroll-fade (useScrollFade) so a
  segment row wider than its container scrolls instead of silently
  clipping under the app's mobile overflow-x:clip guard. No-op when
  content already fits (fadeStyle is `{}`).
- BaseballHelm HubSubNav / resolveActiveHub: suppress the sub-nav strip
  entirely when a hub resolves to < 2 visible tabs (was only 0) — kills
  the pointless single-tab sticky row on /baseball/dashboard/my-stats and
  /academics. Updated the one test that asserted the old single-tab
  resolve behavior.
- FairwayHubSubNav (golf): 44px tap-target floor via an invisible vertical
  hit-slop pseudo-element, NOT a visible size change — golf's sub-nav row
  height (and LargeTitleContext's offset math) stays pixel-identical at
  every breakpoint.
- Cross-referencing doc comments at both z-index token definition sites
  (design-tokens.css --fw-z-*, tokens.css --z-*) flagging the two
  differently-numbered ladders that share tier names. Unification is
  intentionally NOT done here (see PR body follow-up).

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all touched files
clean; vitest run on nav-manifest.test.ts, resolve-active-hub.test.ts, and
every nav|hub-matching suite (60 files / 1556 tests) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): stats-surfaces mobile tap targets + games header wrap

Overnight mobile UI/UX audit — stats-surfaces group (3/3 findings fixed):

- StatsCenterClient: the season stepper, Batting/Pitching + Official/All
  side toggles (shared SegmentedControl), Clear-filters button, and every
  position chip explicitly override Button's 44px min-h floor with
  min-h-0. Ported the Fairway Button/Segmented coarse-pointer pattern
  ([@media(pointer:coarse)]:min-h-[44px]) so touch always clears 44px
  while the compact ~32px chip look is preserved for mouse/desktop.
- GamesList (+ its loading.tsx skeleton): header was an unconditional
  `flex items-center justify-between` packing the title against the
  season Select + Refresh + Add-Game cluster with no wrap, crowding the
  title into a ragged sliver on 320-390px phones. Changed to
  `flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`
  so the control cluster drops to its own row below the title on phone,
  matching the loading skeleton so there's no chrome drift.
- EditGameModal: opened as a centered desktop-style dialog on every
  viewport. Wired the new `sheetOnMobile` opt-in on the shared Modal
  (landed via the primitives PR, cherry-picked below) so it renders as
  a bottom sheet under `md` and an unchanged centered dialog at `md`+.

Deferred (not this group / requires an off-limits shared file):
- Select's outer wrapper (src/components/ui/select.tsx:160) hardcodes
  `w-full` on its own div rather than forwarding the caller's className,
  so the `w-28` passed from GamesList only constrains the inner trigger
  button. In practice the flex-shrink default already caps it to the
  remaining row width, and stacking the header (this commit) gives that
  row its own full-width line, so the visible symptom from the finding
  is resolved — but the underlying non-forwarding wrapper is a
  src/components/ui/** primitives-task file this task may not touch.
  Left as a follow-up for that task.

This commit includes a cherry-pick of primitives PR #829's head
(068899c) — not yet merged into batch/bbh-finish-0714 at task start —
for the `Modal.sheetOnMobile` prop EditGameModal depends on. That
commit will appear as a duplicate here until #829 merges and this
branch rebases/squash-merges past it.

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all 4 touched
files clean; vitest run on GamesList.record-summary.test.tsx +
product-trust.contract.test.ts (7 files / 30 tests) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(mobile): shared-primitive tap-target/overflow/sheet fixes + suppress lonely 1-tab hub strips

Overnight mobile UI/UX audit — shared-shell group + primitive groundwork
requested by other in-flight fixer agents:

- PopoverPanel.Item: 36px -> 44px tap-target floor (min-h-11), app-wide.
- Modal (ui/modal.tsx): new opt-in `sheetOnMobile` prop — bottom sheet
  (rounded-t, drag bar, safe-area padding, dvh-safe max-height) below `md`,
  centered dialog unchanged at md+. Absent prop renders byte-identical.
- Fairway Sheet: new opt-in `mobileSide` prop — resolves to `mobileSide`
  below `md`, `side` at md+, so a desktop docked `side="right"` panel can
  become a bottom sheet on phone without every consumer hand-rolling its
  own useMediaQuery + ternary (the pattern FairwayNewMessageSheet/
  InsightPanel already duplicate by hand).
- Segmented: internal horizontal scroll + scroll-fade (useScrollFade) so a
  segment row wider than its container scrolls instead of silently
  clipping under the app's mobile overflow-x:clip guard. No-op when
  content already fits (fadeStyle is `{}`).
- BaseballHelm HubSubNav / resolveActiveHub: suppress the sub-nav strip
  entirely when a hub resolves to < 2 visible tabs (was only 0) — kills
  the pointless single-tab sticky row on /baseball/dashboard/my-stats and
  /academics. Updated the one test that asserted the old single-tab
  resolve behavior.
- FairwayHubSubNav (golf): 44px tap-target floor via an invisible vertical
  hit-slop pseudo-element, NOT a visible size change — golf's sub-nav row
  height (and LargeTitleContext's offset math) stays pixel-identical at
  every breakpoint.
- Cross-referencing doc comments at both z-index token definition sites
  (design-tokens.css --fw-z-*, tokens.css --z-*) flagging the two
  differently-numbered ladders that share tier names. Unification is
  intentionally NOT done here (see PR body follow-up).

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all touched files
clean; vitest run on nav-manifest.test.ts, resolve-active-hub.test.ts, and
every nav|hub-matching suite (60 files / 1556 tests) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): mobile UI/UX pass — development group (6 findings)

Overnight mobile audit fixes for the "development" group (practice
planner, scrimmage builder, dev plans, compare/comparisons):

- practice/TimeRailBuilder + PracticePlannerClient (critical): the rail's
  drag handle (~14px) and resize strip (h-2) are far under 44px and the
  keyboard Arrow-key alternative can't be reached from a touchscreen (no
  focus-driven soft keyboard). Added Start/Duration +/-5min steppers to
  the Block-details card — the true tap-based equivalent, wired through
  the same updateBlock path the rail uses. RAIL_STEP/MIN_DURATION/fmtClock
  are now exported from TimeRailBuilder so both surfaces share one source
  of truth instead of a second copy that can drift.

- features/player-comparison.tsx (critical): the footer's "Comparing N
  players" label + Export PDF/Save Comparison buttons had no wrap/stack
  fallback; Card's `overflow-clip` silently hid whichever button lost the
  fight for space under ~390px. Footer now stacks (`flex-col` -> `sm:flex-row`)
  with a wrapping button row.

- practice-planner/ScrimmageLineupBuilder.tsx (major): field-position slot
  buttons and the bench/bullpen remove (X) button are the ONLY tap
  mechanism (native HTML5 drag doesn't fire on touch) but both overrode
  Button's 44px floor with `min-h-0`. Dropped the override on the slot
  button; grew the remove button to a real 44x44 box rather than an
  invisible hit-slop pseudo-element, since list rows sit only 4px apart
  (`space-y-1`) and an oversized hit-slop would steal taps from the
  neighboring row's remove button.

- coach/CreateDevPlanModal.tsx (major): hand-rolled centered dialog with
  static `vh` sizing and a hard `grid-cols-2` roster picker — the Save/
  Cancel footer could end up hidden behind the on-screen keyboard, and
  player cells crammed avatar+name+position into ~120px. Switched to
  bottom-anchored-on-phone + `dvh`-safe max-height (centered dialog
  unchanged at `sm:`+), `grid-cols-1 sm:grid-cols-2` picker, and safe-area
  footer padding. Kept the hand-rolled shell rather than re-platforming
  onto the shared Modal's `sheetOnMobile` (can't visually verify a larger
  rewrite without a browser in this environment) — noted in the PR body.

- dashboard/compare/CompareClient.tsx (major): the "Add Players" search
  box and the selected-player chip row sat side-by-side with no stacking
  breakpoint and no `min-w-0`, so search became unusable as soon as a 3rd
  player was selected. Stacks below `sm`; search wrapper can now actually
  shrink.

- features/saved-comparisons-list.tsx (minor): the one un-migrated sibling
  of dev-plans/compare/practice — plain `Card` + hand-rolled empty state
  instead of the Living Annual kit. Ported onto `PaperCard` +
  `<EmptyIssue variant="generic" ink="pursuit" />` (matching
  PracticePlannerClient's usage) and swapped `warm-*` tones for
  `text-text-*`/`--hairline`/`grade-plus` ink tokens.

Cherry-picked primitives PR #829 (068899c) onto this branch since it
wasn't yet merged into batch/bbh-finish-0714 — the duplicate commit will
disappear on squash-merge.

Gates: npm run typecheck (clean), npx eslint --max-warnings 0 on all 7
touched files (clean), npx vitest run on CreateDevPlanModal.test.tsx +
dev-plans-coach-gating.test.ts + practice-validation.test.ts (92/92
passed, 12 test files via shared setup).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#837)

- SignalDrillDown: opt into the Sheet primitive's new `mobileSide="bottom"`
  (PR #829) so the phone drill-down renders as a native bottom sheet instead
  of the desktop docked `side="right"` panel — matching Doctrine rule 4
  already applied to FairwayNewMessageSheet/InsightPanel. No hand-rolled
  useMediaQuery needed; the primitive resolves it.
- command-center/loading.tsx: KPI skeleton now emits StatStrip's real
  GRID_BASE[2] + GRID_COLS[4] classes byte-for-byte (2x2 grid below `sm`)
  instead of a single stacked column, so the loading -> loaded transition no
  longer visibly reflows on phone.

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… content (#840)

- Sticky save bars in ProgramSettingsClient and recruiting-preferences
  used a bare `bottom-4`, which sat ~16px above the viewport edge --
  squarely inside FairwayBottomNav's opaque 56px+safe-area footprint, so
  the only save affordance on these settings forms was invisible/
  untappable on mobile. Both now offset by
  `--golf-mobile-bottom-nav-offset` (zeroes at md), the same idiom the
  Messages sticky footer uses.
- MinimumStandards' enabled/disabled toggle was `<Button variant="primary"
  className="w-6 h-6 ...">` with no `size`, so Button's default md
  `min-h-[44px]` (a different twMerge conflict group than h-6/w-6) stayed
  applied, forcing a 44px-tall/24px-wide sliver with the checkmark
  clipped. Swapped to a plain native button (Button's ripple effect needs
  `overflow-hidden`, which would clip the 44px hit-slop pseudo-element)
  sized correctly at 24x24 with an invisible `before:-inset-3` hit-slop
  restoring the 44px tap-target floor without growing the visible box.
- RecruitingWeightDistributor's per-metric description was gated entirely
  behind onMouseEnter/onMouseLeave -- unreachable on touch. Now always
  rendered.
- ProgramSettingsClient's three brand color swatches were h-10 (40px);
  bumped to h-11 (44px).
- recruiting-preferences' 50-state picker was one flat wall of pills;
  grouped into US Census 4-region clusters so it reads as composed
  content on a phone instead of a dumped tag-cloud.

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…p, green pills, urgency wrap (#831)

* fix(baseball): mobile messages-hub audit — skeleton drift, subnav clip, green pills, urgency wrap

Overnight mobile UI/UX audit, messages-hub group (6 findings):

- messages/loading.tsx (critical): replaced the generic, non-responsive
  `<SkeletonMessages />` (hardcoded w-80 + flex-1 desktop shape, overflows
  a 320px phone by ~48px, mismatched chrome) with `<MessagesFairway loading
  .../>` — the exact fallback MessagesClient's own <Suspense> already uses,
  so there is exactly one skeleton shape for this route instead of two that
  can drift out of sync.
- HubSubNav height accounting (critical): MessagesFairway.tsx and
  ConversationClient.tsx already carry the `--baseball-hub-subnav-offset`
  fix (#481/#817, pre-existing on this branch) that reserves the coach
  "Messages · Announcements" sub-nav strip's real height so the composer
  never clips behind the bottom tab bar. Closed the one remaining gap:
  ConversationClient's composer form was missing the
  `pb-[calc(1rem+env(safe-area-inset-bottom))] lg:pb-4` safe-area padding
  MessagesClient's own composer already has — added it to match.
- NewMessageModal (major): search-result rows used `<Button variant=
  "primary">`, which unconditionally sets `bg-primary-600 text-white` —
  every unselected contact rendered as a solid green pill. Switched to
  `variant="ghost"` so no unconditional accent background survives; the
  existing hover/selected classNames are untouched.
- UrgencyPicker (major): `grid-cols-4` with no responsive override crammed
  a dot + label + description into ~57px cells even at 430px, wrapping
  character-by-character. Dropped to `grid-cols-2 sm:grid-cols-4` so each
  phone cell has room to breathe.
- Compose flows (minor): added `sheetOnMobile` (new opt-in prop from the
  primitives PR, cherry-picked below) to both NewMessageModal's and
  CreateAnnouncementFlow's `<Modal>` so they render as bottom sheets below
  `md` instead of centered shrunk-desktop dialogs; this also gets the
  dvh-safe max-height for free (sheetOnMobile's branch already uses
  `100dvh`, unlike the default centered-dialog branch).
- ConversationClient vs MessagesClient dual-design (minor) — DEFERRED, see
  PR body: fixing this correctly (retire `/messages/[id]` and redirect
  deep links to `?conversation=`) would require touching route-manifest/
  test-registry files outside this finding's listed files (nav-manifest.
  test.ts, baseball-route-inventory.ts) and deleting a route with its own
  deliberate, recently-#481-fixed height-parity work and dedicated loading
  skeleton — disproportionate risk for a minor finding within this PR's
  scope.

Cherry-picked PR #829 (shared primitives, still open at time of this run)
onto this branch for `Modal`'s `sheetOnMobile` prop — see that commit's own
message for its full scope (PopoverPanel 44px floor, Sheet mobileSide,
Segmented overflow-scroll, HubSubNav <2-tab suppression). Duplicate commit
disappears on squash-merge once #829 lands on the base branch first.

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all 5 touched files
clean; vitest run on nav-manifest.test.ts, resolve-active-hub.test.ts, and
use-messages.test.tsx (12 files / 640 tests) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): composer sm-breakpoint safe-area padding gets reset by py-5 shorthand

Review found the sm:640-1023px range dropped the safe-area bottom padding
added in the prior commit: sm:py-5's own padding-bottom sub-declaration
(1.25rem flat) wins over the base pb-[calc(1rem+env(safe-area-inset-bottom))]
utility for any viewport >= 640px, since sm:'s media-query block is emitted
after the unprefixed base rules. lg:pb-4 already masked this for desktop,
so the flattened padding was only visible in the sm-to-lg gap (phone
landscape / small tablet).

Add sm:pb-[calc(1.25rem+env(safe-area-inset-bottom))] alongside the existing
sm:px-6 sm:py-5 so the safe-area term survives through that range too,
keeping lg:pb-4 as the final flat desktop value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ets, honest roster skeleton (#832)

* fix(baseball): mobile team-ops — touch-visible doc actions, 44px targets, honest roster skeleton

Group: team-ops (mobile audit). Fixes all 8 findings (1 critical, 3 major,
4 minor):

- DocumentCard.tsx (critical): kebab "More options" trigger + footer
  Preview/Download row were opacity-0 group-hover only — permanently
  invisible on touch. Now touch-visible by default, hover-gated only at
  md+; footer Download link gets an invisible hit-slop pseudo-element
  (IconButton's overflow-hidden ripple layer rules out hit-slop there, so
  the Preview IconButton instead drops its w-auto/h-auto override back to
  the shared 44px default).
- ScrimmageLineupBuilder.tsx (major): removed min-h-0 overrides on the
  Team Blue/White toggle and roster-row select buttons (both restore the
  Button primitive's 44px floor); bench/bullpen remove (X) button gets an
  explicit min-h-11/min-w-11 since it was p-0 with no other floor.
- PracticeEffectivenessClient.tsx (major): removed min-h-0 from the
  All/Measured/Attention/Need-data filter tabs, restoring the 44px floor
  without the horizontal padding change (kept the simpler "or" branch of
  the fixSketch over swapping to Segmented — lower risk, same outcome).
- roster/loading.tsx (major): added a `md:hidden` stacked skeleton
  (avatar/name + 2 stat pills + row-menu box, no min-width/overflow-x)
  mirroring RosterFairway's real RosterWall mobile branch; existing
  680px table skeleton now `hidden md:block`, so there's no more
  desktop-shaped skeleton flashing on a phone before data loads.
- documents-client.tsx + TravelClient.tsx (minor): swapped both
  window.confirm(...) delete flows for the shared ConfirmDialog
  (danger variant, native bottom-sheet on-device), matching
  RosterMemberActions/SavedLineupsPanel elsewhere in the hub.
- TravelClient.tsx (minor): handleSaved now calls router.refresh()
  instead of window.location.reload(). Fix-sketch deviation: TravelClient
  copies its itineraries prop into local useState, which does NOT
  re-seed on a bare router.refresh() (useState's initializer only runs
  once) — added a useEffect that re-syncs local state whenever the
  server-refreshed prop changes, so the created/edited/deleted trip
  actually appears instead of silently going stale.
- PopoverPanel.Item 44px-floor finding: already resolved by the
  primitives PR (#829, cherry-picked below) — min-h-11 confirmed in
  PopoverPanel.tsx; no additional edit needed in RosterMemberActions.tsx.
- PracticeRecapPanel.tsx (minor): scrimmage scoring grid is now
  `grid-cols-1 sm:grid-cols-3` so Blue/White/Innings stack on a 320px
  phone instead of squeezing into ~80px columns.

Cherry-picked primitives PR #829 (head 068899c) onto this branch first —
tap-target floors, Modal sheetOnMobile, Sheet responsive side, Segmented
overflow. That commit will disappear as a duplicate on squash-merge of
this PR against the primitives PR's own merge.

Gates: npm run typecheck (clean) · npx eslint --max-warnings 0 on all 7
touched files (clean) · npx vitest run PracticeRecapPanel.test.tsx (4/4
passed, only touched-file test importer found).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(baseball): PR #832 review — wire real Travel refetch, fix Download hit-slop overlap

Two mustFix items from mobile review verdict:

- TravelClient.tsx: handleSaved() called router.refresh(), a no-op since
  TravelPageClient fetches itineraries entirely client-side. Lifted a lean
  reloadItineraries() callback (getTeamItineraries only — deliberately not
  the heavier detectRoleAndLoad, which would flash <PageLoading /> over the
  whole page) from TravelPageClient down as onReload, and handleSaved now
  calls it directly. The existing local-state resync effect is no longer
  dead: it's what reflects the refetched prop into TravelClient's mirrored
  state once onReload() resolves.

- DocumentCard.tsx: the footer Download link's before:-inset-2.5 hit-slop
  (needed to reach the 44px floor on a ~26px visible box) expanded 10px
  left, but gap-1 between it and the Preview IconButton was only 4px —
  6px of invisible hit area bled onto Preview's box and could steal taps
  meant for it. Widened the footer row to gap-3 (12px), clearing the 10px
  expansion with margin to spare; left the already-correct -inset-2.5
  alone since shrinking it would drop Download's own target under 44px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…r sheet, camp roster, map default (#833)

* fix(baseball): mobile recruiting hub — peek panel/compare bar off-screen, filter drawer, camp roster row, map default

Overnight mobile UI/UX audit — recruiting group (Discover/Pipeline/
Watchlist/Camps). Fixes every critical + major finding, plus one minor
that lived in an already-touched file.

- PeekPanelRoot (Pipeline/Discover/Watchlist player+team peek): was a
  hard-coded 512px right-edge panel with no responsive override — most of
  it rendered off the left edge of any phone. Now a bottom sheet below
  `sm`, right-edge dock at `sm:` and up.
- CompareBar (Discover): unconstrained fixed-center row overflowed the
  viewport with no way to reach Compare/Clear, and sat inside the mobile
  bottom-nav's band. Now constrained + horizontally scrollable, and
  raised above the bottom nav.
- DiscoverClient: replaced the hand-rolled left-edge drawer (no focus
  trap/Escape/scrim-click) with the app's own vaul Sheet — the codebase's
  own documented "no left drawer" rule.
- FilterPanel: added a `sticky` opt-out (default true) so the mobile
  sheet instance doesn't fight the sheet's own header for the scroll
  container's top edge.
- Discover Map default: Map is now the untouched default only at `md:`+;
  below `md` a first-time/returning visit lands on List (a full US
  choropleth shrinks small states below any tap target on phone). Map
  stays one tap away — picking it from ViewToggle this session still
  works. Also reduced the map card's own padding on phone.
- PlayerCard (Discover grid): quick actions (watchlist/message) were
  hover-only with zero touch fallback — added the same
  `[@media(pointer:coarse)]` + `group-focus-within` treatment
  `<HoverReveal>` applies elsewhere (kept as direct classes since this
  card's `group` is shared by other children — see inline comment).
- DiscoverView pagination pills: `size="icon-sm"` instead of a bare
  `w-8 h-8` fighting the default Button padding, clearing 44px.
- CampDetailClient: roster row now stacks identity above the
  badge/Check-In row below `sm` (was one non-wrapping line where
  Check In was the first thing squeezed off); RosterFilterControl's
  `min-h-0` override dropped so it clears 44px.
- CreateCampModal: rebuilt on the shared `Modal` (`sheetOnMobile`) —
  focus trap, Escape, scrim-click, dvh-safe height, safe-area footer
  padding, all for free instead of re-derived by hand.

Cherry-picked PR #829 (shared-primitive groundwork: Modal `sheetOnMobile`,
Sheet `mobileSide`, PopoverPanel.Item 44px floor, Segmented overflow
scroll) as a prerequisite — not yet merged into batch/bbh-finish-0714 at
task start. That commit disappears from this branch's history on
squash-merge of #829.

Deferred (minor, live in files not otherwise touched this pass):
- CampsClient.tsx: coach-actions row `flex-wrap` at 320px.
- PipelineClient.tsx/WatchlistClient.tsx: commit-ceremony toast overlaps
  HubSubNav.
- ScoutPacketManager.tsx: share-link icon buttons under 44px.

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all 8 touched
files clean; vitest run on discover/camp/peek-panel action tests (16
files, 68 tests) — all passing (no dedicated unit tests import these
UI components directly).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(baseball): mobile review follow-up — pagination overflow + sheet overlay teardown

Two mustFix items from PR #833 mobile review:

- DiscoverView pagination row: clamp+scroll it exactly like CompareBar's
  own mobile fix in this same PR (max-w-[calc(100vw-2rem)] + overflow-x-auto
  + shrink-0 on Previous/pills-wrapper/Next), so the icon-sm 44px pill fix
  can't be undone by flex-shrink at narrow widths, and any overflow stays
  reachable instead of being clipped by the app's global mobile
  `overflow-x: clip` guard on html/body.

- DiscoverClient mobile filter Sheet: stop gating visibility with
  className="lg:hidden" (only hides Drawer.Content — vaul's Drawer.Overlay
  is wired to `open` alone and ignores className, so a sheet left open
  while crossing lg+ left an invisible full-screen backdrop eating clicks).
  Force-close reactively via useMediaQuery + useEffect instead, mirroring
  the isDesktop pattern already used in the sibling DiscoverView.tsx, so
  the sheet and its overlay tear down together at the breakpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…m/program/group split panes (#835)

* fix(mobile): repoint Lift Lab session nav, clear bottom-nav-covered CTAs, phone-ify Lift Lab split panes

performance-lift group — mobile UI/UX overnight pass (7 criticals, 3 majors, 1 minor).

Criticals:
- PlayerLiftSessionClient had no basePath prop (unlike its sibling
  PlayerLiftHomeClient) and hardcoded three '/lifting/dashboard/...' hrefs
  (BackLink, readiness-gate, post-completion). A BaseballHelm player tapping
  any of them on /baseball/dashboard/lift/[sessionId] was routed into the
  sibling Lifting Lab product. Added `basePath`, threaded from both pages
  ('/baseball/dashboard' and '/lifting/dashboard'), mirroring
  PlayerLiftHomeClient's contract exactly. Added a basePath link-wiring
  regression test (mirrors PlayerLiftHomeClient.test.tsx).
- Same file: sticky "Complete lift" bar had no bottom offset / z-index,
  rendering under FairwayBottomNav. Offset via the existing
  --golf-mobile-bottom-nav-offset var, z-[var(--fw-z-sticky)].
- Same file: Card's own default padding + CardContent's own padding
  double-stacked (call-site fix only, no ui/card.tsx edit: padding="none"
  on the outer Card + explicit p-4/md:p-6). Rebuilt the per-set row (label +
  Log button on one line, reps/load/RPE as a 3-col grid) so it no longer
  needs a single unwrapped ~300px+ row; removed the min-h-0 tap-target
  overrides on the three inputs so they fall back to Input's 48px floor.
- LiveWeightRoomClient: swapped the permanent w-80 athlete-detail `<aside>`
  for the shared Fairway `Sheet` (side="right" mobileSide="bottom") so it
  no longer competes with the athlete grid for width below 430px; swapped
  `h-screen` for a shell-aware `--golf-mobile-header-offset` /
  `--golf-mobile-bottom-nav-offset` height calc (same idiom as #481's
  ConversationClient fix) instead of assuming it owns the full viewport.
- ProgramEditorClient / StrengthGroupsClient: both were hardcoded desktop
  split panes (permanent w-72/w-64 rail next to the primary content, no
  breakpoint). Added the same mobile tab-switcher LiftCanvas already uses
  one level up in this vertical (segmented Weeks&Days/Day-editor and
  Groups/Members tabs, `lg:` breakpoint to match LiftCanvas); wrapped the
  now-full-width prescriptions/members tables in overflow-x-auto so the
  pane's own content doesn't just overflow instead. Skeletons updated to
  match.
- LiftBuilderClient: sticky Save bar had z-10 (below the nav's z-20) and a
  bare safe-area offset that didn't clear the nav's 56px height. Same
  --golf-mobile-bottom-nav-offset + z-[var(--fw-z-sticky)] fix.

Majors:
- PerformanceCommandCenter: dropped the min-w-[560px] + overflow-x-auto
  wrapper around WeightRoomRow — that row already stacks itself
  (flex-col ... sm:flex-row); the wrapper was forcing needless horizontal
  scroll on every phone for a problem the row's own layout had solved.
- LiftCanvas: block reorder controls were `absolute -left-8` unconditionally,
  pushed off-screen below the 16px mobile gutter. Kept that treatment
  `lg:flex`-only; SessionBlock now renders its own inline up/down chevrons
  in the block header, `lg:hidden`.
- ProgramListClient: header actions row (w-60 search + New program CTA) had
  no wrap; input now shrinks via flex-1/min-w-0 with the CTA pinned via
  shrink-0, flex-wrap as the last-resort fallback.

Minor (fixed — cheap given the shipped primitives):
- ExerciseWizard: added `sheetOnMobile` to its Modal call so the 7-step
  wizard is a bottom sheet below md instead of a shrunk centered dialog —
  the primitives PR built this exact prop for this exact case.

Deferred: the LiftBuilderClient/LiftCanvas DetailPane Living-Annual re-skin
(minor) — a visual redesign task carries real risk without browser
verification (NO browsers/Playwright rail), so left for a follow-up with
visual review rather than rushed here.

Cherry-picked primitives PR #829 (068899c) onto this branch since it wasn't
merged into batch/bbh-finish-0714 yet — that commit disappears on
squash-merge of #829.

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all 13 touched files
clean; vitest run on the new/touched test + every test importing a touched
component (20 files / 96 tests) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(mobile): review r2 — vaul-safe Sheet mount, restore desktop docked aside, subnav-aware height

Addresses mustFix findings from the mobile-review-verdicts pass on #835:

- LiveWeightRoomClient.tsx: the athlete-detail Sheet is now mounted
  unconditionally below `md` (visibility driven by `open`, body content
  gated on `selectedAthlete` inside `Sheet.Body`) so vaul plays its
  close/exit transition instead of the panel vanishing instantly —
  mirrors FairwayEventDetailDrawer.tsx's established pattern. At `md`+,
  restored the pre-existing non-modal docked `<aside>` instead of the
  Sheet (which is always a vaul `modal` Drawer.Root regardless of
  side/mobileSide) so a coach on desktop/tablet can still click a
  different athlete card without first closing the current detail panel.

- LiveWeightRoomClient.tsx: SHELL_AWARE_HEIGHT now also subtracts
  `var(--baseball-hub-subnav-offset,0px)` as a third term, matching
  ConversationClient.tsx's established idiom, so the component under-fits
  correctly on /baseball/dashboard/performance/live (HubSubNav mounted)
  as well as its /lifting/dashboard/sessions/live sibling.

- LabShell.tsx: scopes `--golf-mobile-bottom-nav-offset: 0px` for its own
  subtree (Lifting Lab never renders FairwayBottomNav), so
  PlayerLiftSessionClient's sticky "Complete lift" bar no longer floats
  above a phantom gap on /lifting/dashboard/lift/[sessionId] while
  BaseballHelm's route (BaseballFairwayShell, which DOES mount the nav)
  keeps the real non-zero offset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…list-row fill (#836)

* fix(mobile): import-center + stats-upload tap targets, chrome drift, list-row fill

Overnight mobile UI/UX audit — import-center group.

- StatsUploadClient/loading.tsx: drop the redundant min-h-dvh on the outer
  wrapper (AppShell's own <main className="flex-1"> already provides full
  available height); double-claiming 100dvh on top of the shell's own sticky
  chrome left 150px+ of dead scroll space below every wizard step.
- StatsUploadClient: Match Players suggestion rows were <Button variant=
  "primary"> with only a hover override, so every unselected row rendered as
  a solid brand-green fill at rest. Swapped to variant="ghost" so the row's
  own border/hover classes are the only styling, matching the sibling toggle
  button one level up which already uses ghost correctly.
- StatsUploadClient: Session Type toggle row had no flex-wrap, so the three
  labels were at real risk of spilling past the card edge at 320px. Added
  flex-wrap so it degrades gracefully instead of depending on exact fit.
- ImportWizardClient: "change data shape" icon-only control was a hand-rolled
  rounded-full p-1 button around an 11x11 svg (~19x19px hit box). Routed
  through the shared fairway IconButton (variant="ghost" size="sm"), which
  already expands 36->44px under pointer:coarse — same primitive the rest of
  the kit uses for icon-only affordances.
- ImportWizardClient: two secondary disclosure controls in the Preview/
  Validate step (IssueGroup's blocking/warning/info header row, and
  DuplicateSummary's "Show what changes" text toggle) were under the 44px
  touch floor (~38px and ~18px respectively). Both are intentionally raw
  <button>s (documented reasons: the header row must not let <Button>'s
  variant fight the severity InkBadge; the text toggle must not take on
  <Button>'s padding/variant inside a dense summary row) — kept that, just
  added min-h-11 (+ inline-flex items-center on the text toggle) so the tap
  target clears 44px without changing either control's visual footprint.

Deferred (explicit product decision, not a code fix): the /stats/upload vs
/baseball/dashboard/import dual-wizard finding — Nick's call whether to
retire /stats/upload in favor of Import Center or re-skin it onto the same
Living Annual primitives. Left both wizards in place per task instructions.

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all 3 touched files
clean; no existing vitest suite imports StatsUploadClient/ImportWizardClient
(confirmed via targeted vitest run — "No test files found"), so no test gate
applicable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile): gate import-center touch-target growth to coarse pointers

Review found two of the #836 44px-floor fixes were applying unconditionally
instead of only under `[@media(pointer:coarse)]:`, inflating desktop/tablet
layout that shares a flex line with other content:

- DuplicateSummary "Show what changes" toggle: min-h-11 was unconditional,
  resetting the row height even on mouse pointers where it shares a line
  with the "N new / N overwrite / N duplicate" summary spans. Now
  `[@media(pointer:coarse)]:min-h-11`.
- "Change data shape" control: swapping in shared IconButton grew it to an
  unconditional 36px (size="sm") beside an ~18px InkBadge stamp on
  desktop/tablet — a visual change the PR body never called out. Reverted
  to the original raw pressableClass button, with touch-target growth to
  44px confined to `[@media(pointer:coarse)]:h-11/w-11` only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ces (#838)

* fix(baseball): mobile-compose public team/program/player/packet surfaces

Mobile UI/UX pass over the public (unauthenticated) share-link surfaces —
team profile, program profile, player passport, scout packet.

- team/[id] + program/[id]: header row (logo + Masthead/name) now stacks
  flex-col below sm instead of squeezing a 40-48px logo + text-5xl/text-3xl
  title into an ~88-158px column at 320-430px, mirroring the existing
  PlayerProfileClient flex-col md:flex-row hero pattern. Fixed in both the
  live page and its loading.tsx skeleton so mount doesn't jump layout.

- player/[id]: PR #818 already collapsed the coach-viewing "Message" button
  into an overflow menu below md, but the "Add to Watchlist" pill was left
  full-label, so it still overlapped the absolute back button by ~28px at
  320px. It now drops to an icon-only 44px control (aria-labelled) below md,
  matching the Message/overflow-menu breakpoint already in this file.

- ScoutPacketCsvButton: dropped a stray `min-h-0` that was winning the
  tailwind-merge conflict against the Button component's own
  `min-h-[44px]` floor, collapsing the packet's one download control to
  ~32px tall.

- OverviewTab "Physical & Metrics" grid: matched the single-column-on-mobile
  treatment StatsTab already uses for the same velocity/time fields, instead
  of a 2-col layout that risks numeral/unit collision at 320-430px.

- "Schools of Interest" rows wore pressableClass (hover/press/focus-ring
  affordance) on a plain non-interactive div. They now link out to the
  school's program profile, same as the identity-chip links elsewhere on
  this page; a row with no organization record stays a plain div.

- All four public-route error.tsx boundaries pointed "Go Home" at
  /baseball/dashboard, an authenticated route that immediately bounces
  anonymous share-link visitors to /login. Repointed to /baseball, which
  role-routes signed-in users and sends everyone else to login directly.

Deferred (not in this PR):
- "/baseball marketing root" finding — redirect-only landing is flagged as
  a product decision for Nick, per task instructions; left as-is.
- ProgramRoster.tsx inline pitch/exit-velo stat blocks (minor, /baseball/
  program/[id]) — lives in a file this PR doesn't otherwise touch; deferred
  per the "fix minors only in files already being edited" rule.

Gates: npm run typecheck (clean), npx eslint --max-warnings 0 on all 10
touched files (clean), npx vitest run on the two source-scanning test
suites that read team/[id] and program/[id] page.tsx (104/104 passed). No
other test file imports PlayerProfileClient or ScoutPacketCsvButton.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(baseball): fill info-column width in mobile header flex-col mode

Reviewer round 2 on PR #838: the team/program header info column used
`min-w-0 flex-1` alone, which shrink-wraps to content width once the
header switched to `flex-col` below `sm`. Both loading.tsx skeletons
already pair `w-full` with `flex-1` for this exact column, so the live
pages now match — `w-full min-w-0 flex-1`. No row-mode (sm+) regression:
flex-basis:0% from flex-1 already wins over width there, same as the
skeleton has always done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ding, skeleton drift (#839)

* fix(baseball): mobile player-experience tap-targets + skeleton drift (player-experience findings)

- PlayerPassportFairway: batting stat grid base grid-cols-3 -> grid-cols-2
  (matches the sibling Pitching block) so 2-3 digit tabular-nums numerals
  at the 40px clamp floor don't crowd/collide at 320-390px.
- PlayerTimelineFairway: AckChip's Acknowledge/Acknowledged toggle gets an
  invisible before-pseudo-element hit-slop expanding its tap target to
  44px without enlarging the visible chip (same technique as
  ModalShell/Sheet's close buttons).
- PassportVisibilityControls: FieldRow's 4-option raw-button segmented
  control replaced with the shared Segmented primitive (44px under
  pointer:coarse, built-in overflow handling); its surface/border tokens
  inherit the Living Annual --paper/--hairline cream via the ambient
  .living-annual scope, so this also folds the panel's legacy
  cream-50/warm-200 chrome (exposure tiles, read-only banner, field-group
  container, footer divider) onto --paper-canvas/--hairline to match the
  rest of the passport page.
- player/timeline/loading.tsx: skeleton rebuilt on
  fairwayScope('min-h-full bg-canvas') + --paper/--hairline PaperCard-
  shaped event rows, replacing the legacy bg-cream-100/bg-warm-100
  skeleton that popped to the cooler canvas on load (same fix already
  applied to passport/today/practice loading.tsx).

Gates: npm run typecheck (clean), npx eslint --max-warnings 0 on all 4
touched files (clean), npx vitest run src/components/baseball (40 files /
248 tests passed) — no test directly imports these presentation
components, so the full baseball component suite stood in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(baseball): AckChip hit-slop no longer crosses into title/time row

Review on #839 flagged the symmetric before:-inset-y-3.5 hit-slop as
crossing 4px into the title/time row above (10px mt-2.5 clearance -
14px top expansion). Made the inset asymmetric: before:-top-2 (8px,
leaves 2px clear) / before:-bottom-3.5 (14px, unchanged, no neighbor
below), keeping before:-inset-x-1.5 as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile): shrink AckChip top hit-slop to 6px — clears wrapped badge-line neighbor too

Verifier-derived: before:-top-2 (8px) overshoots the 6px flex row-gap when
the badge row wraps at 320-390px and AckChip lands on its own line; 6px
clears both the 10px title-row case and the wrapped case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… fixes (#841)

* fix(mobile): onboarding-auth chrome-drift skeletons + tap-target/grid fixes

Fixes 5 major + 1 minor mobile findings in the onboarding-auth group
(chrome that never got the same bespoke-skeleton/premium treatment as its
login/signup siblings):

- forgot-password, reset-password, complete-signup: loading.tsx now mirrors
  login/loading.tsx's narrow centered baseball-auth-field card (sized to
  each page's own form) instead of the generic dashboard-card PageLoading
  skeleton.
- coach-onboarding: loading.tsx now mirrors the wizard's own EntryField +
  masthead + EditorialFrame double-bezel shell instead of PageLoading.
- player onboarding: extracted PlayerOnboardingSkeleton (EntryField +
  HelmMark + a plain pulse card) reused as BOTH loading.tsx AND page.tsx's
  authLoading gate, collapsing three stacked, unrelated skeleton languages
  into one.
- baseball/join/[code]: added a loading.tsx (previously none existed)
  mirroring the sibling staff/join/[code] flow's PaperCard skeleton — this
  page runs strictly more sequential Supabase awaits before its first
  paint, reached almost entirely via SMS/cellular invite links.
- player onboarding "about"/"measurables" steps: NativeSelect 3-up grids
  (Bats/Throws/2nd Pos., Height ft/in/Weight) now stack to 2-up below `sm:`
  so each trigger keeps enough width for its option text to clear the
  chevron; select.tsx itself is off-limits tonight (primitives-owned).
- player onboarding progress bar: swapped the shared, unconstrained
  StepIndicator dot-and-connector `<nav>` (no wrap/overflow guard, can push
  past 320px) for a new local StepProgress — a numbered-eyebrow + hairline
  rule, the same treatment coach-onboarding already uses. Kept as its own
  local copy (not imported from coach-onboarding's private _components
  folder) since that file documents itself as bespoke/non-shared.
- login: "Forgot password?" link now has a 44px tap target via real
  padding (not a hit-slop pseudo-element) — it sits directly above the
  password Input with only a 6px gap, and an invisible ::before sized to
  44px would have overlapped the Input's own tap area.

Deviates from the literal fixSketch in two spots, both explained in the PR
body: the tap-target technique (padding vs. hit-slop, to avoid stealing
taps from the adjacent Input) and the StepIndicator swap (a fresh local
StepProgress copy vs. importing coach-onboarding's private one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile): coach-onboarding loading.tsx matches 'type' step, not later chrome

Review found the skeleton always rendered later-step-only chrome (a numbered
progress rule + EditorialFrame double-bezel panel) even though the wizard's
guaranteed first-rendered step ('type', gated by `step !== 'type'` in
page.tsx) shows neither. Replaced the outer-bezel + 4 h-12 bars stand-in with
an un-bezeled stack of 4 individually-bordered, OptionCard-shaped rows
(rounded-2xl border p-5, icon swatch + two text lines) matching the real
'type' step's flat COACH_TYPES list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
njrini99-code and others added 5 commits July 15, 2026 06:02
…red legacy-stat adapter (#846)

* feat(baseball): migrate Command Center game-context onto the #379 shared legacy-stat adapter

Chunk "Migrate command-center.ts" of the #379 stats-layer reconciliation.
rosterPulse[].careerAvg/totalSessions now resolve via
legacy-stat-adapters.ts's adaptLegacyStatsMap: getStatsCenter()'s
box-score-derived battingAll figures win whenever a player has box-score-era
data this season, the raw legacy aggregate row is only a fallback tier (and
the passthrough source for the still-unreplaced recentTrend field), and a
player with neither is honestly no-data. This closes the
Command-Center-vs-Stats-Center display divergence #827 documented for
realistically-seeded players: both surfaces now show the identical
game-context number by construction.

- command-center.ts: fetch getStatsCenter() concurrently, reshape the legacy
  aggregate rows into the adapter's LegacyAggregateRow contract (missing
  columns explicit null, never fabricated), feed non-noData Stats Center rows
  in as BoxScoreGameContextRow, resolve the pulse via the adapter.
- command-center-stats-center-drift.test.ts: the "KNOWN OPEN GAP" pin flips
  to RECONCILED — same real seedTeamStats() path, now asserting the displayed
  numbers agree while proving the raw legacy blend still differs (so the
  reconciliation is the adapter's doing, not a degenerate fixture).
- command-center.test.ts: three new integration pins (box-score precedence,
  legacy-fallback no-regression, honest no-data) + stats-center table stubs
  for the existing timezone suite.
- stat-layer-manifest.ts: command-center notes updated; entry retained
  because the adapter's legacy-fallback tier still requires a raw
  baseball_player_aggregates fetch here (documented in the note).

Gates: npm run typecheck clean; eslint --max-warnings 0 clean on all four
files; vitest 154/154 across the affected suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): stop pitching-only box-score rows from masking legacy career_avg in Command Center

Command Center's boxScoreRows loop only skipped statsCenterModel rows where
noData was true. A player with real box-score data for a non-batting side
this season (pitching/catching/fielding/baserunning-only) has noData:false
but battingAll.g === 0 / battingAll.avg === null — feeding that row into the
#379 adapter as-is flipped sourceLayer to 'box-score' with a null avg,
silently nulling out a real legacy career_avg fallback instead of falling
through to it. Now skips on the games-count (battingAll.g === 0), not the
derived rate, so a legitimate AB=0/g>0 batting row (e.g. walk-only game)
still wins per the adapter's precedence.

Also surfaces the adapter's sourceLayer on RosterPulseItem instead of
discarding it, matching legacy-stat-adapters.ts's stated contract.

Adds a regression test pinning a pitching-only-this-season + real legacy
career_avg player to the legacy-fallback average, not a masked null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
)

* ci: promote BaseballHelm authed smoke to a required PR gate (#372)

Adds a baseball-auth-smoke job to ci.yml that runs the already-built
e2e/baseball-smoke.spec.ts + e2e/baseball-onboarding-smoke.spec.ts (with
their existing fail-loud auth setup) as a required PR gate, folded into
the `all` aggregate — the same promotion mechanism used for Supabase RLS
tests (#517) and the import-cycle ratchet (#808). Previously this
authenticated coach/player smoke only ran post-merge via playwright.yml's
push/manual-only `e2e` job, which stayed untouched.

Skips (not fails) on fork/Dependabot PRs, which get no repo secrets;
same-repo pushes/PRs must have the required secrets or the job fails
loudly. Docs updated to reflect the new hard-gate status and its added
CI-minutes cost.

Also unmasks the `|| echo "Playwright suite has failures..."` exit-code
shim in playwright.yml's advisory "Playwright (chromium)" job so a real
failure there turns the job red instead of silently passing (reviewer-
flagged on #812 as blocking trust in the e2e gate). That job is advisory,
not a required check, so this cannot newly block a merge — it only makes
the status honest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): skip baseball-auth-smoke on Dependabot PRs, not just forks

Dependabot PRs are opened against this repo (not a fork), so the existing
head.repo.full_name == github.repository check alone did not exclude them.
The job's own docs already claimed a Dependabot skip, but without secrets
it would hard-fail on the pull_request branch, blocking the required `all`
aggregate on every Dependabot PR. Add an explicit github.actor check and
sync the "skips on fork/Dependabot PRs" prose in the job comment,
branch-protection.md, and CI_RUNBOOK.md to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#373) (#850)

* test(e2e): authenticated Baseball route crawler, replace broken script (#373)

scripts/route-crawler-baseball.mjs POSTed credentials to a /api/auth/login
REST endpoint that never existed in this repo (BaseballHelm auth is a
client-side Supabase form, not a JSON login API), so its sign-in always
failed and it exited 0 ("no credentials — skipping") regardless of whether
CI secrets were configured — and it was never wired into any workflow. Its
"contract test" only regex-checked the source text, so it could never catch
this.

Replaces both with e2e/baseball-route-crawler.spec.ts, which runs under the
existing baseball-coach/baseball-player Playwright projects (reusing the
storageState auth baseball-smoke.spec.ts already established — no new login
code) and discovers routes from the live rendered DOM (visible <nav> links,
both the main sidebar and any hub-subnav strip) instead of statically
parsing nav-registry.ts source. e2e/helpers/route-health.ts (shared with
baseball-smoke.spec.ts, which now imports its ERROR_BOUNDARY_TEXT_RE instead
of duplicating the regex) asserts each discovered route isn't a 4xx/5xx,
doesn't bounce to /login, doesn't redirect into /golf/, doesn't render an
error boundary, doesn't get stuck on a loading spinner, and isn't
near-blank. Best-effort discovers public player/team/program/packet links
surfaced on an authenticated page (capped at 3, only real linked routes,
never guessed IDs) and re-verifies each in a fresh unauthenticated context.

Wired into playwright.yml's e2e job as its own step + artifact upload.
Deliberately NOT folded into #372's new required ci.yml gate: DOM-driven
discovery and the stuck-spinner/near-blank heuristics are new, unproven
surface area, so it runs advisory-only until it's demonstrated stable
across several main-branch runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(e2e): wire route-crawler spec into CI review mustFix items (#373)

Review flagged three CI-wiring gaps that would make the new advisory
crawler step deterministically fail on every main-branch run:

- playwright.config.ts: baseball-coach/baseball-player projects only
  matched baseball-smoke.spec.ts, so the crawler step's --project
  filters resolved zero tests ("No tests found").
- playwright.config.ts: the bare chromium project's testIgnore didn't
  exclude the new spec, so it would also run unauthenticated there.
- .github/workflows/playwright.yml: the crawler step lacked
  continue-on-error, so any future genuine failure/flake would cascade
  into skipping the subsequent required "Run Playwright tests" step,
  contradicting the step's own "advisory, doesn't block" comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ak baseline through shared adapter (#847)

* fix(baseball): #379 Phase 2 — reconcile operational-signals cold-streak baseline through shared adapter

Server actions Phase 2 chunk of the #379 stats-layer reconciliation design.

- operational-signals.ts: loadPlayerHittingSpans's season/career OPS baseline
  for the player_cold_streak rule was reading baseball_player_stats WHERE
  stat_type='season' — a value no writer in this codebase has ever produced
  (imports.ts / stats.ts's uploadStatsCSV only write 'practice' | 'game' |
  'other'). That lookup always returned [], so the cold-streak rule could
  never fire in production for any team. Fixed by sourcing the baseline
  through the shared legacy-flat adapter (legacy-stat-adapters.ts, #828),
  preferring the canonical box-score-era baseball_player_season_stats
  roll-up and falling back to the legacy baseball_player_aggregates row's
  career OPS only when no box-score-era row exists yet — the same
  precedence roster.ts/roster-aggregates-merge.ts already use for the same
  class of gap. The "recent N games" rolling window stays a direct
  baseball_player_stats read (no canonical per-game rolling-window
  primitive exists yet); manifest note updated to reflect the narrower
  remaining scope.

- insights.ts: no code change needed — #819 (tonight's withBaseballAction
  migration) already deleted its only deprecated-table references as part
  of removing dead generateTeamInsights/getTeamInsights. Its
  GRANDFATHERED_CONSUMERS entry was stale (the stat-layer contract test's
  "no stale entries" check was red before this commit); removed here.

- stats.ts: recalculatePlayerAggregates deliberately left as the raw
  legacy-row writer (documented why, not code-changed) — roster.ts,
  command-center.ts, and the player-today/-snapshot-cards/-passport
  read-models are still direct, unmigrated consumers of
  baseball_player_aggregates per the manifest, so retiring or bypassing this
  write path now would regress those surfaces to empty state for
  legacy-CSV-only teams. Matches the design's own stated alternative
  ("or is retired once nothing else needs a materialized
  baseball_player_aggregates row").

- New test: operational-signals-cold-streak.test.ts exercises the real
  runOperationalSignalDetection wrapped action (loadPlayerHittingSpans
  itself is intentionally not exported — every export of a 'use server'
  file is a real client-invokable action, and the observability-coverage
  contract enforces every such export is wrapped) across all three
  precedence tiers: box-score preferred, legacy fallback, no-data (rule
  does not fire).

Gates: tsc --noEmit clean; eslint --max-warnings 0 on all touched files
clean; targeted vitest run green (new test 3/3, stat-layer contract's
stale-entry check now green, observability-coverage contract green,
upload-stats-csv.test.ts unaffected/green). Pre-existing unrelated
failures on this base branch (practice-effectiveness.test.ts,
player-today-self-scope.test.ts, player-today-honest-loop.test.ts,
engine-run-helm-lifting.test.ts + 3 others) confirmed present before this
commit too (verified via git stash) — out of this chunk's scope, not
touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): #379 PR review — per-field OPS legacy fallback + season/career honesty

Addresses PR #847 review mustFix items:

- operational-signals.ts's loadPlayerHittingSpans seasonOPSByPlayer build
  loop previously masked a real legacy career_ops whenever a box-score-era
  baseball_player_season_stats row existed for the player but its own OPS
  field was null (the pure-pitcher/DH shape: g=0 this season, avg/obp/slg/ops
  all null per recalculate_baseball_season_stats). Added a per-field fallback
  to legacyAggregates[playerId]?.career_ops, matching
  roster-aggregates-merge.ts's toLegacyAggregateShape precedent for the
  identical gap.

- Added FactPlayerHittingSpan.seasonOPSSourceLayer (box-score/legacy-fallback/
  no-data) so the player_cold_streak rule can label its baseline honestly:
  a legacy-fallback number is the aggregate row's LIFETIME career_ops, not a
  season-scoped figure, so evidence/whyItMatters text now says "career
  average" instead of an unqualified "season average" whenever the resolved
  value didn't come from a real box-score-era season row.

- Added a 4th regression case to operational-signals-cold-streak.test.ts
  mirroring roster-aggregates-merge.test.ts's pure-pitcher/DH fixture: a
  season-stats row present with g=0 and all rate fields null, plus a legacy
  aggregates row with a real career_ops — asserts the rule still fires using
  the legacy baseline rather than silently producing 0 signals.

Does not touch select.tsx (owned by #842).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…esh (#855)

Two batch-level infra breaks blocking green CI on the assembled branch:

1. src/test/setup.tsx — ResizeObserver/IntersectionObserver mocks were
   vi.fn().mockImplementation(arrow); `new` constructs the arrow itself,
   which throws 'is not a constructor'. Latent since forever — first
   triggered by #829's use-scroll-fade (Segmented) constructing a real
   observer in any rendered test (FairwayRecruitingPage x8). Now real
   classes with vi.fn() method spies.

2. .cycles-baseline.json — check-cycles --update to lock in the 2 new
   benign madge-unresolvable externals (@supabase/supabase-js via #827's
   seed scripts, dotenv/config) per the script's own documented flow.
   Still 33 known cycles, none new.

FairwayRecruitingPage.test.tsx: 9/9 passing after fix (was 8 failing).

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Too many files changed for review. (202 files found, 50 file limit)

Bypass the limit by tagging @greptile-apps to review.

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
helmv3 Ignored Ignored Preview Jul 15, 2026 11:53am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 202 files, which is 52 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d311ef2d-34d4-49af-977a-e0db5546dfdb

📥 Commits

Reviewing files that changed from the base of the PR and between 761bea0 and 5c0b2c8.

📒 Files selected for processing (202)
  • .cycles-baseline.json
  • .github/branch-protection.md
  • .github/workflows/ci.yml
  • .github/workflows/playwright.yml
  • docs/CI_RUNBOOK.md
  • docs/audits/BASEBALLHELM_PRODUCTION_VERDICT.md
  • docs/audits/PRODUCTION_READINESS_MISSION_2026-07-09.md
  • docs/baseball/stats-architecture.md
  • docs/baseball/ui-migration-execution-plan.md
  • docs/baseball/ui-migration-map.md
  • docs/operations/BASEBALLHELM_FEATURE_READINESS_MATRIX.md
  • e2e/README.md
  • e2e/baseball-route-crawler.spec.ts
  • e2e/baseball-smoke.spec.ts
  • e2e/helpers/route-health.ts
  • memory/context/baseballhelm-features.md
  • playwright.config.ts
  • scripts/__tests__/route-crawler-baseball.test.mjs
  • scripts/route-crawler-baseball.mjs
  • scripts/seed-baseball-demo.ts
  • scripts/seed-baseball-e2e.ts
  • scripts/seed-baseball-stats.mjs
  • src/app/baseball/(auth)/complete-signup/loading.tsx
  • src/app/baseball/(auth)/forgot-password/loading.tsx
  • src/app/baseball/(auth)/reset-password/loading.tsx
  • src/app/baseball/(dashboard)/_components/hub-sub-nav.tsx
  • src/app/baseball/(dashboard)/_components/resolve-active-hub.ts
  • src/app/baseball/(dashboard)/dashboard/camps/CampsClient.tsx
  • src/app/baseball/(dashboard)/dashboard/camps/[id]/CampDetailClient.tsx
  • src/app/baseball/(dashboard)/dashboard/command-center/loading.tsx
  • src/app/baseball/(dashboard)/dashboard/compare/CompareClient.tsx
  • src/app/baseball/(dashboard)/dashboard/discover/DiscoverClient.tsx
  • src/app/baseball/(dashboard)/dashboard/documents/documents-client.tsx
  • src/app/baseball/(dashboard)/dashboard/lift/[sessionId]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/messages/[id]/ConversationClient.tsx
  • src/app/baseball/(dashboard)/dashboard/messages/[id]/loading.tsx
  • src/app/baseball/(dashboard)/dashboard/messages/loading.tsx
  • src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/RosterClient.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/loading.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/recruiting-preferences/recruiting-preferences-client.tsx
  • src/app/baseball/(dashboard)/dashboard/stats/games/loading.tsx
  • src/app/baseball/(dashboard)/dashboard/stats/upload/loading.tsx
  • src/app/baseball/(dashboard)/dashboard/travel/TravelPageClient.tsx
  • src/app/baseball/(dashboard)/dashboard/watchlist/WatchlistClient.tsx
  • src/app/baseball/(onboarding)/coach-onboarding/loading.tsx
  • src/app/baseball/(onboarding)/player/PlayerOnboardingSkeleton.tsx
  • src/app/baseball/(onboarding)/player/_components/StepProgress.tsx
  • src/app/baseball/(onboarding)/player/loading.tsx
  • src/app/baseball/(onboarding)/player/page.tsx
  • src/app/baseball/(player-dashboard)/player/timeline/loading.tsx
  • src/app/baseball/(public)/packet/[token]/error.tsx
  • src/app/baseball/(public)/player/[id]/PlayerProfileClient.tsx
  • src/app/baseball/(public)/player/[id]/error.tsx
  • src/app/baseball/(public)/player/[id]/page.tsx
  • src/app/baseball/(public)/program/[id]/error.tsx
  • src/app/baseball/(public)/program/[id]/loading.tsx
  • src/app/baseball/(public)/program/[id]/page.tsx
  • src/app/baseball/(public)/team/[id]/error.tsx
  • src/app/baseball/(public)/team/[id]/loading.tsx
  • src/app/baseball/(public)/team/[id]/page.tsx
  • src/app/baseball/actions/__tests__/discover-privacy.test.ts
  • src/app/baseball/actions/__tests__/documents-write-capability.test.ts
  • src/app/baseball/actions/__tests__/insight-lifecycle.test.ts
  • src/app/baseball/actions/__tests__/operational-signals-cold-streak.test.ts
  • src/app/baseball/actions/__tests__/practice-effectiveness.test.ts
  • src/app/baseball/actions/__tests__/practice.test.ts
  • src/app/baseball/actions/__tests__/travel.test.ts
  • src/app/baseball/actions/discover.ts
  • src/app/baseball/actions/insights.ts
  • src/app/baseball/actions/operational-signals.ts
  • src/app/baseball/actions/stats.ts
  • src/app/baseball/join/[code]/loading.tsx
  • src/app/globals.css
  • src/app/lifting/(dashboard)/dashboard/lift/[sessionId]/page.tsx
  • src/components/auth/baseball-sign-in-form.tsx
  • src/components/baseball/announcements/CreateAnnouncementFlow.tsx
  • src/components/baseball/announcements/UrgencyPicker.tsx
  • src/components/baseball/calendar/CalendarFairway.tsx
  • src/components/baseball/calendar/__tests__/CalendarFairway.shell-height.test.tsx
  • src/components/baseball/documents/DocumentCard.tsx
  • src/components/baseball/games/EditGameModal.tsx
  • src/components/baseball/games/GamesList.tsx
  • src/components/baseball/import-center/ImportWizardClient.tsx
  • src/components/baseball/messages/MessagesFairway.tsx
  • src/components/baseball/passport/PassportVisibilityControls.tsx
  • src/components/baseball/passport/PlayerPassportCard.tsx
  • src/components/baseball/passport/PlayerPassportFairway.tsx
  • src/components/baseball/passport/ScoutPacketCsvButton.tsx
  • src/components/baseball/passport/ScoutPacketManager.tsx
  • src/components/baseball/passport/index.ts
  • src/components/baseball/performance/ExerciseWizard.tsx
  • src/components/baseball/performance/LiftBuilderClient.tsx
  • src/components/baseball/performance/PerformanceCommandCenter.tsx
  • src/components/baseball/performance/lift-canvas/LiftCanvas.tsx
  • src/components/baseball/performance/lift-canvas/SessionBlock.tsx
  • src/components/baseball/player-today/PlayerTimelineFairway.tsx
  • src/components/baseball/player-today/PlayerTodayClient.tsx
  • src/components/baseball/practice-effectiveness/PracticeEffectivenessClient.tsx
  • src/components/baseball/practice-planner/PracticePlannerClient.tsx
  • src/components/baseball/practice-planner/PracticeRecapPanel.tsx
  • src/components/baseball/practice-planner/ScrimmageLineupBuilder.tsx
  • src/components/baseball/practice-planner/TimeRailBuilder.tsx
  • src/components/baseball/program/ProgramRoster.tsx
  • src/components/baseball/recruiting-philosophy/MinimumStandards.tsx
  • src/components/baseball/recruiting-philosophy/RecruitingWeightDistributor.tsx
  • src/components/baseball/settings/PhilosophySettingsClient.tsx
  • src/components/baseball/settings/ProgramSettingsClient.tsx
  • src/components/baseball/signals/SignalDrillDown.tsx
  • src/components/baseball/staff/StaffSettingsClient.tsx
  • src/components/baseball/stats-center/StatsCenterClient.tsx
  • src/components/baseball/stats/StatsUploadClient.tsx
  • src/components/baseball/travel/TravelClient.tsx
  • src/components/coach/CreateCampModal.tsx
  • src/components/coach/CreateDevPlanModal.tsx
  • src/components/coach/discover/CompareBar.tsx
  • src/components/coach/discover/DiscoverView.tsx
  • src/components/coach/discover/FilterPanel.tsx
  • src/components/coach/discover/PlayerCard.tsx
  • src/components/fairway/app-shell/FairwayHubSubNav.tsx
  • src/components/fairway/controls/segmented.tsx
  • src/components/fairway/overlays/PopoverPanel.tsx
  • src/components/fairway/overlays/Sheet.tsx
  • src/components/features/player-comparison.tsx
  • src/components/features/saved-comparisons-list.tsx
  • src/components/layout/header.tsx
  • src/components/layout/mobile-menu-button.tsx
  • src/components/lifting/groups/StrengthGroupsClient.tsx
  • src/components/lifting/players/PlayerLiftSessionClient.tsx
  • src/components/lifting/players/__tests__/PlayerLiftSessionClient.test.tsx
  • src/components/lifting/programs/ProgramEditorClient.tsx
  • src/components/lifting/programs/ProgramListClient.tsx
  • src/components/lifting/sessions/LiveWeightRoomClient.tsx
  • src/components/lifting/shell/LabShell.tsx
  • src/components/messages/NewMessageModal.tsx
  • src/components/panels/PeekPanelRoot.tsx
  • src/components/player/settings/PrivacySettingsForm.tsx
  • src/components/ui/modal.tsx
  • src/components/ui/select.test.tsx
  • src/components/ui/select.tsx
  • src/components/ui/skeleton.tsx
  • src/contracts/baseball/access/player-today-self-scope.test.ts
  • src/contracts/baseball/access/signal-inbox-staff-scope.test.ts
  • src/contracts/baseball/access/video-visibility-scope.test.ts
  • src/contracts/baseball/coachhelm/signal-inbox-evidence.test.ts
  • src/contracts/baseball/product-trust.contract.test.ts
  • src/contracts/baseball/product-trust/player-today-honest-loop.test.ts
  • src/contracts/baseball/product-trust/video-honest-empty-state.test.ts
  • src/contracts/baseball/stats/command-center-stats-center-drift.test.ts
  • src/contracts/baseball/stats/seeded-stats-non-empty.smoke.test.ts
  • src/lib/admin/feature-registry.ts
  • src/lib/baseball/__tests__/action-baseline.test.ts
  • src/lib/baseball/__tests__/ai-policy-enforcement.test.ts
  • src/lib/baseball/__tests__/engine-run-helm-lifting.test.ts
  • src/lib/baseball/__tests__/engine-stat-rows.test.ts
  • src/lib/baseball/__tests__/outcome-sweep-insight-resolve.test.ts
  • src/lib/baseball/__tests__/practice-validation.test.ts
  • src/lib/baseball/__tests__/resolve-active-hub.test.ts
  • src/lib/baseball/__tests__/roster-read-model.test.ts
  • src/lib/baseball/__tests__/signal-from-insight.test.ts
  • src/lib/baseball/coachhelm/action-baseline.ts
  • src/lib/baseball/coachhelm/engine-run.ts
  • src/lib/baseball/coachhelm/engine-stat-rows.ts
  • src/lib/baseball/coachhelm/outcome-sweep.ts
  • src/lib/baseball/insights/resolve-coach-id.ts
  • src/lib/baseball/operational-rule-engine.ts
  • src/lib/baseball/read-models/__tests__/command-center.test.ts
  • src/lib/baseball/read-models/__tests__/elite-stat-events-pagination.test.ts
  • src/lib/baseball/read-models/__tests__/legacy-stat-adapters.test.ts
  • src/lib/baseball/read-models/__tests__/player-passport-innings.test.ts
  • src/lib/baseball/read-models/__tests__/player-passport-recent-activity.test.ts
  • src/lib/baseball/read-models/__tests__/player-snapshot-cards.test.ts
  • src/lib/baseball/read-models/__tests__/roster-aggregates-merge.test.ts
  • src/lib/baseball/read-models/__tests__/roster-legacy-aggregates-source.test.ts
  • src/lib/baseball/read-models/__tests__/scout-packet-innings.test.ts
  • src/lib/baseball/read-models/command-center.ts
  • src/lib/baseball/read-models/elite-stat-events.ts
  • src/lib/baseball/read-models/legacy-stat-adapters.ts
  • src/lib/baseball/read-models/player-passport.ts
  • src/lib/baseball/read-models/player-snapshot-cards.ts
  • src/lib/baseball/read-models/player-today.ts
  • src/lib/baseball/read-models/roster-aggregates-merge.ts
  • src/lib/baseball/read-models/roster-legacy-aggregates-source.ts
  • src/lib/baseball/read-models/roster.ts
  • src/lib/baseball/read-models/scout-packet.ts
  • src/lib/baseball/stat-layer-manifest.ts
  • src/lib/coachhelm/baseball/engine-v10.test.ts
  • src/lib/coachhelm/baseball/engine.ts
  • src/lib/coachhelm/baseball/generators/index.ts
  • src/lib/coachhelm/baseball/generators/v10.test.ts
  • src/lib/coachhelm/baseball/generators/v10.ts
  • src/lib/coachhelm/baseball/loaders.ts
  • src/lib/coachhelm/baseball/metrics/registry.role-visibility.test.ts
  • src/lib/coachhelm/baseball/metrics/registry.ts
  • src/lib/redesign/flag.ts
  • src/lib/utils.ts
  • src/lib/validation/action-schemas.ts
  • src/styles/design-tokens.css
  • src/styles/tokens.css
  • src/test/fixtures/fake-supabase-fail-select.ts
  • src/test/setup.tsx
  • supabase/tests/rls/baseball_scope_player_ids_isolation.sql

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch batch/bbh-finish-0714
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

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.

@supabase

supabase Bot commented Jul 15, 2026

Copy link
Copy Markdown

Updates to Preview Branch (batch/bbh-finish-0714) ↗︎

Deployments Status Updated
Database Wed, 15 Jul 2026 11:53:28 UTC
Services Wed, 15 Jul 2026 11:53:28 UTC
APIs Wed, 15 Jul 2026 11:53:28 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Wed, 15 Jul 2026 11:53:29 UTC
Migrations Wed, 15 Jul 2026 11:53:29 UTC
Seeding Wed, 15 Jul 2026 11:53:29 UTC
Edge Functions ⚠️ Wed, 15 Jul 2026 11:53:29 UTC

⚠️ Warning — Only Functions declared in config.toml will be automatically deployed to branches: [functions.my-slug]


View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

document.documentElement.style.setProperty(SUBNAV_OFFSET_VAR, `${node.offsetHeight}px`);
};
publish();
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(publish) : null;
@njrini99-code
njrini99-code marked this pull request as ready for review July 15, 2026 11:52
@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@njrini99-code
njrini99-code enabled auto-merge (squash) July 15, 2026 11:52
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@njrini99-code
njrini99-code merged commit ad25955 into main Jul 15, 2026
43 checks passed
@njrini99-code
njrini99-code deleted the batch/bbh-finish-0714 branch July 15, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants