BaseballHelm finish: #379 stats reconciliation, heavy mobile pass, CI gates, tests — 45-PR overnight batch - #856
Conversation
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>
…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>
|
Too many files changed for review. ( Bypass the limit by tagging |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Important Review skippedToo many files! This PR contains 202 files, which is 52 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (202)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Updates to Preview Branch (batch/bbh-finish-0714) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
| document.documentElement.style.setProperty(SUBNAV_OFFSET_VAR, `${node.offsetHeight}px`); | ||
| }; | ||
| publish(); | ||
| const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(publish) : null; |
|
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. |
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
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)
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