Skip to content

Baseball statsync verification: box-score↔profile sync proven, destructive RPC replaced, shell/routes remediation#794

Merged
njrini99-code merged 30 commits into
mainfrom
integration/final-verification-2026-07-08
Jul 9, 2026
Merged

Baseball statsync verification: box-score↔profile sync proven, destructive RPC replaced, shell/routes remediation#794
njrini99-code merged 30 commits into
mainfrom
integration/final-verification-2026-07-08

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Summary

Integration branch from the 7-round statsync verify+fix cycle (2026-07-08/09):

  • Box-score save RPC — migration 20260708150000 replaces the banned DELETE-then-INSERT in save_baseball_full_box_score with per-row UPSERT + targeted delete of removed players; per-player recalc failures no longer roll back the whole game; real error_detail/error_code returned. Applied to prod 2026-07-09, verified live (pg_proc body check).
  • Migration 20260708141000 committed as a file with an explicit ⛔ HOLD header — NOT applied (touches a golf function + invite-redemption gating risks; needs owner sign-off).
  • Box-score/game/profile stat display parity (shared format-stat helper, EditGameModal, GameDetailHeader).
  • Shell/nav/routes remediation: BaseballFairwayShell + nav-registry consistency, dead season-stats + video-detail routes deleted, phantom exit-velocity columns dropped.
  • Includes previously-merged-in fix/baseballhelm-coherence-2026-07-08 + fix/db-security-hardening-2026-07-08 + cost-control CI changes.

Gates (local, exit codes captured)

  • tsc --noEmit = 0
  • eslint --max-warnings 0 = 0
  • unit: 4497 passed / 1 failed — pre-existing on origin/main (chat-tools-visibility.test.ts, reproduced in clean worktree at origin/main), tracked for the follow-up production-readiness mission.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX

Fable Integrator and others added 25 commits July 8, 2026 01:06
…ell, capped nav IA, one lift lab

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…te dead (coach-dashboard) route group

Coherence Ruling 1 (docs/baseball/COHERENCE_RULING_2026-07-08.md): NEXT_PUBLIC_REDESIGN=true
is what prod serves, so the legacy shell fork in (dashboard)/layout.tsx and
(player-dashboard)/player/layout.tsx was pure regression risk. Both now mount
BaseballFairwayShell unconditionally; the team-context DashboardSessionGuard is
unchanged. Deletes src/app/baseball/(coach-dashboard)/ entirely (confirmed dead:
only layout.tsx/template.tsx/error.tsx, zero page.tsx). Updates stale
BaseballShellLayout references in comments/test descriptions to BaseballFairwayShell.
…, strip baseball nav from shared sidebar

Both files had zero real importers once the (dashboard)/(player-dashboard) layouts
render BaseballFairwayShell unconditionally (previous commit) — deleted per Ruling 1/5.

src/components/layout/sidebar.tsx is shared with golf: removed the 5 legacy baseball
nav arrays (collegeTeamNav/hsCoachTeamNav/jucoTeamNav/showcaseOrgNav/playerTeamNav),
buildCondensedBaseballNavigation(), the showcase org/team TeamSwitcher branch, and
every other baseball-only branch. The isGolf branch (golfCoachNav, dashboardHref,
EXACT_MATCH_HREFS golf entry, coachSecondaryNav/playerSecondaryNav fallback,
isHubItemActive) is byte-identical to before — golf renders the exact same values it
did prior to this change.

Note: BaseballHelm no longer renders this Sidebar component at all (it only ever had
one consumer, the now-deleted BaseballDashboardShell) — GolfHelm's real rendered
sidebar is the separate GolfSidebar.tsx via GolfDashboardShell, not this file. This
component is consequently unreferenced by any route today; flagged for the commander
as a follow-up deletion candidate, not removed here since it falls outside this task's
named scope and the file is marked shared/untouchable without further authorization.
…dar/documents/messages pages

Calendar, Documents, and Messages pages had their own local isRedesignEnabled()
forks rendering a legacy pre-Fairway UI. Since the shell fork is gone (BaseballFairwayShell
is now the only shell), these pages always take the Fairway branch — deleted the
legacy JSX and now-unused imports (Link, BaseballCalendarWrapper, EVENT_TYPE_CONFIG,
Card/CardContent/Input/Select/Checkbox/DocumentCard/cn, the messages skeleton block).
practice/page.tsx already had no fork (comment-only precedent reference, untouched).
…ubtabs (Ruling 2)

Implements the coach hub/subtab table from
docs/baseball/COHERENCE_RULING_2026-07-08.md Ruling 2. Mechanism: nav-registry.ts
gets a `foldedUnder` field (a registry entry marks itself as reachable only
from a parent landing page instead of its own hub-tab slot, but keeps its
registry row so command palette + deep links still work) and a `hubTabLabel`
override (lets a subtab read differently from the entry's canonical label,
e.g. "Performance" reads "Training" under Development). hub-definitions.ts's
`hubEntries()` excludes folded entries from the rendered strip.

Regrouped: Announcements moves from Team to a new real Messages hub
(Messages · Announcements, replacing the old bare flat link); Practice
Planner/Effectiveness move from Stats & Performance to Team (fold into the
new Operations landing); Performance moves from Stats & Performance to
Development (folds in as "Training"); Import Center folds into Stats Center;
Interest folds into Pipeline; Watchlist/Compare/Saved Comparisons/Scout
Packets/Camps fold into a new Scouting landing; Program Info/Staff
Settings/Program Settings fold into the existing Settings landing; Teams/
Events fold into Organization. Deleted the 9-item SETTINGS_* hub-tab splice
and the 4-item PERFORMANCE_* splice outright (dead code, not layered over)
since Performance's own page already links Programs/Live/Builder/Groups.

Command Center's nav-visible label renames to "Overview" (breadcrumbs, hub
subtab, command palette, mobile nav) so "Dashboard" never has two names in
the UI; the route/page masthead keep "Command Center".

BaseballFairwayShell.tsx: Messages now flows through the generic
COACH_HUB_ORDER loop (with its unread badge threaded in by hub id) instead of
a hand-injected flat item; showcase org/team rails route their Messages item
through the same hub-aware path so showcase coaches don't lose Announcements
reachability. Mobile bottom nav trimmed to the top 3 destinations per role
(Ruling 2 item 8) — the top-bar hamburger (already bridged to the same
drawer) is the "+ More".

/baseball/dashboard/practice now server-redirects players to the canonical
/baseball/player/practice surface (item 7); coach/staff rendering unchanged.

nav-manifest.ts and route-contract.ts extended to track the new Messages hub
tabs for completeness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
New card-grid landing pages that keep the folded destinations reachable in
≤2 clicks now that Team and Recruiting cap at 3 rendered subtabs:

- /baseball/dashboard/operations (Team hub): Documents, Travel, Practice
  Planner, Practice Effectiveness.
- /baseball/dashboard/scouting (Recruiting hub): Watchlist, Compare Players,
  Saved Comparisons, Scout Packets, Camps.

Both are server components (no hooks) built from the Living Annual kit
(SectionMasthead/PaperCard/Eyebrow). Neither re-declares capability or
program-type gating — each card is resolved via getBaseballNavEntry +
isBaseballNavEntryVisible against the SAME registry entry the sidebar and
command palette use, so a coach without a capability simply doesn't see that
card, exactly like they wouldn't see it in the sidebar. Operations uses the
green "team" ink lane; Scouting uses the clay "pursuit" ink lane (recruiting).

Also adds the Stats Center masthead's persistent header-level actions
(Upload, Import Center) per Ruling 2 item 2 — Upload + Import Center are now
CTAs on the Stats Center page instead of their own hub-tab slots; the
existing empty-state "Import a box score" CTA is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…e (Ruling 2)

The card-grid landing at /dashboard/settings stays the single settings nav
surface (its own 13-tab strip is gone now that Management caps at 3 subtabs:
Decision Room · Settings · Organization). It was mounting the legacy
<Header> component on top of the shell's own top bar, duplicating breadcrumbs
and the mobile-menu affordance; replaced with a plain title block. Verified
the card grid still covers every fold target (Program Info/Settings, Staff,
Team, Season, Philosophy, Roles, Permissions, Imports, Integrations, Audit) —
nothing orphaned by the splice removal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…(Ruling 2)

nav-manifest.test.ts: extends the "every coach/both entry declares exactly
one hub" test to the new 8-hub set (adds 'messages'), and adds a permanent
lock (item 10 of the A2 task) asserting:
  (b) every coach hub renders ≤3 subtabs — both the raw arrays and a
      fully-capable head coach in every program type.
  (c) every player hub renders ≤3 subtabs.
  (d) every foldedUnder-marked registry entry still resolves to a hub via
      resolveActiveHub (command palette + deep links stay live even without
      their own hub-tab slot).

resolve-active-hub.test.ts: rewritten for the restructured hubs — Training
(performance) now resolves under Development instead of Stats & Performance;
Stats Center's Season/Upload/Import Center folds; Team's Operations folds
(Documents/Travel/Practice Planner/Practice Effectiveness); the new Messages
hub (Announcements moved off Team); Management's single Settings tab
(replacing the deleted 9-tab splice) plus Organization's Teams/Events folds;
Recruiting's Scouting folds and Pipeline's Interest fold. Behavior
intentionally changed per the ruling, so these tests were updated WITH the
change rather than left asserting the old structure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…listic seed event times

Readiness page dated check-ins in server UTC while the gate compared team-local
(evening US submissions mis-dated, gate kept nagging). Command Center day + day
bounds now team-local via the same contract-day helpers player-today already
uses. Seed script events get real times (practice 15:30-17:30, game 13:00-16:00,
meeting 12:00-13:00 ET) instead of the script's own wall-clock with zero duration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…IT V, unclip KPI numerals, slash-line backfill

Academics: is_eligible is boolean|null end-to-end; no record renders neutral
'Not on file', red Ineligible reserved for a real determination. Roster: EXIT V
column removed (avg_exit_velocity doesn't exist in the live schema and has no
write path). RuledStatLine: drop leading-none stacked over the text-ink token's
own line-height (numeral clipping). New idempotent backfill script populated
career_obp/slg/ops for 20/22 aggregate rows in prod using the app's tested
computeCareerSlashLine logic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…y real names

buildBreadcrumbs extracted to a pure module with an id-shape guard (UUID/numeric
segments fall back to the owning label, never title-cased). New BreadcrumbLabel
override channel: players/[id] shows the player's name, stats/games/[gameId] the
opponent, dev-plans/[id] the plan. 17 unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
Null end_time renders start+1h (display only), events query bounded (90d back /
365d fwd, limit 500 under the PostgREST cap), count badges pluralize. Swallowed
supabase insert errors in createBaseballEvent/createGame/box-score paths are now
checked and surfaced (ActionResult.warning). E2E box-score spec deletes its own
created games+events in teardown (prod was already purged of 160 junk rows).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…cal helm_lifting stack

All six lift/performance routes (programs, program editor, groups, live weight
room, player lift home, player session) repointed at src/components/lifting/*.
Six legacy duplicates in components/baseball/performance/ deleted (GolfHelm
palette, legacy baseball_lift_* writes). PlayerLiftToday comment corrected to
helm_lifting_*. Coach edit access preserved via helm_lifting_org_viewers bridge
migration (9/10 coaches; 10th has no org).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…lumns (applied to prod)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…cold-URL bounce

Deleted: duplicate players/[id]/profile page (canonical = players/[id]),
orphaned sidebar.tsx + team-switcher.tsx (zero importers — golf renders
GolfSidebar), knip-confirmed orphans (MatchScoreBadge, match-calculator,
dashboard-types), stale baseballhelm-* local scripts. Dev-plan page is now a
server component (requireBaseballPlayerRoute) wrapping DevPlanClient — no more
guard-race bounce to Today on hard navigation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…ber empty states, dev-plan chrome, warning toasts

Removed duplicate page-level Header mounts (shell owns the masthead). All
empty/error states on team-ops + player surfaces route through the kit
(EmptyIssue/EditorsLetter) — ReadModelStateNotice de-ambered for its 6 call
sites. DevPlanClient rebuilt on kit primitives with ≥44px touch targets.
createBaseballEvent partial-failure warnings surface as toasts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…-portal revalidation, read-only dynamic groups

Command Center week window now team-local AND respects forDate (was bare
new Date()). Player snapshot cards fall back to career obp/slg/ops like avg.
Lifting mutations revalidate the baseball routes that now render them (Wave C),
not just /lifting. StrengthGroupsClient renders rule-managed dynamic groups
first-class read-only (badge + rule summary, destructive affordances disabled)
— both existing prod groups are dynamic. +15 unit tests.

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

Stat-layer manifest grandfathers the command-center test fixture (mirrors the
already-grandfathered read-model); RosterFairway comment drops the literal
deprecated-table name; product-trust documents contract asserts the structural
EmptyIssue empty path + kit copy source instead of the pre-redesign string;
inventory docs regenerated (same content as auto-PR #784).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…-grep gate)

The box-score spec's cleanup client referenced SUPABASE_SERVICE_ROLE_KEY
directly, tripping helmv3-no-service-role-key (e2e/ is in scan scope;
scripts/ is the sanctioned exempt home, same as seed-baseball-e2e.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…ge holes)

get_users_with_auth() and get_platform_health_stats() were authenticated-
executable with zero authorization — any logged-in user could dump every
user's email + auth metadata. Both now gated with is_admin()/is_super_admin(),
matching sibling admin RPCs. update_user_last_seen() constrained to self-or-
admin; dead task-reminder RPCs, two CRM analytics RPCs, and the cron-only
refresh_crm_coach_engagement had authenticated EXECUTE revoked. Verified each
against app call sites; anon never had access. Applied to prod via MCP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…write, reconcile type with table

uploadStatsCSV wrote upload_batch_id (a column that never existed on
baseball_player_stats) on every row → every CSV upload failed the insert.
Removed the legacy write (import_run_id is the canonical linkage). Added the 7
real stat columns the type declared but the table lacked (caught_stealing,
sacrifice_bunts, runs_allowed, pitches_thrown, strikes_thrown, launch_angle,
spin_rate) additively; BaseballPlayerStats type now names the 6 real source_*/
import_run_id columns it was missing. Table and type now match exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu
…07-08' into integration/final-verification-2026-07-08
…7-08' into integration/final-verification-2026-07-08

# Conflicts:
#	src/lib/types/database.ts
…sync, shell/routes/features pass

7-round verify+fix cycle (2026-07-08/09) across the baseball surface:
- Box-score save path: RPC migration 20260708150000 replaces the banned
  DELETE-then-INSERT in save_baseball_full_box_score with per-row UPSERT
  (ON CONFLICT game_id,player_id) + targeted removal of dropped players;
  recalc loop isolated so one player's failure can't roll back the game;
  real error_detail/error_code surfaced. APPLIED to prod 2026-07-09.
- Migration 20260708141000 committed as file only — header marked HOLD
  (golf function + invite-redemption gating risks); intentionally NOT applied.
- Box-score UI: shared format-stat helper, EditGameModal, GameDetailHeader;
  stat display parity between box score, game detail, and player profiles.
- Shell/nav: BaseballFairwayShell + nav-registry consistency, dead-route
  and stat-layer-manifest cleanup, exit-velocity phantom columns dropped
  from BaseballPlayerAggregates.
- e2e + readiness matrix updated.

Known pre-existing failure (also on origin/main, untouched by this branch):
src/test/coachhelm/v3/chat-tools-visibility.test.ts — tracked for the
production-readiness mission.

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

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

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

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
helmv3 Ignored Ignored Preview Jul 9, 2026 7:29am

Request Review

@supabase

supabase Bot commented Jul 9, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project qmnssrrolpinvwjjnufo due to reaching the limit of concurrent preview branches.
Go to Project Integrations Settings ↗︎ if you wish to update this limit.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

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

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7d9cc599-51f5-46d5-8174-0c9bf4c1487a

📥 Commits

Reviewing files that changed from the base of the PR and between 761cacb and e8d213b.

📒 Files selected for processing (1)
  • docs/operations/BASEBALLHELM_FEATURE_READINESS_MATRIX.md

Summary by CodeRabbit

  • New Features

    • Added new Baseball dashboard sections for Operations and Scouting.
    • Introduced a player Development Plan page and updated Lift, Program, and Messages navigation.
  • Bug Fixes

    • Improved date handling for calendars, readiness, lifts, and event seeds to match team-local time.
    • Added safer cleanup for automated game creation and other test data, helping prevent leftover records.
    • Refined player stats, box scores, and breadcrumbs for clearer, more accurate display.
  • Documentation

    • Added several audit and readiness notes documenting current product and database status.

Walkthrough

This PR adds Baseball docs/tooling updates, unifies the shell and hub model, migrates Lift Lab flows to helm_lifting_*, and refactors calendar, stats, and player-profile behavior. It also tightens team-local date handling, partial-write warnings, and E2E cleanup.

Changes

Docs, Scripts & E2E Tooling

Layer / File(s) Summary
Audit, readiness, and route inventory docs
docs/audits/*, docs/baseball/COHERENCE_RULING_2026-07-08.md, docs/operations/BASEBALLHELM_FEATURE_READINESS_MATRIX.md, memory/projects/golfhelm.md, docs/audits/PRODUCTION_READINESS_MISSION_2026-07-09.md
New audit and mission docs are added, and route inventories/readiness tables are updated to match the current route set.
E2E teardown, backfill, and demo seeding
scripts/e2e-supabase-admin.ts, e2e/baseball-box-score.spec.ts, e2e/README.md, scripts/backfill-baseball-slash-lines.ts, scripts/seed-rini-baseball-demo.ts
A service-role teardown helper and box-score cleanup flow are added, a slash-line backfill script paginates aggregate rows and updates slash-line columns, and demo seeding uses DST-safe local times.

Baseball Shell, Breadcrumbs, and Hub Navigation

Layer / File(s) Summary
Unified BaseballFairwayShell layouts
src/app/baseball/(dashboard)/BaseballFairwayShell.tsx, src/app/baseball/(dashboard)/layout.tsx, src/app/baseball/(player-dashboard)/player/layout.tsx, src/hooks/use-baseball-auth.ts, src/hooks/__tests__/use-baseball-auth.test.tsx, src/components/baseball/NotificationBell.tsx
The dashboard and player layouts now always render BaseballFairwayShell, and related comments/tests are updated to reference the new shell mount path.
Breadcrumb override provider and builder
src/app/baseball/(dashboard)/_components/breadcrumb-label.tsx, src/app/baseball/(dashboard)/_components/breadcrumbs.ts, src/lib/baseball/__tests__/breadcrumbs.test.ts, src/app/baseball/(dashboard)/BaseballFairwayShell.tsx
A route-scoped breadcrumb label context feeds a pure breadcrumb builder that handles id-shaped segments, singularization, and route-level overrides.
Hub/tab registry rules and Messages hub
src/app/baseball/(dashboard)/_components/hub-definitions.ts, src/app/baseball/(dashboard)/_components/resolve-active-hub.ts, src/lib/baseball/nav-manifest.ts, src/lib/baseball/__tests__/*
Coach tabs now derive from the nav registry, a Messages hub is added, and routing/tests enforce the ≤3-subtab rule and the new hub mappings.
Operations, Scouting, and Messages surfaces
src/app/baseball/(dashboard)/dashboard/operations/page.tsx, src/app/baseball/(dashboard)/dashboard/scouting/page.tsx, src/app/baseball/(dashboard)/dashboard/messages/*, src/components/messages/EmptyChatState.tsx
New landing pages use nav-visible card grids, while Messages rendering/loading is simplified and EmptyChatState is standardized on EmptyIssue.

Header Removal and Living-Annual UI Migration

Layer / File(s) Summary
Dashboard pages replace Header with inline markup
src/app/baseball/(dashboard)/dashboard/*, src/app/baseball/(dashboard)/dashboard/travel/page.tsx, src/app/baseball/(dashboard)/dashboard/documents/page.tsx, src/components/baseball/travel/TravelClient.tsx, src/components/baseball/documents/DocumentsFairway.tsx, src/components/baseball/ReadModelStateNotice.tsx
Many dashboard pages replace the shared Header with inline headers or Living-Annual components like SectionMasthead, EditorsLetter, and EmptyIssue.
Settings client components inline headers
src/components/baseball/settings/*
ImportSources, Integrations, Philosophy, Season, Audit, and Team settings clients inline their headers and actions.
New Dev Plan client and server wrapper
src/app/baseball/(dashboard)/dashboard/dev-plan/*, src/app/baseball/(dashboard)/dashboard/dev-plans/*
A new player Dev Plan client is added, and the list/detail pages switch to inline headers and breadcrumb labels.

Timezone, Eligibility, Calendar, and Warning-Path Correctness

Layer / File(s) Summary
Team-local today in readiness and lift flows
src/app/baseball/(dashboard)/dashboard/readiness/page.tsx, src/app/baseball/actions/player-today-lift.ts, src/app/baseball/(dashboard)/dashboard/performance/builder/page.tsx, src/components/baseball/performance/PlayerLiftToday.tsx, src/components/baseball/performance/PerformanceDashboardClient.tsx, src/app/baseball/(dashboard)/dashboard/lift/_lift-athlete-context.ts, src/app/baseball/(dashboard)/dashboard/lift/[sessionId]/page.tsx, src/app/baseball/(dashboard)/dashboard/lift/page.tsx, src/app/baseball/(dashboard)/dashboard/performance/page.tsx
today now comes from the team timezone instead of server UTC in readiness, lift, and performance flows.
Calendar fallback end time and bounded fetch
src/app/baseball/(dashboard)/dashboard/calendar/page.tsx, src/hooks/useCalendarEvents.ts, src/components/baseball/calendar/CalendarFairway.tsx
Timed events get a display-only fallback end time, the fetch window is bounded, and the calendar frame gains a separate no-team empty state and updated upcoming-event strip.
Tri-state eligibility and partial calendar writes
src/app/baseball/(dashboard)/dashboard/academics/AcademicsClient.tsx, src/app/baseball/actions/academics.ts, src/app/baseball/actions/calendar.ts, src/components/baseball/calendar/BaseballCalendarWrapper.tsx
Academic eligibility preserves null for missing records, and calendar event creation can return a warning when secondary writes fail.
Practice routing and lift onboarding guards
src/app/baseball/(dashboard)/dashboard/practice/page.tsx, src/components/baseball/performance/LiftOnboardingGate.tsx
Player roles are redirected server-side to the player practice route, and the lift onboarding gate exits early when no athlete id is available.

Lift Lab / Helm Lifting Unification

Layer / File(s) Summary
Lift pages and athlete context
src/app/baseball/(dashboard)/dashboard/lift/_lift-athlete-context.ts, src/app/baseball/(dashboard)/dashboard/lift/[sessionId]/page.tsx, src/app/baseball/(dashboard)/dashboard/lift/page.tsx, src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx, src/app/baseball/(dashboard)/dashboard/performance/groups/page.tsx, src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx, src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx, src/app/lifting/(dashboard)/dashboard/lift/page.tsx, src/app/lifting/(dashboard)/dashboard/programs/*, src/components/lifting/programs/*
Lift pages and shared program clients now use helm_lifting_* tables, server-side athlete resolution, and basePath-aware lifting components.
Groups, live room, and programs pages
src/app/baseball/(dashboard)/dashboard/performance/groups/page.tsx, src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx, src/components/lifting/groups/StrengthGroupsClient.tsx, src/components/lifting/groups/__tests__/StrengthGroupsClient.dynamic.test.tsx
Groups/live/program pages assemble data from lifting tables and render lifting-native clients; dynamic groups are displayed as rule-managed and read-only.
Lifting actions and cache revalidation
src/app/lifting/actions/*, src/app/baseball/actions/signals.ts, src/app/lifting/actions/__tests__/*, src/app/baseball/actions/__tests__/*
Program, group, session, and lift actions revalidate baseball routes too; dynamic groups are blocked from manual edits; lift_modification now materializes into helm_lifting_sessions.
Lift canvas and styles
src/components/baseball/performance/lift-canvas/*, src/components/baseball/performance/lift-onboarding/LiftOnboardingGate.tsx
Lift canvas surfaces switch selected/idle backgrounds to cream and the onboarding gate exits early when athleteId is missing.

Box Score, Player Profile, and Stats Rendering Refactor

Layer / File(s) Summary
Shared box-score formatting
src/components/baseball/box-score/format-stat.ts, src/components/baseball/box-score/BoxScoreEntry.tsx, src/components/baseball/box-score/BoxScoreView.tsx
New box-score format helpers and EMPTY_STAT are added, and box-score entry/view components adopt them.
Games actions, uploads, and player-season stats
src/app/baseball/actions/games.ts, src/app/baseball/actions/stats.ts, src/app/baseball/actions/insights.ts
Game/save/upload actions now handle insert failures explicitly, revalidate dynamic game/player routes, and remove an old exit-velocity insight; one CSV upload link field is removed.
Game detail UI and seeded games list
src/components/baseball/games/EditGameModal.tsx, src/components/baseball/games/GameDetailHeader.tsx, src/components/baseball/games/GamesList.tsx, src/app/baseball/(dashboard)/dashboard/stats/games/*, src/app/baseball/(dashboard)/dashboard/players/[id]/stats/page.tsx
New game-detail UI is added, GamesList accepts seeded initial state, game stats pages route through the new header, and the old profile-route link is removed.
Exit-velocity removal
src/app/baseball/(dashboard)/dashboard/roster/*, src/components/baseball/command-center/analytics/PlayerPerformanceGrid.tsx, src/components/baseball/player-stats/StatsOverviewCards.tsx
Exit-velocity columns and metrics are removed from roster export/sort, analytics grids, and stats overview cards.
Player profile canonical season stats
src/app/baseball/(dashboard)/dashboard/players/[id]/page.tsx, src/components/baseball/player-profile/PlayerProfileClient.tsx, src/components/baseball/season-stats/*
Player profile pages fetch season stats and render season/per-game metrics, trend, and pitching logs from box-score read models instead of legacy aggregate stats; the old season-stats client is removed.

Estimated code review effort: 5 (Critical) | ~110 minutes

Possibly related PRs

Suggested labels: security, database


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
No Service-Role In Client Bundles ❌ Error FAIL: scripts/e2e-supabase-admin.ts:5-7, scripts/backfill-baseball-slash-lines.ts:11-14, scripts/seed-rini-baseball-demo.ts:9-12 read SUPABASE_SERVICE_ROLE_KEY outside allowed admin paths. Move service-role clients into src/lib/supabase/admin* or src/app/api//admin/, or remove direct env reads from these changed scripts.
Title check ⚠️ Warning The title is related, but it does not follow the required Conventional Commit format or include an allowed scope. Rewrite it as a Conventional Commit with an allowed scope, e.g. docs(baseball): summarize statsync verification and shell/routes remediation.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (9 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the PR’s statsync, routing, and hardening work, so it passes the lenient relevance check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Rls Coverage On New Tables ✅ Passed PASS: touched migrations are function/index rewrites only (20260708141000:35-78,102-156; 20260708150000:52-220; 20260709000000:24-26), so no changed migration adds CREATE TABLE.
Auth Check In Server Actions ✅ Passed Scanned 12 changed action files; no exported async function body contained direct .from()/.rpc() before supabase.auth.getUser(), and no async export arrows were present.
Sport-Prefixed Table Names ✅ Passed PASS: changed-file scan found no bare teams/players/coaches/rounds/events literals; query sites use prefixed tables like baseball_events and baseball_games.
No Destructive Writes ✅ Passed PASS: migration 20260708150000 replaces delete-then-insert with UPSERT+targeted DELETE, and saveCsvBoxScoreViaRpc uses the atomic RPC (migration:19-36; games.ts:3-12).
No Edits To Historical Migrations ✅ Passed Diff only touches supabase/migrations/202607*.sql files; none have a prefix <= 20260527120000.
Conventional Commits ✅ Passed PASS: The squashed commit subject docs(matrix): remove 22 closed owner-issue links per maintenance rule matches the Conventional Commits regex.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch integration/final-verification-2026-07-08
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

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

@coderabbitai coderabbitai Bot added the security Auth, secrets, RLS, PII, webhooks label Jul 9, 2026
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Baseball statsync: safe box-score UPSERT RPC, one shell, nav IA rework

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Replaces destructive DELETE-then-INSERT in save_baseball_full_box_score RPC with per-row UPSERT
 + targeted delete, adding real error_detail/error_code on failure and isolating per-player
 recalc failures; migration applied and verified in prod.
• Wires box-score saves to profile/game stats: shared format-stat helpers,
 EditGameModal/GameDetailHeader for editing games without cascading deletes, box-score-canonical
 season stats on the player profile.
• Consolidates BaseballHelm to one shell (BaseballFairwayShell rendered unconditionally), deletes
 dead (coach-dashboard) route, legacy BaseballShellLayout/dashboard-shell, and legacy baseball
 nav from the shared sidebar/team-switcher (both fully removed).
• Reworks nav IA to ≤8 primary hubs / ≤3 subtabs via a new foldedUnder registry mechanism, adds
 operations and scouting landing pages, and adds a pathname-keyed breadcrumb-override channel so
 dynamic routes show real record names instead of raw UUIDs.
• Deletes ~15 dead files (season-stats page, video-detail route, legacy Lift Lab tree,
 MatchScoreBadge/match-calculator, baseballhelm-* scripts) confirmed orphaned by knip/route audit.
• Ships DB security hardening migrations (PII-leaking admin RPCs gated, ungated SECURITY DEFINER
 RPCs revoked/scoped, source-trust/visibility CHECK constraints) plus a schema-drift fix for
 baseball_player_stats; one high-risk migration (golf function + invite gating) is committed but
 explicitly held, not applied.
• Adds/expands unit and e2e test coverage (nav-manifest ≤3-subtab lock, breadcrumbs, box-score e2e
 teardown) and documents findings in a forensic audit + coherence ruling doc.
Diagram

graph TD
  A["Coach UI: BoxScoreEntry"] --> B["saveFullBoxScore action"] --> C["save_baseball_full_box_score RPC"]
  C --> D[("baseball_box_score_batting/pitching")]
  C --> E["recalculate_baseball_season_stats"] --> F[("baseball_player_season_stats")]
  F --> G["PlayerProfileClient / GameDetailHeader"]
  H["nav-registry hub + foldedUnder"] --> I["hub-definitions hubEntries"] --> J["BaseballFairwayShell"]
  J --> K["breadcrumbs + BreadcrumbLabel override"]

  subgraph Legend
    direction LR
    _db[(Database)] ~~~ _svc([Service/Action]) ~~~ _ext{{External/UI}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Ship the box-score RPC fix as its own PR, separate from shell/nav IA rework
  • ➕ Isolates the highest-risk, prod-applied change for focused review and easy revert
  • ➕ Faster review/merge cycle for the safety-critical fix
  • ➖ Requires re-sequencing an already-completed 7-round verification cycle
  • ➖ Loses the single narrative tying box-score fix to the profile-sync verification it was validated against
2. Use a real migration-tracking table/flag for the HOLD-status golf/invite migration instead of a file-level comment header
  • ➕ Machine-enforceable block against accidental application in CI/deploy tooling
  • ➕ Clearer audit trail than a comment convention
  • ➖ Adds process/tooling overhead for what is currently a rare, manually-reviewed case

Recommendation: The UPSERT-plus-targeted-delete approach for save_baseball_full_box_score is the correct fix given the existing UNIQUE(game_id, player_id) constraint, and isolating per-player recalc failures in a nested exception block is the right way to avoid an all-or-nothing rollback. The one strategic risk is bundling this large, prod-applied RPC/security fix together with a sweeping, unrelated shell/nav/dead-code consolidation in a single integration PR — splitting the DB-safety fix from the UI/IA rework would have made the high-risk migration reviewable in isolation and reduced blast radius if either half needed a revert.

Files changed (46) +3132 / -3119

Enhancement (14) +1121 / -238
breadcrumb-label.tsxAdd pathname-keyed breadcrumb label override channel +88/-0

Add pathname-keyed breadcrumb label override channel

• Introduces a provider and a render-null writer component allowing dynamic pages to register real breadcrumb names keyed by pathname (preventing UUID crumbs and stale label leakage).

src/app/baseball/(dashboard)/_components/breadcrumb-label.tsx

breadcrumbs.tsAdd pure breadcrumb builder that never renders raw IDs +81/-0

Add pure breadcrumb builder that never renders raw IDs

• Implements registry-driven breadcrumb building with UUID/numeric detection and singularized fallbacks, plus support for page-supplied override labels.

src/app/baseball/(dashboard)/_components/breadcrumbs.ts

resolve-active-hub.tsExtend active-hub resolver for Messages hub +1/-0

Extend active-hub resolver for Messages hub

• Adds the new messages hub to the mapping so highlighting/breadcrumb resolution works across the updated IA.

src/app/baseball/(dashboard)/_components/resolve-active-hub.ts

page.tsxAdd Team hub 'Operations' landing page +117/-0

Add Team hub 'Operations' landing page

• Adds a server-rendered card-grid landing page that surfaces folded team-logistics destinations (Documents/Travel/Practice) while inheriting visibility/gating from the nav registry.

src/app/baseball/(dashboard)/dashboard/operations/page.tsx

page.tsxAdd Recruiting hub 'Scouting' landing page +122/-0

Add Recruiting hub 'Scouting' landing page

• Adds a server-rendered card-grid landing page that surfaces folded scouting tools (Watchlist/Compare/etc.) while inheriting visibility/gating from the nav registry.

src/app/baseball/(dashboard)/dashboard/scouting/page.tsx

page.tsxImprove game detail header: real breadcrumb label + coach edit affordance +14/-11

Improve game detail header: real breadcrumb label + coach edit affordance

• Replaces inline breadcrumb with BreadcrumbLabel and adds a coach-only edit flow via GameDetailHeader, gated by can_manage_stats capability.

src/app/baseball/(dashboard)/dashboard/stats/games/[gameId]/page.tsx

EditGameModal.tsxAdd coach-only game metadata editor modal +214/-0

Add coach-only game metadata editor modal

• Adds an edit modal for game metadata (date/opponent/location/notes/weather) to avoid the previous delete-to-fix behavior that would cascade-delete box scores.

src/components/baseball/games/EditGameModal.tsx

GameDetailHeader.tsxAdd game detail breadcrumb header with Edit button +54/-0

Add game detail breadcrumb header with Edit button

• Provides a reusable header showing the breadcrumb trail and a coach-only Edit button that opens EditGameModal.

src/components/baseball/games/GameDetailHeader.tsx

BoxScoreView.tsxAlign box score view columns and formatting with entry form +25/-17

Align box score view columns and formatting with entry form

• Adds missing CS/SAC/SF/LOB columns and totals, and switches avg/era/whip formatting and placeholders to the shared formatter utilities.

src/components/baseball/box-score/BoxScoreView.tsx

page.tsxUse box-score-canonical season stats on player profile and set breadcrumb label +42/-48

Use box-score-canonical season stats on player profile and set breadcrumb label

• Stops reading deprecated flat stat logs/aggregates for the profile, fetches canonical season stats + batting log via getPlayerSeasonStats, and supplies the player name to the breadcrumb override channel.

src/app/baseball/(dashboard)/dashboard/players/[id]/page.tsx

PlayerProfileClient.tsxCompute profile season totals/trends from saved box scores +173/-150

Compute profile season totals/trends from saved box scores

• Replaces legacy aggregated/career computations with season totals and a per-game batting log derived from saved box scores, ensuring profile stats reflect box-score saves immediately.

src/components/baseball/player-profile/PlayerProfileClient.tsx

nav-registry.tsAdd Messages hub and foldedUnder/hubTabLabel support; tighten program-type gates +180/-12

Add Messages hub and foldedUnder/hubTabLabel support; tighten program-type gates

• Adds a new 'messages' hub, introduces foldedUnder for tab folding and hubTabLabel for in-strip renames, and gates Postgame Review to college/juco program types while keeping deep-link resolution intact.

src/lib/baseball/nav-registry.ts

route-contract.tsInclude new coach dashboard/messages hubs in route-contract inventory +6/-0

Include new coach dashboard/messages hubs in route-contract inventory

• Extends hub tab source tracking to include the newly separated coach dashboard and coach messages tab arrays.

src/lib/baseball/route-contract.ts

nav-manifest.tsInclude new hub tab arrays in nav manifest computation +4/-0

Include new hub tab arrays in nav manifest computation

• Adds coach dashboard/messages tab arrays to the manifest’s hub tab sources so route-to-hub resolution stays consistent.

src/lib/baseball/nav-manifest.ts

Bug fix (6) +558 / -26
BoxScoreEntry.tsxFix box score entry state resync and row-retention logic +51/-18

Fix box score entry state resync and row-retention logic

• Re-syncs local state on prop changes after router refresh, auto-assigns batting order when unset, and retains previously-saved zero lines while filtering only true empty placeholders.

src/components/baseball/box-score/BoxScoreEntry.tsx

games.tsRevalidate dynamic pages after box-score saves and harden error handling +88/-8

Revalidate dynamic pages after box-score saves and harden error handling

• Adds game/player route revalidation after saves, derives season-year recalculation from game_date, and stops silently discarding calendar-event and upload-tracking errors in game creation/upload flows.

src/app/baseball/actions/games.ts

20260708150000_baseball_box_score_upsert_and_error_detail.sqlReplace destructive box-score save RPC with UPSERT + targeted delete and real error detail +263/-0

Replace destructive box-score save RPC with UPSERT + targeted delete and real error detail

• Rewrites save_baseball_full_box_score to UPSERT by (game_id, player_id), targeted-delete removed players, isolate recalc failures per player, and return error_detail/error_code to clients; also tightens grants. Applied and verified in prod.

supabase/migrations/20260708150000_baseball_box_score_upsert_and_error_detail.sql

20260708020000_gate_admin_telemetry_rpcs_pii.sqlGate PII/telemetry admin RPCs behind admin check +97/-0

Gate PII/telemetry admin RPCs behind admin check

• Adds is_admin/is_super_admin gates to SECURITY DEFINER admin telemetry RPCs that previously leaked platform-wide auth/PII to any authenticated user. Applied to prod.

supabase/migrations/20260708020000_gate_admin_telemetry_rpcs_pii.sql

20260708021000_gate_ungated_definer_rpcs.sqlRestrict/revoke additional SECURITY DEFINER RPC execution +42/-0

Restrict/revoke additional SECURITY DEFINER RPC execution

• Scopes update_user_last_seen to self-or-admin and revokes authenticated/anon EXECUTE on dead/cron-only RPCs to reduce attack surface. Applied to prod.

supabase/migrations/20260708021000_gate_ungated_definer_rpcs.sql

20260708022000_baseball_player_stats_real_stat_columns.sqlAdd missing stat columns to baseball_player_stats to match TS type +17/-0

Add missing stat columns to baseball_player_stats to match TS type

• Adds seven nullable stat columns referenced by the app/type but missing in the DB schema, fixing drift and upload failures. Applied to prod.

supabase/migrations/20260708022000_baseball_player_stats_real_stat_columns.sql

Refactor (14) +310 / -2833
baseballhelm-command-center.mjsRemove legacy baseballhelm command-center script +0/-818

Remove legacy baseballhelm command-center script

• Deletes an obsolete operational script superseded by the newer Helm Bridge/tooling direction.

scripts/baseballhelm-command-center.mjs

baseballhelm-loc.mjsRemove legacy baseballhelm loc script +0/-160

Remove legacy baseballhelm loc script

• Deletes an obsolete LOC script now unneeded after consolidation.

scripts/baseballhelm-loc.mjs

layout.tsxDelete dead coach-dashboard layout +0/-7

Delete dead coach-dashboard layout

• Removes a confirmed-dead route-group layout (no page.tsx existed under this group).

src/app/baseball/(coach-dashboard)/coach/layout.tsx

template.tsxDelete dead coach-dashboard template +0/-89

Delete dead coach-dashboard template

• Removes unused template for the deleted coach-dashboard route group.

src/app/baseball/(coach-dashboard)/coach/template.tsx

error.tsxDelete dead coach-dashboard error boundary +0/-35

Delete dead coach-dashboard error boundary

• Removes unused error boundary for the deleted coach-dashboard route group.

src/app/baseball/(coach-dashboard)/error.tsx

layout.tsxRender BaseballFairwayShell unconditionally +11/-25

Render BaseballFairwayShell unconditionally

• Removes the redesign flag fork and mounts BaseballFairwayShell as the only dashboard shell path for baseball routes.

src/app/baseball/(dashboard)/layout.tsx

layout.tsxRender BaseballFairwayShell unconditionally for player dashboard routes +1/-9

Render BaseballFairwayShell unconditionally for player dashboard routes

• Mirrors the (dashboard) layout: removes legacy shell branching so player routes use the same Fairway shell.

src/app/baseball/(player-dashboard)/player/layout.tsx

BaseballFairwayShell.tsxUnify shell chrome: capped bottom nav, showcase back-link behavior, breadcrumb provider +142/-86

Unify shell chrome: capped bottom nav, showcase back-link behavior, breadcrumb provider

• Updates BaseballFairwayShell to be the single source of shell chrome, adds breadcrumb label provider usage, adjusts bottom-nav tab selection to match the new ≤3-per-hub IA, and refines showcase org/team rail behaviors.

src/app/baseball/(dashboard)/BaseballFairwayShell.tsx

hub-definitions.tsEnforce ≤3 subtabs via foldedUnder + simplify supplements +118/-227

Enforce ≤3 subtabs via foldedUnder + simplify supplements

• Derives hub tab strips from the nav registry while excluding foldedUnder entries, supports hubTabLabel overrides, and removes most prior supplemental tab lists now folded into landings/CTAs.

src/app/baseball/(dashboard)/_components/hub-definitions.ts

format-stat.tsAdd shared stat-formatting helpers for box score UI +38/-0

Add shared stat-formatting helpers for box score UI

• Introduces a canonical empty placeholder and shared ratio/rate formatting helpers to keep entry/view glyphs and decimals consistent.

src/components/baseball/box-score/format-stat.ts

BaseballShellLayout.tsxDelete legacy BaseballShellLayout +0/-87

Delete legacy BaseballShellLayout

• Removes the old shell component now that BaseballFairwayShell is the sole dashboard frame.

src/components/baseball/BaseballShellLayout.tsx

dashboard-shell.tsxDelete legacy baseball dashboard shell composition +0/-268

Delete legacy baseball dashboard shell composition

• Removes the legacy dashboard shell component path that is no longer referenced after shell unification.

src/components/baseball/dashboard-shell.tsx

sidebar.tsxDelete shared sidebar with legacy baseball nav arrays +0/-795

Delete shared sidebar with legacy baseball nav arrays

• Removes the old shared sidebar implementation that carried baseball nav arrays and is no longer used under the Fairway shell approach.

src/components/layout/sidebar.tsx

team-switcher.tsxDelete legacy team-switcher component used by the removed sidebar +0/-227

Delete legacy team-switcher component used by the removed sidebar

• Removes the unused team-switcher component following sidebar deletion.

src/components/layout/team-switcher.tsx

Documentation (4) +176 / -13
DB_FORENSIC_AUDIT_2026-07-08.mdAdd production database forensic audit report +60/-0

Add production database forensic audit report

• Documents the 2026-07-08 forensic audit findings, what was fixed in this integration set, and what was explicitly deferred (including migration drift and retention concerns).

docs/audits/DB_FORENSIC_AUDIT_2026-07-08.md

COHERENCE_RULING_2026-07-08.mdAdd BaseballHelm coherence ruling decision doc +88/-0

Add BaseballHelm coherence ruling decision doc

• Introduces the authoritative consolidation plan (one shell, capped nav IA, one lift lab, breadcrumb correctness, and dead-code deletion) that this PR implements.

docs/baseball/COHERENCE_RULING_2026-07-08.md

BASEBALLHELM_FEATURE_READINESS_MATRIX.mdUpdate BaseballHelm readiness matrix +2/-2

Update BaseballHelm readiness matrix

• Adjusts readiness matrix entries to reflect the final verification outcomes and consolidation state.

docs/operations/BASEBALLHELM_FEATURE_READINESS_MATRIX.md

README.mdDocument service-role teardown for create-game e2e tests +26/-11

Document service-role teardown for create-game e2e tests

• Replaces prior 'known limitation' guidance with the new cleanup approach that deletes games/events created by the spec using a service-role client when available.

e2e/README.md

Other (8) +967 / -9
baseball-box-score.spec.tsSelf-clean create-game e2e data via service-role teardown +77/-4

Self-clean create-game e2e data via service-role teardown

• Adds beforeAll sweep and afterAll teardown to delete any games/events this spec created, using a helper service-role client and gracefully no-oping when the env key is absent.

e2e/baseball-box-score.spec.ts

e2e-supabase-admin.tsIntroduce shared e2e service-role admin client helper +26/-0

Introduce shared e2e service-role admin client helper

• Centralizes service-role Supabase client creation for teardown/seed use so test code doesn’t reference the service-role key directly.

scripts/e2e-supabase-admin.ts

seed-rini-baseball-demo.tsTighten baseball demo seed behavior +56/-3

Tighten baseball demo seed behavior

• Refines the demo seeding script (times/fixtures) to better match current BaseballHelm expectations and reduce drift.

scripts/seed-rini-baseball-demo.ts

backfill-baseball-slash-lines.tsAdd backfill script for baseball slash-line fields +184/-0

Add backfill script for baseball slash-line fields

• Adds a one-off data backfill script intended to populate/repair derived slash-line stats for existing records.

scripts/backfill-baseball-slash-lines.ts

breadcrumbs.test.tsAdd unit tests for breadcrumb ID handling and overrides +122/-0

Add unit tests for breadcrumb ID handling and overrides

• Adds tests ensuring UUID/numeric segments never appear in breadcrumbs, singularization behavior is stable, and page-provided overrides win as intended.

src/lib/baseball/tests/breadcrumbs.test.ts

nav-manifest.test.tsAdd permanent ≤3-subtabs-per-hub anti-regression lock +107/-2

Add permanent ≤3-subtabs-per-hub anti-regression lock

• Adds tests asserting every hub remains capped at ≤3 subtabs across program types/capability sets and that folded destinations still resolve via resolveActiveHub.

src/lib/baseball/tests/nav-manifest.test.ts

20260708141000_gate_secdef_ownership_and_redemption.sqlHOLD migration: security gating for golf recompute + invite redemption (not applied) +156/-0

HOLD migration: security gating for golf recompute + invite redemption (not applied)

• Commits a security hardening migration with an explicit HOLD header and documented break risks; intentionally not applied pending service_role/trigger and onboarding-flow fixes plus owner sign-off.

supabase/migrations/20260708141000_gate_secdef_ownership_and_redemption.sql

20260708142000_baseball_source_trust_visibility_check.sqlAdd NOT VALID CHECK constraints for source-trust/visibility columns +239/-0

Add NOT VALID CHECK constraints for source-trust/visibility columns

• Adds forward-only CHECK constraints (NOT VALID) to prevent invalid enum-like strings being inserted into trust/match/visibility columns while avoiding legacy-data validation failures.

supabase/migrations/20260708142000_baseball_source_trust_visibility_check.sql

@qodo-code-review

qodo-code-review Bot commented Jul 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (5) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 97 rules

Grey Divider


Action required

1. Anon last_seen guard bypass 🐞 Bug ⛨ Security
Description
update_user_last_seen() uses target_user_id <> auth.uid() which evaluates to NULL when
auth.uid() is NULL, so the IF does not fire and unauthenticated callers can update arbitrary
users’ last_seen. This is exploitable because the baseline grants this SECURITY DEFINER function
to anon and the migration does not revoke that privilege.
Code

supabase/migrations/20260708021000_gate_ungated_definer_rpcs.sql[R15-17]

+  IF target_user_id <> auth.uid() AND NOT public.is_admin() THEN
+    RAISE EXCEPTION 'Forbidden' USING ERRCODE = '42501';
+  END IF;
Relevance

⭐⭐⭐ High

Security hardening against anon-executable SECURITY DEFINER RPCs is a strong accepted theme (PRs
#115, #211, #733).

PR-#115
PR-#211
PR-#733

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The migration’s guard uses <> auth.uid() which becomes NULL (not true) for unauthenticated
sessions, so the IF block doesn’t raise. Separately, the baseline migration grants this function
to anon, and this PR does not revoke that grant, leaving the bypass reachable.

supabase/migrations/20260708021000_gate_ungated_definer_rpcs.sql[14-17]
supabase/migrations/20260527000000_prod_public_baseline.sql[20911-20913]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`public.update_user_last_seen(target_user_id uuid)` has a fail-open authorization check when `auth.uid()` is NULL, and the function remains callable by `anon`.

## Issue Context
Because SQL boolean expressions can evaluate to NULL, `target_user_id <> auth.uid()` does not behave like a strict inequality when `auth.uid()` is NULL.

## Fix Focus Areas
- supabase/migrations/20260708021000_gate_ungated_definer_rpcs.sql[8-24]
- supabase/migrations/20260527000000_prod_public_baseline.sql[20911-20913]

## What to change
1. Make the guard NULL-safe, e.g.:
  - `IF auth.uid() IS NULL OR (target_user_id IS DISTINCT FROM auth.uid() AND NOT public.is_admin()) THEN ...`
  - or `IF target_user_id IS DISTINCT FROM auth.uid() AND NOT public.is_admin() THEN ...` plus an explicit `auth.uid() IS NULL` clause.
2. Revoke EXECUTE/ALL from `anon` (and PUBLIC if applicable) and grant only to `authenticated` (and `service_role` if needed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. supabase as any assertions ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The updated calendar server action performs DB writes via (supabase as any), keeping any-based
access in newly modified code. This violates the rule disallowing any and makes DB call typing and
refactors less safe.
Code

src/app/baseball/actions/calendar.ts[R240-243]

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      await (supabase as any).from('baseball_event_attendance').insert(attendanceRecords);
+      const { error: attendanceError } = await (supabase as any)
+        .from('baseball_event_attendance')
+        .insert(attendanceRecords);
Relevance

⭐⭐⭐ High

Similar “remove (supabase as any)” suggestion was accepted previously (PR #296).

PR-#296

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519320 disallows any usage. The modified insert paths use (supabase as any) to
call .from(...).insert(...).

Rule 1519320: Disallow use of the any type in TypeScript
src/app/baseball/actions/calendar.ts[240-246]
src/app/baseball/actions/calendar.ts[249-252]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The calendar action uses `(supabase as any)` for inserts, which violates the no-`any` compliance rule.

## Issue Context
This module already imports `fromUntyped` (`@/lib/supabase/untyped`). Prefer using that helper (or a properly typed Supabase client/table helper) rather than `as any` assertions.

## Fix Focus Areas
- src/app/baseball/actions/calendar.ts[233-266]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Edited AUTOGEN routes block 📘 Rule violation ⚙ Maintainability
Description
The PR changes content inside an <!-- AUTOGEN:routes:start --> / <!-- AUTOGEN:routes:end -->
region. AUTOGEN-marked regions must not be manually modified.
Code

memory/projects/golfhelm.md[R264-268]

<!-- AUTOGEN:routes:start -->
<!-- DO NOT EDIT — regenerated by scripts/regen-docs.mjs -->

-**231 routes** (source: `src/app/**/page.tsx`).
+**224 routes** (source: `src/app/**/page.tsx`).
Relevance

⭐⭐⭐ High

Team auto-regenerates AUTOGEN doc blocks via bot; manual edits likely reverted (docs regen PR #620).

PR-#620
PR-#102

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519333 forbids modifying documentation content inside AUTOGEN blocks. The route
count/list inside the AUTOGEN routes region has been changed.

Rule 1519330: Do not manually modify AUTOGEN-marked documentation regions
memory/projects/golfhelm.md[264-288]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Content inside an AUTOGEN-marked documentation region was modified.

## Issue Context
AUTOGEN regions must remain byte-for-byte identical; updates must come from the generator workflow rather than hand edits.

## Fix Focus Areas
- memory/projects/golfhelm.md[264-292]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (4)
4. @supabase/supabase-js server client 📘 Rule violation ⌂ Architecture
Description
New scripts create Supabase clients by importing createClient directly from
@supabase/supabase-js instead of using the shared server-side factory. This bypasses centralized
configuration/guardrails and violates the requirement for standardized server-side Supabase access.
Code

scripts/backfill-baseball-slash-lines.ts[R43-45]

+import { config as loadEnv } from 'dotenv';
+import { createClient, type SupabaseClient } from '@supabase/supabase-js';
+import { computeCareerSlashLine, type SlashLineStatRow } from '../src/lib/baseball/aggregates/career-slash-line';
Relevance

⭐⭐ Medium

No clear prior review evidence on banning direct createClient imports in scripts; similar script
standards vary.

PR-#774
PR-#775

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519229 requires server-side Supabase access to use the shared factory
(@/lib/supabase/server). The new scripts directly import from @supabase/supabase-js.

Rule 1519229: Use the shared server-side Supabase client factory for all database access
scripts/backfill-baseball-slash-lines.ts[43-45]
scripts/e2e-supabase-admin.ts[19-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Server-side scripts are importing `createClient` from `@supabase/supabase-js` directly, violating the requirement to use the shared server-side Supabase client factory.

## Issue Context
These scripts are Node-only, but the compliance rule still requires all server-side Supabase access to go through `@/lib/supabase/server` to centralize configuration and hardening.

## Fix Focus Areas
- scripts/backfill-baseball-slash-lines.ts[43-83]
- scripts/e2e-supabase-admin.ts[19-26]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Raw glass classes used 📘 Rule violation ⚙ Maintainability
Description
A modified UI element uses raw Tailwind glass styling (backdrop-blur-* with translucent background
classes) instead of an approved .glass-* utility class. This increases style drift and violates
the glass utility enforcement rule.
Code

src/components/baseball/player-profile/PlayerProfileClient.tsx[1016]

+              <div className="flex bg-cream-100/75 backdrop-blur-sm border border-warm-200/45 rounded-xl p-1 gap-1 shadow-sm" role="group" aria-label="Filter games by type">
Relevance

⭐⭐ Medium

Design-system consolidation exists, but no historical suggestion found enforcing .glass-* over raw
blur classes.

PR-#157

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1555264 requires glass effects to use the approved .glass-* utility classes
rather than raw Tailwind blur/translucency combinations. The modified filter container uses
bg-cream-100/75 backdrop-blur-sm ... directly.

Rule 1555264: Use glass utility classes instead of raw Tailwind glass styles
src/components/baseball/player-profile/PlayerProfileClient.tsx[1016-1016]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A changed element applies glass effects using raw Tailwind classes (`backdrop-blur-*` + translucent background) instead of the approved `.glass-subtle` / `.glass-standard` / `.glass-prominent` utilities.

## Issue Context
Standardizing glass surfaces prevents variant drift and ensures consistent theming.

## Fix Focus Areas
- src/components/baseball/player-profile/PlayerProfileClient.tsx[1014-1032]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Hold migration still runnable 🐞 Bug ☼ Reliability
Description
A migration labeled “⛔ HOLD — DO NOT APPLY” is committed under supabase/migrations/, so it can be
executed by any workflow/tooling that initializes/applies the migrations directory, despite the
warning comment. The SQL itself acknowledges it can break legitimate writes (e.g., trigger/service
contexts where auth.uid() is NULL) and also touches Golf surfaces on the shared DB.
Code

supabase/migrations/20260708141000_gate_secdef_ownership_and_redemption.sql[R1-17]

+-- ============================================================================
+-- ⛔ HOLD — DO NOT APPLY AS-IS. Caller-audit (2026-07-08) found two break risks:
+--   1. recompute_golf_round_totals is invoked BY A TRIGGER on golf_shots and is
+--      GRANTed to service_role. The owner/coach-only guard here has NO
+--      service_role/backend allowance, so any service_role or seed/import write
+--      that fires the trigger (auth.uid() = NULL) would hit RAISE 'Forbidden'
+--      and BREAK legitimate golf writes. Before applying, add
+--      `auth.role() = 'service_role'` (backend/trigger context) to the allowed
+--      conditions, keeping owner/coach for direct authenticated/anon .rpc().
+--   2. try_redeem/release_baseball_team_invitation gate on "owns a
+--      baseball_players row" — a first-time user redeeming their initial invite
+--      may not yet hold one, so the guard could block onboarding. Fix needs the
+--      invite CODE threaded through (a signature change), per W10's own note.
+-- This file touches a GOLF function (owner rule: golf untouchable) and is P2
+-- security hardening, orthogonal to the routing/shell/UI/half-built pass. Left
+-- unapplied on purpose; apply only after the two fixes above + owner sign-off.
+-- ============================================================================
Relevance

⭐⭐ Medium

Team cares about migration replay safety (baseline/archive PR #117; no-op unsafe migration PR #156),
but no “HOLD file” precedent.

PR-#117
PR-#156

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The file explicitly states it must not be applied and documents known break risks, but it still
lives under supabase/migrations/. The repo’s CI spins up a local Supabase stack (which will
operate over the migrations set), and the repo has scripts that enumerate all .sql files in that
directory as migrations inputs—there is no built-in “HOLD” exclusion shown in-repo.

supabase/migrations/20260708141000_gate_secdef_ownership_and_redemption.sql[1-16]
.github/workflows/ci.yml[238-243]
scripts/check-migration-versions.mjs[96-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A “DO NOT APPLY” migration is in the canonical migrations directory, which makes accidental execution likely and undermines deterministic environment setup.

## Issue Context
CI starts a local Supabase stack, and the repo contains tooling that treats every `supabase/migrations/*.sql` file as part of the migrations set. A header comment cannot prevent execution.

## Fix Focus Areas
- supabase/migrations/20260708141000_gate_secdef_ownership_and_redemption.sql[1-24]
- .github/workflows/ci.yml[238-243]
- scripts/check-migration-versions.mjs[96-104]

## What to change
Choose one:
1. **Remove the file from `supabase/migrations/`** and move it to a non-applied location (e.g. `supabase/investigations/` or `supabase/migrations_archive/`) until it is safe.
2. If you need it tracked but not applied, introduce an explicit repo-level mechanism to exclude HOLD migrations from apply/lint/start flows (and update CI accordingly).
3. Alternatively, split it into safe-to-apply pieces and ship only the safe subset as real migrations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. builder: any in test 📘 Rule violation ⚙ Maintainability
Description
The new lifting groups action test introduces an explicit any type (const builder: any = {}).
This weakens type safety and violates the rule disallowing any in TypeScript.
Code

src/app/lifting/actions/tests/groups.test.ts[R22-24]

+function makeQueryBuilder(finalResult: FinalResult) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const builder: any = {};
Relevance

⭐⭐ Medium

Some any-cast removals accepted (PR #296) but test/mock escape hatches often tolerated; no direct
precedent.

PR-#296
PR-#776

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519320 disallows explicit any. The test defines builder as any.

Rule 1519320: Disallow use of the any type in TypeScript
src/app/lifting/actions/tests/groups.test.ts[22-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new test file uses an explicit `any` (`const builder: any = {}`), which violates the no-`any` TypeScript compliance rule.

## Issue Context
This is a mocked Supabase query builder. It can be typed with an index signature or a small explicit mock interface/type so the test remains flexible without `any`.

## Fix Focus Areas
- src/app/lifting/actions/__tests__/groups.test.ts[17-47]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

8. Server actions outside app/actions 📘 Rule violation ⌂ Architecture
Description
Modified server action modules (with top-level 'use server') live under feature directories like
src/app/baseball/actions/ and src/app/lifting/actions/ rather than src/app/actions/. This
violates the required server action placement convention.
Code

src/app/baseball/actions/calendar.ts[R72-83]

+type ActionResult<T = unknown> = {
+  success: boolean;
+  error?: string;
+  data?: T;
+  /**
+   * Set when the primary write succeeded but a secondary, best-effort write
+   * (RSVP invites, the linked baseball_games row) failed — a partial-success
+   * signal so callers can surface it without treating the whole action as
+   * failed (the primary row really was created).
+   */
+  warning?: string;
+};
Relevance

⭐ Low

Repo routinely keeps server actions under feature paths like src/app/*/actions (calendar.ts changed
in PRs #612, #648).

PR-#612
PR-#648

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519234 requires server action modules to reside under src/app/actions/. These
modified modules are server action files ('use server') located elsewhere.

Rule 1519234: Place Next.js server actions in src/app/actions directory
src/app/baseball/actions/calendar.ts[1-5]
src/app/lifting/actions/groups.ts[1-4]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Server action files that start with `'use server'` are located outside `src/app/actions/`, violating the required placement rule.

## Issue Context
These files are server action modules (top-level `'use server'`). They should be moved under `src/app/actions/` (with subfolders as needed) and imports updated accordingly.

## Fix Focus Areas
- src/app/baseball/actions/calendar.ts[1-8]
- src/app/lifting/actions/groups.ts[1-10]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +43 to +45
import { config as loadEnv } from 'dotenv';
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
import { computeCareerSlashLine, type SlashLineStatRow } from '../src/lib/baseball/aggregates/career-slash-line';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. @supabase/supabase-js server client 📘 Rule violation ⌂ Architecture

New scripts create Supabase clients by importing createClient directly from
@supabase/supabase-js instead of using the shared server-side factory. This bypasses centralized
configuration/guardrails and violates the requirement for standardized server-side Supabase access.
Agent Prompt
## Issue description
Server-side scripts are importing `createClient` from `@supabase/supabase-js` directly, violating the requirement to use the shared server-side Supabase client factory.

## Issue Context
These scripts are Node-only, but the compliance rule still requires all server-side Supabase access to go through `@/lib/supabase/server` to centralize configuration and hardening.

## Fix Focus Areas
- scripts/backfill-baseball-slash-lines.ts[43-83]
- scripts/e2e-supabase-admin.ts[19-26]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +22 to +24
function makeQueryBuilder(finalResult: FinalResult) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const builder: any = {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. builder: any in test 📘 Rule violation ⚙ Maintainability

The new lifting groups action test introduces an explicit any type (const builder: any = {}).
This weakens type safety and violates the rule disallowing any in TypeScript.
Agent Prompt
## Issue description
A new test file uses an explicit `any` (`const builder: any = {}`), which violates the no-`any` TypeScript compliance rule.

## Issue Context
This is a mocked Supabase query builder. It can be typed with an index signature or a small explicit mock interface/type so the test remains flexible without `any`.

## Fix Focus Areas
- src/app/lifting/actions/__tests__/groups.test.ts[17-47]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/app/baseball/actions/calendar.ts
Comment on lines 264 to 268
<!-- AUTOGEN:routes:start -->
<!-- DO NOT EDIT — regenerated by scripts/regen-docs.mjs -->

**231 routes** (source: `src/app/**/page.tsx`).
**224 routes** (source: `src/app/**/page.tsx`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Edited autogen routes block 📘 Rule violation ⚙ Maintainability

The PR changes content inside an <!-- AUTOGEN:routes:start --> / <!-- AUTOGEN:routes:end -->
region. AUTOGEN-marked regions must not be manually modified.
Agent Prompt
## Issue description
Content inside an AUTOGEN-marked documentation region was modified.

## Issue Context
AUTOGEN regions must remain byte-for-byte identical; updates must come from the generator workflow rather than hand edits.

## Fix Focus Areas
- memory/projects/golfhelm.md[264-292]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

{/* Filter toggle */}
<div className="flex items-center gap-2">
<div className="flex bg-cream-100/75 backdrop-blur-sm border border-warm-200/45 rounded-xl p-1 gap-1 shadow-sm" role="group" aria-label="Filter sessions by type">
<div className="flex bg-cream-100/75 backdrop-blur-sm border border-warm-200/45 rounded-xl p-1 gap-1 shadow-sm" role="group" aria-label="Filter games by type">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

6. Raw glass classes used 📘 Rule violation ⚙ Maintainability

A modified UI element uses raw Tailwind glass styling (backdrop-blur-* with translucent background
classes) instead of an approved .glass-* utility class. This increases style drift and violates
the glass utility enforcement rule.
Agent Prompt
## Issue description
A changed element applies glass effects using raw Tailwind classes (`backdrop-blur-*` + translucent background) instead of the approved `.glass-subtle` / `.glass-standard` / `.glass-prominent` utilities.

## Issue Context
Standardizing glass surfaces prevents variant drift and ensures consistent theming.

## Fix Focus Areas
- src/components/baseball/player-profile/PlayerProfileClient.tsx[1014-1032]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +15 to +17
IF target_user_id <> auth.uid() AND NOT public.is_admin() THEN
RAISE EXCEPTION 'Forbidden' USING ERRCODE = '42501';
END IF;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

7. Anon last_seen guard bypass 🐞 Bug ⛨ Security

update_user_last_seen() uses target_user_id <> auth.uid() which evaluates to NULL when
auth.uid() is NULL, so the IF does not fire and unauthenticated callers can update arbitrary
users’ last_seen. This is exploitable because the baseline grants this SECURITY DEFINER function
to anon and the migration does not revoke that privilege.
Agent Prompt
## Issue description
`public.update_user_last_seen(target_user_id uuid)` has a fail-open authorization check when `auth.uid()` is NULL, and the function remains callable by `anon`.

## Issue Context
Because SQL boolean expressions can evaluate to NULL, `target_user_id <> auth.uid()` does not behave like a strict inequality when `auth.uid()` is NULL.

## Fix Focus Areas
- supabase/migrations/20260708021000_gate_ungated_definer_rpcs.sql[8-24]
- supabase/migrations/20260527000000_prod_public_baseline.sql[20911-20913]

## What to change
1. Make the guard NULL-safe, e.g.:
   - `IF auth.uid() IS NULL OR (target_user_id IS DISTINCT FROM auth.uid() AND NOT public.is_admin()) THEN ...`
   - or `IF target_user_id IS DISTINCT FROM auth.uid() AND NOT public.is_admin() THEN ...` plus an explicit `auth.uid() IS NULL` clause.
2. Revoke EXECUTE/ALL from `anon` (and PUBLIC if applicable) and grant only to `authenticated` (and `service_role` if needed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1 to +17
-- ============================================================================
-- ⛔ HOLD — DO NOT APPLY AS-IS. Caller-audit (2026-07-08) found two break risks:
-- 1. recompute_golf_round_totals is invoked BY A TRIGGER on golf_shots and is
-- GRANTed to service_role. The owner/coach-only guard here has NO
-- service_role/backend allowance, so any service_role or seed/import write
-- that fires the trigger (auth.uid() = NULL) would hit RAISE 'Forbidden'
-- and BREAK legitimate golf writes. Before applying, add
-- `auth.role() = 'service_role'` (backend/trigger context) to the allowed
-- conditions, keeping owner/coach for direct authenticated/anon .rpc().
-- 2. try_redeem/release_baseball_team_invitation gate on "owns a
-- baseball_players row" — a first-time user redeeming their initial invite
-- may not yet hold one, so the guard could block onboarding. Fix needs the
-- invite CODE threaded through (a signature change), per W10's own note.
-- This file touches a GOLF function (owner rule: golf untouchable) and is P2
-- security hardening, orthogonal to the routing/shell/UI/half-built pass. Left
-- unapplied on purpose; apply only after the two fixes above + owner sign-off.
-- ============================================================================

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

8. Hold migration still runnable 🐞 Bug ☼ Reliability

A migration labeled “⛔ HOLD — DO NOT APPLY” is committed under supabase/migrations/, so it can be
executed by any workflow/tooling that initializes/applies the migrations directory, despite the
warning comment. The SQL itself acknowledges it can break legitimate writes (e.g., trigger/service
contexts where auth.uid() is NULL) and also touches Golf surfaces on the shared DB.
Agent Prompt
## Issue description
A “DO NOT APPLY” migration is in the canonical migrations directory, which makes accidental execution likely and undermines deterministic environment setup.

## Issue Context
CI starts a local Supabase stack, and the repo contains tooling that treats every `supabase/migrations/*.sql` file as part of the migrations set. A header comment cannot prevent execution.

## Fix Focus Areas
- supabase/migrations/20260708141000_gate_secdef_ownership_and_redemption.sql[1-24]
- .github/workflows/ci.yml[238-243]
- scripts/check-migration-versions.mjs[96-104]

## What to change
Choose one:
1. **Remove the file from `supabase/migrations/`** and move it to a non-applied location (e.g. `supabase/investigations/` or `supabase/migrations_archive/`) until it is safe.
2. If you need it tracked but not applied, introduce an explicit repo-level mechanism to exclude HOLD migrations from apply/lint/start flows (and update CI accordingly).
3. Alternatively, split it into safe-to-apply pieces and ship only the safe subset as real migrations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@njrini99-code njrini99-code enabled auto-merge July 9, 2026 04:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 65

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
docs/operations/BASEBALLHELM_FEATURE_READINESS_MATRIX.md (1)

1-112: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Blocking: CI check check:readiness-matrix is red — 22 closed issues still linked.

Per this file's own Maintenance rule 6 ("Resolved owner issues must be removed or replaced, not left linked"), the pipeline failure lists 22 stale closed-issue URLs still cited across the matrix (416, 392, 371, 412, 368, 369, 411, 399, 381, 415, 407, 370, 400, 408, 384, 397, 393, 375, 402, 396, 405, 406). This PR must update each affected row's Owner Issue column to either remove the closed link or replace it with the next real open gap before merge, per the doc's advisory-but-must-fix-when-flagged contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/operations/BASEBALLHELM_FEATURE_READINESS_MATRIX.md` around lines 1 -
112, The readiness-matrix CI check is failing because several rows still
reference closed Owner Issue URLs. Update the affected rows in
BASEBALLHELM_FEATURE_READINESS_MATRIX so each stale issue link is either removed
or replaced with the next real open issue, following the Maintenance rule that
resolved issues must not remain linked. Use the row feature names and issue
groupings in the matrix to locate each stale Owner Issue cell, and verify the
updated table still satisfies the check: only open, valid issue URLs or
intentional N/A entries should remain.

Source: Pipeline failures

src/app/baseball/(dashboard)/dashboard/academics/AcademicsClient.tsx (1)

161-172: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve is_eligible = null during edit flowssrc/app/baseball/(dashboard)/dashboard/academics/AcademicsClient.tsx:161-172 seeds editValues.is_eligible with true when the record is unset, and src/app/baseball/actions/academics.ts:716 then writes validated.is_eligible ?? true back to the DB. Editing GPA/credits on a record with no eligibility value will silently turn it into true. Keep null through the edit flow or add an explicit “not on file” option.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/academics/AcademicsClient.tsx around
lines 161 - 172, The edit flow is converting an unset eligibility value into
true, which causes records with no eligibility on file to be overwritten during
unrelated edits. Update startEditing in AcademicsClient to preserve
student.is_eligible as null instead of defaulting it to true, and make sure the
submit path in upsertPlayerAcademics keeps null intact rather than applying a
fallback. If the UI needs a default, add an explicit “not on file” choice
instead of silently coercing the value.
src/components/baseball/documents/DocumentsFairway.tsx (1)

116-132: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated upload-CTA JSX — extract to avoid drift.

The upload <Button> at 121-131 (masthead) and 193-204 (empty-state action) are near-identical: same onClick={onUpload}, same busy={isUploadingDocument}, same icon — only the label text differs ("Upload" vs "Upload document"). If one call site's props change (e.g., a disabled/loading variant) the other can silently drift out of sync.

+  const uploadButton = (label: string) =>
+    isCoach ? (
+      <Button
+        variant="primary"
+        size="sm"
+        busy={isUploadingDocument}
+        leftIcon={<Upload className="h-4 w-4" />}
+        onClick={onUpload}
+      >
+        {label}
+      </Button>
+    ) : undefined;

Then use uploadButton('Upload') and uploadButton('Upload document') at the two call sites.

Also applies to: 188-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/baseball/documents/DocumentsFairway.tsx` around lines 116 -
132, The upload CTA JSX is duplicated between the masthead and empty-state
actions in DocumentsFairway, which risks the two buttons drifting apart. Extract
the shared Button config/markup into a helper such as uploadButton in
DocumentsFairway and reuse it at both call sites, passing only the label text
("Upload" and "Upload document") while keeping onClick={onUpload},
busy={isUploadingDocument}, and the Upload icon consistent.
src/components/baseball/settings/TeamSettingsClient.tsx (1)

3-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale doc comment still lists Header as a reused primitive.

Line 13 says "Reuses GolfHelm UI primitives verbatim (Card / Button / Header)" — Header is no longer imported or used after this change (lines 18-26). Update the comment so it doesn't mislead the next person touching this file about what's actually reused.

📝 Suggested fix
-// Reuses GolfHelm UI primitives verbatim (Card / Button / Header)+ cream/green
+// Reuses GolfHelm UI primitives verbatim (Card / Button) + cream/green
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/baseball/settings/TeamSettingsClient.tsx` around lines 3 - 16,
The top-of-file comment in TeamSettingsClient is stale because it still mentions
Header as a reused primitive even though the component no longer imports or uses
it. Update that doc comment to match the actual primitives used in
TeamSettingsClient, keeping the wording aligned with the current imports and JSX
so it doesn’t mislead future changes.
src/app/baseball/(dashboard)/dashboard/calendar/page.tsx (1)

75-101: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

ascending order + hard .limit(500) biases toward past events — future events get silently dropped, not the past ones.

eventsWindowStart/eventsWindowEnd span 455 days (90 back, 365 forward). Once a team's row count in that span exceeds 500 — plausible for a full-season program (daily practices + games + travel + meetings, and if recurring events materialize as individual rows), the query returns the earliest 500 chronologically. That means the oldest rows in the window win and the most-future events — the ones a coach checking /dashboard/calendar actually cares about — are what silently disappears. There's no count check or truncation signal anywhere downstream, so this fails invisibly.

Prioritize future events so past-event volume can never crowd them out of the cap:

🐛 Proposed fix — split past/future so the future half is never truncated by the past half
+    const nowIso = new Date().toISOString();
+    const EVENT_COLUMNS = 'id, team_id, title, event_type, start_time, end_time, location, description, is_mandatory, max_attendees, rsvp_deadline, all_day, status, recurring, created_by';
-    const [eventsResult, membersResult, teamOrgResult] = await Promise.all([
-      fromUntyped(supabase, 'baseball_events')
-        .select('id, team_id, title, event_type, start_time, end_time, location, description, is_mandatory, max_attendees, rsvp_deadline, all_day, status, recurring, created_by')
-        .eq('team_id', teamId)
-        .gte('start_time', eventsWindowStart.toISOString())
-        .lte('start_time', eventsWindowEnd.toISOString())
-        .order('start_time', { ascending: true })
-        .limit(500),
+    const [futureEventsResult, pastEventsResult, membersResult, teamOrgResult] = await Promise.all([
+      // Future events are the ones a coach opens this page to see — never let
+      // trailing past-event volume push these out of the cap.
+      fromUntyped(supabase, 'baseball_events')
+        .select(EVENT_COLUMNS)
+        .eq('team_id', teamId)
+        .gte('start_time', nowIso)
+        .lte('start_time', eventsWindowEnd.toISOString())
+        .order('start_time', { ascending: true })
+        .limit(500),
+      fromUntyped(supabase, 'baseball_events')
+        .select(EVENT_COLUMNS)
+        .eq('team_id', teamId)
+        .gte('start_time', eventsWindowStart.toISOString())
+        .lt('start_time', nowIso)
+        .order('start_time', { ascending: false })
+        .limit(200),
       supabase
         .from('baseball_team_members')
         .select('player_id, baseball_players!inner(id, first_name, last_name, avatar_url)')
         .eq('team_id', teamId)
         .limit(100),
       supabase
         .from('baseball_teams')
         .select('organization_id')
         .eq('id', teamId)
         .maybeSingle(),
     ]);
+    const eventsResult = {
+      data: [...(pastEventsResult.data || []).reverse(), ...(futureEventsResult.data || [])],
+    };

At minimum, log/track when either half hits its cap so silent truncation stops being invisible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/calendar/page.tsx around lines 75 -
101, The calendar query in the dashboard page is capping results with
`order('start_time', { ascending: true })` and `.limit(500)`, which can silently
drop the most-future events when the 455-day window is crowded. Update the
`eventsResult` fetch to prioritize future events over older past entries,
ideally by splitting the range or ordering so upcoming `start_time` rows are
guaranteed to survive the cap. If a cap is still kept, add a visible
warning/metric when the query truncates results so this loss is not silent.
src/app/lifting/actions/programs.ts (1)

118-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

DRY up the 9x-repeated LAB_PATH + BASEBALL_PROGRAMS_PATH revalidation pair; addLiftWeek is missing a free per-program bust.

Six functions (createProgram:152, deleteProgram:279, addLiftWeek:727, addLiftDay:756, addLiftSection:784, addLiftPrescription:823) each hand-roll revalidatePath(LAB_PATH); revalidatePath(BASEBALL_PROGRAMS_PATH);. That's the same two lines copy-pasted 6 times, and it's already grown to a third path (BASEBALL_LIVE_PATH/BASEBALL_LIFT_PATH in publishProgram) that's not part of the pair. If a golf-side lift portal ever needs the same treatment, someone has to remember to touch all 6+ call sites individually.

Separately: addLiftWeek (L708-730) already has input.programId in scope, yet — unlike updateProgram/duplicateLiftDay/duplicateLiftWeek — it never busts ${LAB_PATH}/${input.programId} or ${BASEBALL_PROGRAMS_PATH}/${input.programId}. If the per-program builder page for either portal does any RSC-level caching, a coach adding week 1 to a brand-new program won't see it reflected on that detail route until an unrelated revalidation happens elsewhere.

♻️ Proposed helper + wiring
 const BASEBALL_PROGRAMS_PATH = '/baseball/dashboard/performance/programs';
 const BASEBALL_LIVE_PATH = '/baseball/dashboard/performance/live';
 const BASEBALL_LIFT_PATH = '/baseball/dashboard/lift';
+
+// Single choke point for lifting↔baseball cross-portal cache invalidation.
+// Any future portal (e.g. golf) only needs one new constant + one call here.
+function revalidateProgramPaths(programId?: string): void {
+  revalidatePath(LAB_PATH);
+  revalidatePath(BASEBALL_PROGRAMS_PATH);
+  if (programId) {
+    revalidatePath(`${LAB_PATH}/${programId}`);
+    revalidatePath(`${BASEBALL_PROGRAMS_PATH}/${programId}`);
+  }
+}

Then, e.g. for addLiftWeek:

-    revalidatePath(LAB_PATH);
-    revalidatePath(BASEBALL_PROGRAMS_PATH);
+    revalidateProgramPaths(input.programId);

And for createProgram / deleteProgram (no programId at hand for delete, but createProgram has the new id post-insert):

-    revalidatePath(LAB_PATH);
-    revalidatePath(BASEBALL_PROGRAMS_PATH);
+    revalidateProgramPaths((data as { id: string }).id);

Also applies to: 216-282, 708-730, 736-759, 765-787, 793-826

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/lifting/actions/programs.ts` around lines 118 - 155, Extract the
repeated revalidation pair into a shared helper used by createProgram,
deleteProgram, addLiftWeek, addLiftDay, addLiftSection, and addLiftPrescription
so the LAB_PATH and BASEBALL_PROGRAMS_PATH invalidation stays consistent in one
place. Also update addLiftWeek to revalidate the specific program routes using
input.programId, matching the per-program cache busting already done in
updateProgram, duplicateLiftDay, and duplicateLiftWeek. Use the existing symbols
addLiftWeek, createProgram, deleteProgram, and the path constants to wire this
in without duplicating the calls.
src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx (1)

40-93: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a lifting-access fallback for this page
src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx:43-63,84-93 still depends on public.helm_lifting_can_view_org() for the read path, and that helper only allows an active helm_lifting_coaches row or a helm_lifting_org_viewers row (supabase/migrations/20260625000000_helm_lifting_identity.sql:162-199). A coach can pass the BaseballHelm can_manage_lifting gate and still get [] here if they were never onboarded into the lifting org, so the Programs screen goes blank without explanation. Surface an onboarding/linked-org state or fail fast before querying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/performance/programs/page.tsx around
lines 40 - 93, The Programs page currently queries lifting data even when the
coach has BaseballHelm manage access but is not linked to the lifting org, which
can cause an empty list with no explanation. In ProgramsPage and getPrograms,
add a lifting-access fallback before querying: if
resolveBaseballLiftingOrg(teamId) returns no org or the user is not onboarded,
surface an onboarding/linked-org state or redirect/fail fast instead of
returning []. Keep the check aligned with the existing gate around
resolveBaseballCapabilities and the lifting-org lookup used by ProgramsPage.
src/app/lifting/actions/player-sessions.ts (1)

10-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the BASEBALL_LIFT_PATH const out of the import block.

Line 22's import type {...} is sandwiched after a non-import statement (lines 14-21). Not a syntax error, but it breaks import-grouping conventions and can trip import/first-style ESLint rules depending on config. Move the constant below all imports.

🔧 Suggested reorder
 import { revalidatePath } from 'next/cache';
 import { createClient } from '`@/lib/supabase/server`';
 import { fromUntyped } from '`@/lib/supabase/untyped`';
 import { resolveMyBaseballAthleteId } from '`@/lib/lifting/resolve-baseball-context`';
-
-// Baseball Wave C renders these same helm_lifting_sessions /
-// helm_lifting_readiness_checkins reads at /baseball/dashboard/lift and
-// /baseball/dashboard/lift/[sessionId] (see src/app/baseball/(dashboard)/
-// dashboard/lift/page.tsx + [sessionId]/page.tsx) — bust both alongside
-// '/lifting/dashboard/lift' below so a baseball athlete's own lift portal
-// reflects a checkin/set/status mutation immediately.
-const BASEBALL_LIFT_PATH = '/baseball/dashboard/lift';
 import type {
   HelmLiftingSessionRow,
   HelmLiftingSessionStatus,
   HelmLiftingReadinessCheckinRow,
   HelmLiftingSetResultInsert,
 } from '`@/lib/types`';
+
+// Baseball Wave C renders these same helm_lifting_sessions /
+// helm_lifting_readiness_checkins reads at /baseball/dashboard/lift and
+// /baseball/dashboard/lift/[sessionId] (see src/app/baseball/(dashboard)/
+// dashboard/lift/page.tsx + [sessionId]/page.tsx) — bust both alongside
+// '/lifting/dashboard/lift' below so a baseball athlete's own lift portal
+// reflects a checkin/set/status mutation immediately.
+const BASEBALL_LIFT_PATH = '/baseball/dashboard/lift';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/lifting/actions/player-sessions.ts` around lines 10 - 22, The
BASEBALL_LIFT_PATH constant is placed between import statements, breaking import
grouping and potentially triggering import/first lint rules. Move
BASEBALL_LIFT_PATH below all imports in player-sessions.ts, keeping the existing
symbols like revalidatePath, createClient, fromUntyped, and
resolveMyBaseballAthleteId together at the top before any non-import
declarations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/baseball-box-score.spec.ts`:
- Around line 84-145: The cleanup logic in the baseball box score spec can still
fail the run because the Supabase delete/select calls in the beforeAll and
afterAll hooks are not guarded. Update the teardown in test.beforeAll and
test.afterAll to wrap the shared delete flow in try/catch so transient failures
are swallowed or logged without failing an otherwise-passing run, and extract
the repeated select→collect ids→delete sequence into a helper used by both
hooks. Keep the existing filtering differences (ilike for leftovers,
createdOpponents for owned rows) but centralize the delete behavior around the
existing createdOpponents tracking and getServiceRoleClient usage.

In `@scripts/backfill-baseball-slash-lines.ts`:
- Around line 81-84: The backfill in backfill-baseball-slash-lines.ts is only
fetching the first page of baseball_player_aggregates, so it silently stops at
Supabase’s default 1000-row limit. Update the aggregate scan in the main
backfill loop to paginate deterministically by adding explicit ordering and a
ranged fetch loop until no more rows are returned, and apply the same paging
pattern to the baseball_player_stats lookup if needed. Use the existing backfill
routine and its fetchError/rows handling so the script processes every aggregate
row before printing completion.

In `@scripts/e2e-supabase-admin.ts`:
- Around line 21-26: getE2eAdminClient currently reads only process.env, so it
can return null during a normal Playwright run when the env vars are only
present in .env.local. Update the bootstrap in scripts/e2e-supabase-admin.ts to
load .env.local before getE2eAdminClient reads NEXT_PUBLIC_SUPABASE_URL and
SUPABASE_SERVICE_ROLE_KEY, or reuse the same dotenv initialization pattern used
by the other scripts utilities. Keep the fix localized around getE2eAdminClient
and the module’s startup setup so the client is created consistently without
manual env exports.

In `@src/app/baseball/`(dashboard)/_components/breadcrumbs.ts:
- Around line 52-81: The breadcrumb builder in buildBreadcrumbs() is collapsing
all /dashboard/settings/* pages to the parent Settings label because those
subpages do not provide a page-specific override. Add a distinct breadcrumb
label for each settings subroute, either by mounting BreadcrumbLabel in the
permissions and roles page components or by passing an overrideLabel from those
routes, so the breadcrumb can show the actual current page name instead of just
the hub label.

In `@src/app/baseball/`(dashboard)/BaseballFairwayShell.tsx:
- Around line 542-561: The showcase link handler in BaseballFairwayShell’s
shellLink callback drops the click event when isShowcaseCoach is true and href
matches SHOWCASE_ORG_HOME_HREF. Update the Link onClick branch to accept and
forward the MouseEvent to onClick after calling setSelectedTeamId(null),
matching the fallback path so consumers preserve modifier-key and
default-prevented behavior.

In `@src/app/baseball/`(dashboard)/dashboard/academics/AcademicsClient.tsx:
- Around line 234-237: The Academics page header copy is duplicated across
multiple render states and has already drifted in wording. Extract the repeated
header block from AcademicsClient into a shared component like AcademicsHeader,
then replace each inline header instance in the different states with that
component so the title/subtitle stay consistent in one place.

In `@src/app/baseball/`(dashboard)/dashboard/calendar/page.tsx:
- Around line 251-262: Add a distinct empty state for the no-team case before
rendering CalendarFairway in the calendar page. Right now the page only handles
the college-coach branch, so when teamId is null for non-college coaches or
players it still falls through to the calendar shell; update the page-level
logic around the teamId/isCollegeCoach checks to return a “no team assigned”
state whenever teamId === null. Also review BaseballCalendarWrapper so its
null-to-undefined normalization does not mask this branch, and keep the fallback
separate from the existing calendar rendering path.

In `@src/app/baseball/`(dashboard)/dashboard/camps/[id]/page.tsx:
- Around line 224-226: The inline section header markup is duplicated multiple
times in this page, including the simple and detail variants, so extract it into
a shared reusable component and replace the repeated blocks with that component.
Use a primitive like InlineSectionHeader in this file’s header sections to
centralize the border/padding/title styling and support optional
subtitle/actions for the detail variant, keeping the Camp Details and related
header blocks consistent across the page.

In `@src/app/baseball/`(dashboard)/dashboard/compare/CompareClient.tsx:
- Around line 133-138: The CompareClient header JSX is duplicated across
loading, error, main, and Suspense fallback states, so extract it into a local
CompareHeader component and reuse it at each call site. Keep the shared title
and layout inside CompareHeader, and pass only the subtitle as a prop so the
main render can still vary the message while loading/error/fallback use the same
static subtitle.

In `@src/app/baseball/`(dashboard)/dashboard/dev-plan/DevPlanClient.tsx:
- Around line 539-546: The plan header is formatting date-only fields with new
Date, which bypasses the local-date handling already added for DATE strings.
Update the plan start/end rendering in DevPlanClient to use parseLocalDate for
plan.start_date and plan.end_date before calling toLocaleDateString, matching
how getDaysUntil and the categorization logic handle date-only values. Keep
completed_at unchanged since it is a full timestamp.
- Around line 333-351: `DevPlanClient` can stay stuck on `DevPlanSkeleton` when
`player?.id` is missing because `fetchPlan` returns before clearing loading
state. Update the `fetchPlan` callback to handle the no-player path by setting
`isLoading(false)` before returning, or adjust the render/useEffect flow so the
skeleton is only shown when `player?.id` exists. Use the `fetchPlan`,
`useEffect`, and `useAuth` loading/player state as the key points to fix this.

In `@src/app/baseball/`(dashboard)/dashboard/dev-plans/[id]/page.tsx:
- Around line 90-102: The repeated Development Plan header block in the page
component should be extracted into a local reusable component so the back link,
icon, and static title/subtitle are defined once and reused at each occurrence.
Update the component used for this header in the page to replace all four inline
copies, and add a visible focus-visible ring to the back Link’s className so
keyboard users can see focus on the chevron button.

In `@src/app/baseball/`(dashboard)/dashboard/discover/DiscoverClient.tsx:
- Around line 337-342: The DiscoverClient header markup is duplicated across the
coach-access and authenticated render paths, both using the same wrapper and
inner structure. Extract a small local helper in DiscoverClient so the shared
header shell has one source of truth and future spacing/token changes only need
to be made once. While doing so, remove the unnecessary flex items-center
justify-between gap-4 from the outer wrapper since it only contains one child
and the justify-between behavior has no effect.

In `@src/app/baseball/`(dashboard)/dashboard/documents/page.tsx:
- Line 29: The `EditorsLetter` usage in the dashboard documents page is passing
`ink="team"` even though `EditorsLetter` already defaults `ink` to `team`;
remove this explicit prop from the `EditorsLetter` JSX so the call site stays
clean and relies on the component default.

In `@src/app/baseball/`(dashboard)/dashboard/events/EventsClient.tsx:
- Around line 392-414: The empty-state branch in EventsClient is not gated by
fetchError, so the `EditorsLetter` error surface and the `EmptyIssue` “Create
Your First Event” CTA can render together on load failure. Update the
conditional around the `filteredEvents.length === 0`/`EmptyIssue` render path so
it only shows when there is no `fetchError`, keeping the error state and empty
state mutually exclusive. Use the existing `fetchError`, `loading`,
`filteredEvents`, and `EmptyIssue`/`EditorsLetter` symbols in `EventsClient` to
place the guard cleanly.

In `@src/app/baseball/`(dashboard)/dashboard/journey/page.tsx:
- Around line 204-209: The journey page duplicates the same header markup in
both the loading and loaded branches, so refactor the shared wrapper and <h1>
into a single header render in page.tsx and branch only on the subtitle/action
area. Use the existing journey page header structure around the current loading
and loaded sections, preserving the “Add Schools” action only in the loaded
state and keeping the conditional wrapper closed where the loaded body already
ends.

In `@src/app/baseball/`(dashboard)/dashboard/lift/_lift-athlete-context.ts:
- Around line 54-56: The handled error paths in resolvePlayerTeamId and
hasReadinessCheckinToday are logging to console.error instead of the paged
server error logger. Replace those console.error calls with logServerError so
both branches still degrade to null/false but also report the incident through
the proper observability channel, keeping the existing function behavior intact.
- Around line 34-83: The current team resolution in resolvePlayerTeamId() only
returns the earliest active baseball_team_members row, which can cause
resolvePlayerLiftAthleteContext() to fail even when another active team would
map to a valid Lift Lab athlete. Update the resolution flow so it tries all
active team_id values for the player, then for each candidate continues through
resolveBaseballLiftingOrg() and resolveMyBaseballAthleteId() until the first
successful organization/athlete pair is found, returning null only after all
active memberships are exhausted.

In `@src/app/baseball/`(dashboard)/dashboard/lift/[sessionId]/page.tsx:
- Around line 60-106: The session/exercises/sets failure branches in
fetchSessionWithExercises currently only use console.error, so handled lift-load
errors are not recorded in the shared server error pipeline. Update those
session query, exercises query, and sets query error paths in page.tsx to call
logServerError with a clear message and the caught error before returning the
error state, so these failures flow into Sentry, error_logs, and admin_events.

In `@src/app/baseball/`(dashboard)/dashboard/lift/page.tsx:
- Around line 62-80: The handled query failures in fetchPlayerSessions are only
sent to console.error, so they should be routed through logServerError instead.
Update the upcomingError and recentError branches in lift/page.tsx to call
logServerError with the same contextual message and error details, keeping the
structured { error: true } return path unchanged. Use the fetchPlayerSessions
logic in the page component to locate both failure branches and replace the
direct console.error logging with the server error logger.
- Around line 55-68: The current upcoming sessions read in the lift dashboard is
applying a single .limit(20) across both future/today rows and overdue-open
rows, which can let overdue items crowd out today’s workout. Update the shared
fetch path in the lift page logic around the upcoming query and filtering so
current/future sessions are prioritized before any cap, either by splitting the
overdue-open and current/future reads into separate bounded queries or by
ordering the query to fetch current/future rows first. Make the same adjustment
in the mirrored lift home path so both `dashboard/lift/page.tsx` implementations
stay consistent.

In `@src/app/baseball/`(dashboard)/dashboard/performance/builder/page.tsx:
- Around line 26-29: Add regression coverage for the team-local date anchoring
in this dashboard page: the current fix in the page component is correct, but
there is no test proving that `todayIsoInTz` and the downstream
`mondayOf()`/`weekOf` logic stay tied to the team timezone instead of server
UTC. Mirror the readiness page test pattern by mocking `resolveTeamTimezone` to
return a timezone where the local date differs from `new Date().toISOString()`,
then assert the page’s `today` and `weekOf` values used by
`getGroupSorenessFlags` and `getGroupAvailability` reflect the team-local date.

In `@src/app/baseball/`(dashboard)/dashboard/performance/groups/page.tsx:
- Around line 45-91: The Supabase loaders are swallowing query failures, so
`getGroupsWithMembers` and `getAthletes` can return empty results on RLS or SQL
errors just like a true no-data case. Update both functions to destructure the
`error` from the `fromUntyped(...).select(...)` calls, and in each path either
throw the error or pass it to the page error handling so failures surface
instead of falling back to `[]`/`data ?? []`.

In `@src/app/baseball/`(dashboard)/dashboard/performance/live/page.tsx:
- Around line 83-101: The `buildLiveRoomData` flow is swallowing `sessionsError`
and returning an empty data set, which makes a DB failure look identical to a
legitimately empty live board. Update `buildLiveRoomData` to return a
failure/notice state instead of `{ athletes: [], exerciseLibrary }` when the
`helm_lifting_sessions` query fails, and have `page.tsx` render the existing
read-model state notice pattern rather than passing empty `initialAthletes` to
`LiveWeightRoomClient`. Use the existing `logQueryError`, `buildLiveRoomData`,
and `LiveWeightRoomClient` entry points to propagate the error state cleanly.

In
`@src/app/baseball/`(dashboard)/dashboard/performance/programs/[programId]/page.tsx:
- Around line 79-115: The prescriptions tree load is unbounded and can truncate
at PostgREST’s row cap, so update the data fetch in the program page loader to
paginate `helm_lifting_prescriptions` using the existing `fetchAllRowsResult` +
`.range()` pattern, and apply the same fix in the mirrored lifting program page.
While touching this loader, stop discarding query failures from `program`,
`weeks`, `days`, `sections`, `prescriptions`, and `exRows`; handle the `error`
values explicitly in the page data function so DB/RLS issues surface instead of
returning empty trees or 404s. Use the existing loader symbols like
`fromUntyped`, `weekList`, `dayList`, `sectionList`, `prescList`, and
`exerciseNameMap` to keep the fix localized.

In `@src/app/baseball/`(dashboard)/dashboard/players/[id]/page.tsx:
- Around line 109-120: The player profile is fetching pitching log data but
`PlayerProfileClient` only consumes `seasonStats` and `battingLog`, so the
pitching history is never shown. Update the profile flow by either threading
`pitchingLog` from the `page.tsx` fetch through `PlayerProfileClient` and using
it where per-game trends are rendered, or remove `pitchingLog` from
`getPlayerSeasonStats()` for this batting-only surface. Use the
`PlayerProfileClient` component and the `getPlayerSeasonStats()` call in the
dashboard player page to locate the affected data flow.

In `@src/app/baseball/`(dashboard)/dashboard/program/page.tsx:
- Around line 214-217: The Program page header shell is duplicated across the
three conditional branches, which will cause the shared layout to drift over
time. Extract the repeated header wrapper from the branch rendering in
program/page.tsx into a reusable local component or helper used by the Program
page, and pass only the varying subtitle/action content into it. Keep the unique
header content in the existing Program page branches, but centralize the shared
border-b/px-6/pb-5/pt-6 header structure so future styling changes are made
once.
- Around line 251-262: The Program Profile header row can overlap on narrow
screens because the wrapper in the dashboard page uses a non-wrapping flex
layout. Update the header container in the Program Profile section to allow
wrapping and better vertical alignment, similar to the shared Header component
behavior, so the title/subtitle block and the “View Public Page” action can
stack cleanly on mobile. Use the existing header markup around the title,
subtitle, and Link/Button to locate the fix.

In `@src/app/baseball/`(dashboard)/dashboard/readiness/__tests__/page.test.tsx:
- Around line 44-59: Replace the bespoke vi.mock for '`@/lib/supabase/server`' in
page.test.tsx with the shared createFakeSupabase fixture from
src/test/fixtures/fake-supabase.ts. The test only needs the
from(...).select().eq().maybeSingle() behavior on the baseball_teams table, so
configure that fixture instead of hand-rolling the chain. Keep the existing
createClient symbol mocked, but delegate its return value to the shared fake to
stay consistent with the rest of the suite.
- Around line 90-97: The readiness page test is asserting against the raw React
element tree from PlayerReadinessPage, which makes it brittle to JSX shape
changes; update the test to verify user-visible behavior indirectly by spying on
PlayerReadinessClient’s props instead of unwrapping element.props.children. Use
the existing PlayerReadinessPage and mocked PlayerReadinessClient setup, make
the mock capture its received props, render the page output with the test
renderer you already use, and assert the captured checkDate matches the expected
team-local date without depending on a single wrapping <div>.

In `@src/app/baseball/`(dashboard)/dashboard/scouting/page.tsx:
- Around line 39-122: Both ScoutingPage and the Operations landing page are
duplicating the same nav-card rendering and empty-state logic, so extract that
shared behavior into a reusable NavCardGrid component/helper. Move the
getBaseballNavEntry and isBaseballNavEntryVisible filtering plus the
PaperCard/Link grid and empty-state handling into the shared abstraction, then
have ScoutingPage and the Operations page pass their card definitions, masthead
text, and empty-state copy into it.
- Around line 66-73: The fallback in the scouting dashboard page is fabricating
a partial BaseballNavContext when getBaseballNavContext() returns null, which
hides a real unresolved-team state and can make the empty-state copy misleading.
Update the logic in the scouting page component so it does not cast a synthetic
{ role: 'coach', capabilities: {} } object to BaseballNavContext; instead either
fail loudly after requireRecruitingCoachRoute() if context is unexpectedly
missing, or render a dedicated no-active-team state. Keep the real resolver
shape intact, including programType, and ensure SCOUTING_CARD_IDS filtering via
isBaseballNavEntryVisible only runs with a valid resolved context.

In `@src/app/baseball/`(dashboard)/dashboard/settings/page.tsx:
- Around line 222-228: Extract the repeated page header markup into a shared
page-header primitive and replace the inline block in Settings with that
component. Create or reuse a small component such as PageHeaderBar that owns the
border-b wrapper, h1 title, and p subtitle styling so the same 3-node structure
is centralized across this PR. Update the Settings page and other duplicated
page-header usages to pass title and subtitle props instead of hand-rolling the
markup.

In `@src/app/baseball/`(dashboard)/dashboard/settings/roles/page.tsx:
- Around line 47-52: The inline page header block is duplicated across many
sibling dashboard pages, so replace this repeated `div`/`h1`/`p` pattern with a
shared header primitive instead of keeping it local to this `Roles` page. Update
the `page.tsx` implementation to use the existing reusable `Header` (or extract
a new shared inline-header component if needed) so the common border, spacing,
title, and subtitle styling live in one place and future changes only require a
single edit.

In `@src/app/baseball/`(dashboard)/dashboard/stats/games/[gameId]/page.tsx:
- Around line 84-96: The page currently makes an extra database round-trip by
calling hasBaseballCapability(team.id, 'can_manage_stats') after already loading
coach/team/member data. Update the game page component to reuse or cache the
existing getBaseballStaff(team.id) result when determining canManageStats, so
the capability check in the dashboard game page does not trigger a redundant
fetch while keeping the BreadcrumbLabel and GameDetailHeader behavior unchanged.

In `@src/app/baseball/`(dashboard)/dashboard/stats/games/page.tsx:
- Around line 42-48: The fetch results in the dashboard page are being silently
collapsed into empty values, so failed `gamesResult` or `recordResult` loads
look identical to a real empty season. Update the `page.tsx` server component to
handle both failure branches explicitly in the `GamesList` data-loading path:
call `logServerError` with a clear message and metadata when either result has
`success === false`, and pass an error indicator into `GamesList` so it can
render a true failure state instead of defaulting to
`initialGames`/`initialRecord`.

In `@src/app/baseball/actions/calendar.ts`:
- Around line 262-273: The warning composition in createBaseballEvent is joining
multiple warning messages without a separator, so if both RSVP invite and linked
game creation fail the returned warning becomes unreadable; update the warning
aggregation so each message is separated cleanly before returning the result
object. Also add a unit test around createBaseballEvent that covers secondary
insert failures (baseball_event_attendance and baseball_games) and asserts the
returned warning field is populated and formatted correctly, since
EventsClient.tsx now branches on result.warning.
- Around line 225-232: The soft-warning path in `calendar.ts` only surfaces
`attendanceError` and `gameError` to the client, so repeated sync or schema
failures are not logged server-side. Update the `warnings` handling in the
calendar action flow to pass each warning through `logServerError` as soon as it
is pushed, while still returning the existing `warning` field on success. Use
the existing `logServerError` helper from `src/lib/server-error-logger.ts` and
apply it around the `attendanceError`/`gameError` branches in the calendar
action logic.

In `@src/app/baseball/actions/games.ts`:
- Around line 720-724: The season-year calculation in the baseball games flow is
using date parsing that is timezone-sensitive and can reintroduce the
cross-New-Year bug. Update the logic in the `games.ts` season bucketing path
(the `seasonYear` derivation near `getTeamGamesImpl` /
`getPlayerSeasonStatsImpl` behavior) so it reads the year directly from the
`game.game_date` string instead of constructing a `Date`, keeping it consistent
with the file’s string-based season filters and preventing mismatches between
the game row and the season row.

In `@src/app/baseball/actions/player-today-lift.ts`:
- Around line 107-109: Add dedicated unit coverage for player-today-lift.ts to
lock in the new team-local date behavior in the action that computes today via
resolveTeamTimezone and todayIsoInTz. Mock resolveTeamTimezone/todayIsoInTz (or
use a negative-UTC-offset team timezone fixture) and assert the
scheduled_date/checkin_date filtering in the player-today-lift action uses the
team-local calendar day rather than server UTC or new Date().toISOString().

In `@src/app/baseball/actions/signals.ts`:
- Around line 401-414: The scheduled_date used in the dedupe query is still
based on UTC via new Date().toISOString().slice(0, 10), which can diverge from
the team-local “Today” logic. Update the write path in signals.ts to use the
same team-local date source already used elsewhere, specifically
todayIsoInTz(resolveTeamTimezone(...)), so the helm_lifting_sessions lookup and
insert key stay aligned with player Today/readiness behavior. Keep the existing
(athlete_id, scheduled_date, title) dedupe in place, but feed it the team-local
date computed from the relevant team timezone.

In `@src/app/lifting/actions/__tests__/groups.test.ts`:
- Around line 4-12: The guard comment in groups.test.ts overstates coverage: the
suite only tests createGroup, updateGroup, and deleteGroup, but not the
member-list mutators addGroupMember and removeGroupMember that match the
stale-member-list regression. Add regression tests in the groups.test.ts
describe block for addGroupMember and removeGroupMember using the real
signatures from groups.ts, and verify both portals’ caches are revalidated; if
those functions are not meant to revalidate both caches, tighten the header
comment so it only claims the functions actually covered.

In `@src/app/lifting/actions/__tests__/player-sessions.test.ts`:
- Around line 112-129: The regression tests in player-sessions.test.ts are only
checking revalidation calls and can pass even if the action fails, so add an
explicit assertion that the action result succeeded before inspecting
revalidatePath. Update the relevant logMySetResult test cases to assert
res.success is true first, then keep the existing portal path checks; use the
logMySetResult call site and revalidatePath assertions as the anchors for both
affected tests.

In `@src/app/lifting/actions/__tests__/sessions.test.ts`:
- Around line 145-174: The Wave C revalidation test in logSetResult should
assert the full revalidation contract, not just the [sessionId] page call.
Update the expectations in sessions.test.ts to verify the second argument passed
to revalidatePath for /baseball/dashboard/lift,
/baseball/dashboard/performance/live, and /lifting/dashboard/sessions/live (and
keep the existing page assertion for /baseball/dashboard/lift/[sessionId]) so
accidental type swaps like layout/page changes are caught. Also remove the
duplicated resetTables() in beforeEach if you touch that setup block.

In `@src/app/lifting/actions/groups.ts`:
- Around line 186-191: The dynamic-group guard in groups.ts is duplicated across
addGroupMember, removeGroupMember, updateGroup, and deleteGroup, and the shared
error text is wrong for the rename/delete call sites. Extract the
`group.group_type === 'dynamic'` check into a single helper (or shared guard)
and pass an action-specific message from each caller: keep the membership
wording only for member-edit actions, and use rename/delete wording for
`updateGroup` and `deleteGroup`. Also verify whether `deleteGroup` should allow
a retirement path for dynamic groups; if not, keep the block but make the
message reflect deletion rather than membership.

In `@src/app/lifting/actions/player-sessions.ts`:
- Around line 14-21: The lifting actions module is hardcoding a sport-specific
portal path, which will keep duplicating revalidation logic as more sports are
added. Refactor the mutation flow in player-sessions.ts by centralizing the
extra portal paths into a small helper or shared array, and have
submitLiftReadiness, startMySession, logMySetResult, and completeMySession call
that helper instead of directly referencing BASEBALL_LIFT_PATH. Keep the generic
lifting path plus any sport-specific paths (like baseball and golf) in one place
so future portals only need a single update.

In `@src/app/lifting/actions/sessions.ts`:
- Around line 237-239: The repeated cache invalidation triple should be
centralized so the same revalidation logic is not duplicated across multiple
mutation paths. Extract the shared revalidatePath calls for BASEBALL_LIVE_PATH,
BASEBALL_LIFT_PATH, and BASEBALL_LIFT_SESSION_PATH into a helper in sessions.ts,
then replace each call site with that helper to keep the behavior consistent and
easier to maintain.

In `@src/components/baseball/calendar/CalendarFairway.tsx`:
- Around line 44-48: The LiveDot label is duplicating pluralization logic
instead of using the shared pluralizeEventLabel helper. Update the label
construction in CalendarFairway so the upcoming-events text is built through
pluralizeEventLabel, matching the existing count-badge formatting and keeping
all singular/plural rules centralized in one place.

In `@src/components/baseball/documents/DocumentsFairway.tsx`:
- Around line 206-213: The filtered-empty branch in DocumentsFairway currently
shows only the EmptyState with no way to recover from a zero-results
search/category view. Update the empty-state handling in DocumentsFairway so the
“No results” branch includes a “Clear filters” CTA, and wire it to reset the
active query and category via onSearchChange('') and onCategoryChange('all').
Keep the existing EmptyState variant="search" copy, but add the new action in
the same conditional branch that renders the filtered-to-zero state.

In `@src/components/baseball/games/EditGameModal.tsx`:
- Around line 74-96: The game type toggle and error banner in EditGameModal are
using non-canonical palette classes, so update the selected scrimmage state in
the game type button group to use the app’s approved design-system token instead
of purple-* and change the error banner styling to use the destructive token
rather than red-*. Keep the rest of the logic in EditGameModal unchanged and
align the className branches for the game type selector and validation message
with the repo’s permitted color tokens.
- Around line 74-96: The game-type and home/away button rows in EditGameModal
lack accessible selection semantics, so update each mutually exclusive group to
expose its state to assistive tech. In the game-type and home/away mappings, add
aria-pressed based on the current selection for each Button, and wrap each
button set in a role="group" container with aria-labelledby referencing the
existing label text (or give the label an id and reference it). Keep the visual
styling as-is, but make sure the selected option is announced and the group
relationship is clear.
- Around line 50-58: The optional baseball game fields are being dropped instead
of cleared because EditGameModal’s update payload converts empty values to
undefined, and updateGame skips undefined keys before calling the database
update. Update the EditGameModal submission logic so empty opponentName,
location, notes, and weather are sent as null values, and verify updateGame
preserves those nulls when building the update payload for baseball_games.

In `@src/components/baseball/games/GameDetailHeader.tsx`:
- Around line 47-49: The conditional render around EditGameModal is redundantly
gating on editOpen, which unmounts the modal immediately and prevents any close
animation from playing. Update GameDetailHeader so EditGameModal stays mounted
and let its own open prop control visibility/transition behavior, using the
component’s existing open/onClose contract rather than wrapping it in an extra
editOpen && check.

In `@src/components/baseball/games/GamesList.tsx`:
- Around line 36-46: The mount-time fetch in GamesList is still running on first
render and overwriting seeded initialGames/initialRecord, causing a duplicate
client fetch and loading flash. Update the existing useEffect that calls
fetchGames so it skips the initial mount when initialGames is already present,
while keeping the later prop-sync effects in place. Use the GamesList
component’s fetchGames callback and the mount effect near the existing useEffect
block to apply the guard cleanly.

In `@src/components/baseball/living-annual/RuledStatLine.tsx`:
- Line 132: The className change on RuledStatLine’s numeral wrapper removed
leading-none, which may break the intended baseline/rule alignment described by
the component’s header comment and the rule-rendering markup. Verify the numeral
still visually rests on the baseline rule in RuledStatLine, especially for the
hero size and the numeralSize-driven text classes, and restore leading-none if
the default line-height adds unwanted vertical space or detaches the glyph from
the rule.

In `@src/components/baseball/performance/PerformanceDashboardClient.tsx`:
- Around line 284-287: The “Checked In Today” count in
PerformanceDashboardClient still falls back to client time because the page does
not pass the team-local today value. Update the server page component to compute
todayIso there and pass it into PerformanceDashboardClient, then rely on the
today prop inside the reportedToday useMemo instead of new
Date().toLocaleDateString('en-CA').

In `@src/components/baseball/settings/ImportSourcesClient.tsx`:
- Around line 383-394: The inline title block in ImportSourcesClient is already
functionally correct, but it can be cleaned up for semantics and readability:
change the outer wrapper used for the page title region to the appropriate
Header landmark, and simplify the teamName/source registry text in the h1
section by removing the unnecessary template-literal wrapper while keeping the
same JSX interpolation.

In `@src/components/baseball/settings/PhilosophySettingsClient.tsx`:
- Around line 238-248: The back control is currently implemented as a Button
nested inside a Link, which creates invalid interactive nesting; update the
shared pattern in PhilosophySettingsClient by making the Link itself the single
interactive element styled like the existing button, or switch to a plain Button
that calls router.push for navigation. Apply the same fix to both
back-navigation instances in PhilosophySettingsClient so only one focusable
control remains, and do not rely on Button asChild since
src/components/ui/button.tsx does not support it.

In `@src/components/baseball/settings/TeamSettingsClient.tsx`:
- Around line 130-135: The Team Settings header JSX is duplicated in both
branches of TeamSettingsClient, so extract the shared `border-b`/`h1`/`p` block
into a reusable header component or helper and pass the subtitle text as a prop.
Update both render paths to use that shared symbol so the title styling stays in
one place and the duplicated markup does not drift. While doing so, remove the
unnecessary `flex items-center justify-between gap-4` wrapper behavior if the
extracted header still has only one child.

In `@src/components/baseball/stats-center/StatsCenterClient.tsx`:
- Around line 546-566: The two adjacent action buttons in StatsCenterClient use
the same upload icon, making Upload and Import Center visually
indistinguishable. Update the Import Center button in the StatsCenterClient
header actions to use a different import-related icon from the existing icon set
(for example a folder/open/import symbol), while keeping Upload on IconUpload.
Use the button JSX in StatsCenterClient as the target location and ensure the
two CTA icons are distinct.

In `@src/components/baseball/travel/TravelClient.tsx`:
- Around line 142-155: The empty-state “Create First Trip” CTA in TravelClient
should mirror the masthead “Create Trip” behavior by clearing editingItinerary
before opening the modal. Update the EmptyIssue action handler in TravelClient
so it calls setEditingItinerary(null) and then setShowCreateModal(true),
matching the existing Create Trip button logic for consistency and future
safety.

In `@src/components/lifting/programs/ProgramListClient.tsx`:
- Around line 45-48: The program link construction in ProgramListClient.tsx is
vulnerable to double-slash routes when basePath includes a trailing slash.
Normalize basePath once in the component (for example in ProgramListClient and
any shared helper) to remove any trailing slash, then use that normalized value
for the “New program” navigation and the per-card links currently built from
basePath and result.id/program.id.

In `@src/lib/baseball/__tests__/nav-manifest.test.ts`:
- Around line 312-336: The hardcoded PROGRAM_TYPES array in nav-manifest.test.ts
is not exhaustively tied to BaseballProgramType, so it can drift silently when
the union changes. Update the test near the PROGRAM_TYPES declaration to derive
the program-type list from a compile-time exhaustive source, using a
Record<BaseballProgramType, true> or equivalent pattern, so any new or removed
BaseballProgramType forces a compile error. Keep the rest of the loop over
COACH_HUB_ORDER, filterHubTabsByCapabilities, and filterHubTabsByProgramType
unchanged.
- Around line 353-378: The folded-route test in resolveActiveHub only checks for
any non-null hub, which can miss a wrong-hub match. Update the assertion in the
folded destination case to verify the resolved hub is the expected owner by
checking resolved?.tabs includes entry.foldedUnder or that resolved?.id matches
the owning hub, using BASEBALL_NAV_REGISTRY, foldedUnder, and resolveActiveHub
as the key symbols.

In `@src/lib/baseball/__tests__/resolve-active-hub.test.ts`:
- Around line 211-220: The cap test in resolve-active-hub.test.ts is incomplete
because it only covers four coach hub tab constants and misses the other coach
hubs exercised in this file. Update the existing describe/it.each block that
references COACH_STATS_TABS, COACH_DEVELOPMENT_TABS, COACH_MANAGEMENT_TABS, and
COACH_MESSAGES_TABS to also include the missing coach tab constants for the
team, recruiting, and academics hubs (the same COACH_*_TABS symbols used by the
hub-specific cases in this test file) so the “every coach hub caps at 3 rendered
subtabs” invariant is actually enforced everywhere.

---

Outside diff comments:
In `@docs/operations/BASEBALLHELM_FEATURE_READINESS_MATRIX.md`:
- Around line 1-112: The readiness-matrix CI check is failing because several
rows still reference closed Owner Issue URLs. Update the affected rows in
BASEBALLHELM_FEATURE_READINESS_MATRIX so each stale issue link is either removed
or replaced with the next real open issue, following the Maintenance rule that
resolved issues must not remain linked. Use the row feature names and issue
groupings in the matrix to locate each stale Owner Issue cell, and verify the
updated table still satisfies the check: only open, valid issue URLs or
intentional N/A entries should remain.

In `@src/app/baseball/`(dashboard)/dashboard/academics/AcademicsClient.tsx:
- Around line 161-172: The edit flow is converting an unset eligibility value
into true, which causes records with no eligibility on file to be overwritten
during unrelated edits. Update startEditing in AcademicsClient to preserve
student.is_eligible as null instead of defaulting it to true, and make sure the
submit path in upsertPlayerAcademics keeps null intact rather than applying a
fallback. If the UI needs a default, add an explicit “not on file” choice
instead of silently coercing the value.

In `@src/app/baseball/`(dashboard)/dashboard/calendar/page.tsx:
- Around line 75-101: The calendar query in the dashboard page is capping
results with `order('start_time', { ascending: true })` and `.limit(500)`, which
can silently drop the most-future events when the 455-day window is crowded.
Update the `eventsResult` fetch to prioritize future events over older past
entries, ideally by splitting the range or ordering so upcoming `start_time`
rows are guaranteed to survive the cap. If a cap is still kept, add a visible
warning/metric when the query truncates results so this loss is not silent.

In `@src/app/baseball/`(dashboard)/dashboard/performance/programs/page.tsx:
- Around line 40-93: The Programs page currently queries lifting data even when
the coach has BaseballHelm manage access but is not linked to the lifting org,
which can cause an empty list with no explanation. In ProgramsPage and
getPrograms, add a lifting-access fallback before querying: if
resolveBaseballLiftingOrg(teamId) returns no org or the user is not onboarded,
surface an onboarding/linked-org state or redirect/fail fast instead of
returning []. Keep the check aligned with the existing gate around
resolveBaseballCapabilities and the lifting-org lookup used by ProgramsPage.

In `@src/app/lifting/actions/player-sessions.ts`:
- Around line 10-22: The BASEBALL_LIFT_PATH constant is placed between import
statements, breaking import grouping and potentially triggering import/first
lint rules. Move BASEBALL_LIFT_PATH below all imports in player-sessions.ts,
keeping the existing symbols like revalidatePath, createClient, fromUntyped, and
resolveMyBaseballAthleteId together at the top before any non-import
declarations.

In `@src/app/lifting/actions/programs.ts`:
- Around line 118-155: Extract the repeated revalidation pair into a shared
helper used by createProgram, deleteProgram, addLiftWeek, addLiftDay,
addLiftSection, and addLiftPrescription so the LAB_PATH and
BASEBALL_PROGRAMS_PATH invalidation stays consistent in one place. Also update
addLiftWeek to revalidate the specific program routes using input.programId,
matching the per-program cache busting already done in updateProgram,
duplicateLiftDay, and duplicateLiftWeek. Use the existing symbols addLiftWeek,
createProgram, deleteProgram, and the path constants to wire this in without
duplicating the calls.

In `@src/components/baseball/documents/DocumentsFairway.tsx`:
- Around line 116-132: The upload CTA JSX is duplicated between the masthead and
empty-state actions in DocumentsFairway, which risks the two buttons drifting
apart. Extract the shared Button config/markup into a helper such as
uploadButton in DocumentsFairway and reuse it at both call sites, passing only
the label text ("Upload" and "Upload document") while keeping
onClick={onUpload}, busy={isUploadingDocument}, and the Upload icon consistent.

In `@src/components/baseball/settings/TeamSettingsClient.tsx`:
- Around line 3-16: The top-of-file comment in TeamSettingsClient is stale
because it still mentions Header as a reused primitive even though the component
no longer imports or uses it. Update that doc comment to match the actual
primitives used in TeamSettingsClient, keeping the wording aligned with the
current imports and JSX so it doesn’t mislead future changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread e2e/baseball-box-score.spec.ts
Comment thread scripts/backfill-baseball-slash-lines.ts Outdated
Comment thread scripts/e2e-supabase-admin.ts
Comment thread src/app/baseball/(dashboard)/_components/breadcrumbs.ts
Comment on lines +542 to +561
const shellLink: ShellLinkComponent = useCallback(
({ href, children, onClick, ...rest }) => (
<Link
href={href}
prefetch
onClick={
isShowcaseCoach && href === SHOWCASE_ORG_HOME_HREF
? () => {
setSelectedTeamId(null);
onClick?.();
}
: onClick
}
{...rest}
>
{children}
</Link>
),
[isShowcaseCoach, setSelectedTeamId],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File outline =="
ast-grep outline "src/app/baseball/(dashboard)/BaseballFairwayShell.tsx" --view expanded || true

echo
echo "== Relevant lines around shellLink =="
sed -n '520,575p' "src/app/baseball/(dashboard)/BaseballFairwayShell.tsx" | cat -n

echo
echo "== Locate useTeams / usePlayerTeams definitions =="
rg -n "function useTeams|const useTeams|export .*useTeams|function usePlayerTeams|const usePlayerTeams|export .*usePlayerTeams" src/app src/hooks src/lib -g '!**/node_modules/**' || true

echo
echo "== File locations for those hooks =="
fd -a "useTeams.*|usePlayerTeams.*" src/app src/hooks src/lib || true

Repository: njrini99-code/helmv3

Length of output: 4145


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== use-teams.ts =="
sed -n '1,220p' src/hooks/use-teams.ts | cat -n

echo
echo "== use-player-teams.ts =="
sed -n '1,220p' src/hooks/use-player-teams.ts | cat -n

Repository: njrini99-code/helmv3

Length of output: 6115


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== team store definition =="
sed -n '1,260p' src/stores/team-store.ts | cat -n

echo
echo "== any custom equality or re-created actions =="
rg -n "setSelectedTeamId|setTeams|setIsLoading|getSelectedTeam|create\\(" src/stores/team-store.ts src/stores -g '!**/node_modules/**' || true

Repository: njrini99-code/helmv3

Length of output: 3233


Forward the click event in the showcase link handler
src/app/baseball/(dashboard)/BaseballFairwayShell.tsx:542-561 — the showcase branch calls onClick?.() without the MouseEvent, while the fallback path forwards it. Pass e through so link consumers keep modifier-key/default-prevented behavior intact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/BaseballFairwayShell.tsx around lines 542 -
561, The showcase link handler in BaseballFairwayShell’s shellLink callback
drops the click event when isShowcaseCoach is true and href matches
SHOWCASE_ORG_HOME_HREF. Update the Link onClick branch to accept and forward the
MouseEvent to onClick after calling setSelectedTeamId(null), matching the
fallback path so consumers preserve modifier-key and default-prevented behavior.

Comment thread src/components/baseball/stats-center/StatsCenterClient.tsx
Comment thread src/components/baseball/travel/TravelClient.tsx
Comment thread src/lib/baseball/__tests__/nav-manifest.test.ts
Comment thread src/lib/baseball/__tests__/nav-manifest.test.ts
Comment thread src/lib/baseball/__tests__/resolve-active-hub.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment thread src/app/baseball/(dashboard)/dashboard/calendar/page.tsx
Comment thread src/app/baseball/(dashboard)/dashboard/lift/_lift-athlete-context.ts Outdated
Comment thread src/app/baseball/(dashboard)/dashboard/lift/[sessionId]/page.tsx
Comment thread src/app/baseball/(dashboard)/dashboard/lift/page.tsx Outdated
Comment on lines +14 to +21

// Baseball Wave C renders these same helm_lifting_sessions /
// helm_lifting_readiness_checkins reads at /baseball/dashboard/lift and
// /baseball/dashboard/lift/[sessionId] (see src/app/baseball/(dashboard)/
// dashboard/lift/page.tsx + [sessionId]/page.tsx) — bust both alongside
// '/lifting/dashboard/lift' below so a baseball athlete's own lift portal
// reflects a checkin/set/status mutation immediately.
const BASEBALL_LIFT_PATH = '/baseball/dashboard/lift';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sport-specific path baked into the generic lifting actions module.

This file also resolves golf athletes (golf_players branch in getMyLiftToday, lines 60-85), but only a baseball consumer path gets a constant + revalidation call. Every future consuming portal (golf's own lift view, or a third one) means another hardcoded *_LIFT_PATH const and another 1-2 lines duplicated across submitLiftReadiness, startMySession, logMySetResult, completeMySession. Consider centralizing "extra portal paths to bust on mutation" as a small array/helper (e.g. revalidateLiftingPortals(sessionId?: string)) so adding a sport doesn't require touching every mutation function again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/lifting/actions/player-sessions.ts` around lines 14 - 21, The lifting
actions module is hardcoding a sport-specific portal path, which will keep
duplicating revalidation logic as more sports are added. Refactor the mutation
flow in player-sessions.ts by centralizing the extra portal paths into a small
helper or shared array, and have submitLiftReadiness, startMySession,
logMySetResult, and completeMySession call that helper instead of directly
referencing BASEBALL_LIFT_PATH. Keep the generic lifting path plus any
sport-specific paths (like baseball and golf) in one place so future portals
only need a single update.

Comment thread src/app/lifting/actions/sessions.ts Outdated
Comment thread src/components/baseball/calendar/CalendarFairway.tsx
Comment thread src/components/baseball/performance/PerformanceDashboardClient.tsx
Comment thread src/components/lifting/programs/ProgramListClient.tsx
…s across 5 clusters)

Root-cause fixes for every Major + correctness finding:
- backfill-baseball-slash-lines: paginate past the PostgREST 1000-row cap
  (both the aggregates scan and per-player stats fetch)
- games.ts: cross-New-Year season_year bug — parse year from the date string
- signals.ts + performance page/client: team-local "today"
  (todayIsoInTz/resolveTeamTimezone) instead of server-UTC
- EditGameModal: optional fields clearable (null not undefined); off-palette
  purple-*/red-* → canonical tokens
- GamesList: honor seeded initialGames/initialRecord (no double fetch)
- lift/_lift-athlete-context: try all active memberships before null;
  errors → logServerError
- stats/games + performance/{groups,live}: DB errors no longer masquerade
  as empty states
- DevPlanClient: stuck-spinner no-player path resolved; parseLocalDate for
  header dates
- scouting: honest null-context failure state (no fabricated NavContext)
- breadcrumbs: settings subpages no longer collapse to hub label
- EventsClient: error state and first-event CTA now mutually exclusive
- e2e box-score: teardown guarded per its own invariant + deduped helper
- lifting actions: accurate guard messages, shared revalidation helper,
  missing member add/remove revalidation tests, trailing-slash guard
- nav tests: exhaustive PROGRAM_TYPES from canonical array; folded-route
  hub assertions; full coach-hub cap coverage
- + a11y/copy/token nits per review

Deferred by design (mission plan W4): shared header-primitive extractions —
those headers are replaced wholesale by the Living Annual masthead migration
(docs/audits/PRODUCTION_READINESS_MISSION_2026-07-09.md). Readiness-matrix
closed-issue cleanup lands with the W7 full matrix refresh.

Also adds docs/audits/PRODUCTION_READINESS_MISSION_2026-07-09.md (mission
plan: target 8/8/8/8 nav IA + waves W0-W10 from the 30-agent discovery).

Gates: tsc=0, eslint --max-warnings 0 =0, unit 4505 passed / 0 failed.

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

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

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

@njrini99-code

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Pushed 1297b57 addressing the review: 53 root-cause fixes across all Major + correctness findings (pagination cap, cross-New-Year season bug, team-local dates, clearable optional fields, error-vs-empty honesty, stuck spinner, breadcrumb collapse, teardown guard, token violations, test-coverage gaps — full breakdown in the commit message).

Intentionally deferred, tracked in docs/audits/PRODUCTION_READINESS_MISSION_2026-07-09.md (added in this push): shared header-primitive extractions — those headers are replaced wholesale by the Living Annual masthead migration (wave W4); the readiness-matrix closed-issue cleanup lands with the W7 full matrix refresh.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
git log --oneline -15
echo "---"
git show --stat 1297b579 | head -100
echo "---commit message---"
git show -s --format=%B 1297b579

Length of output: 9597


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== backfill-baseball-slash-lines.ts pagination ==="
sed -n '1,220p' scripts/backfill-baseball-slash-lines.ts

echo "=== games.ts season_year ==="
rg -n "season_year|game_date" src/app/baseball/actions/games.ts | head -40

echo "=== EditGameModal clearable fields ==="
sed -n '1,220p' src/components/baseball/games/EditGameModal.tsx

echo "=== GamesList double fetch ==="
sed -n '1,80p' src/components/baseball/games/GamesList.tsx

echo "=== lift athlete context ==="
sed -n '1,140p' 'src/app/baseball/(dashboard)/dashboard/lift/_lift-athlete-context.ts'

Length of output: 27823


The verification confirms the claimed fixes are substantively implemented and root-caused, not superficial patches. Spot-checking the key items:

  • Pagination capscripts/backfill-baseball-slash-lines.ts:70-80 (fetchAllRows): properly pages via .range(from, to) in PAGE=1000 chunks for both the aggregates scan and the per-player stats fetch, matching the described PostgREST cap fix.
  • Cross-New-Year season bugsrc/app/baseball/actions/games.ts:729: seasonYear is now parsed from game.game_date.slice(0, 4) instead of new Date().getFullYear(), correctly fixing a Dec 31 game recalculated in January.
  • Clearable optional fieldssrc/components/baseball/games/EditGameModal.tsx:55-61: emptied optional fields (opponent_name, location, notes, weather) now send null instead of undefined, with an inline comment explaining why updateGame would otherwise silently retain stale values.
  • GamesList double-fetch/stalenesssrc/components/baseball/games/GamesList.tsx:71-79: adds a useEffect syncing initialGames/initialRecord/initialError into client state so a revalidated RSC payload from router.refresh() actually reaches the UI.
  • Lift athlete context (JUCO + Showcase dual-team)_lift-athlete-context.ts:39-89: resolvePlayerTeamIds now returns all active memberships and resolvePlayerLiftAthleteContext iterates through each before giving up, with logServerError on query failure instead of silent null. This is a legitimate root-cause fix (dual-team players previously could get incorrectly stuck on the first membership's org if it had no seeded Lift Lab athlete).

These are root-cause fixes, not surface patches — good work. The deferred items (shared header-primitive extraction to W4 Living Annual masthead migration, readiness-matrix cleanup to W7) are reasonable scope calls given they're superseded by planned follow-on waves rather than being ignored.

I'll kick off a fresh review pass over 1297b57 to catch anything the above spot-check didn't cover (e.g., the signals.ts team-local-today change, the lifting/actions revalidation helper, and the new nav/breadcrumb test assertions).

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (16)
src/app/baseball/(dashboard)/dashboard/performance/page.tsx (1)

163-168: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Readiness lookback window still computed in UTC — same bug class this diff exists to fix.

L165-168 builds fromDate for the 7-day readiness check-in query using new Date() (server UTC), while L238 correctly switches to a team-local today for the client. Both variables are named today but are different types/semantics (Date vs. team-local ISO string) declared in overlapping scope — easy to conflate later.

Net effect: for a team behind UTC, the readiness query's 7-day window can silently miss/include a day at the UTC boundary — exactly the class of bug todayIsoInTz(resolveTeamTimezone(...)) was introduced to eliminate, just not applied here.

🐛 Proposed fix — compute team-local `today` once, reuse for the readiness window
-  let readiness: BaseballReadinessSummary[] = [];
-  if (canViewReadiness && liftCtx) {
-    const today = new Date();
-    const sevenDaysAgo = new Date(today);
-    sevenDaysAgo.setDate(today.getDate() - 7);
-    const fromDate = sevenDaysAgo.toISOString().slice(0, 10);
+  // Team-local "today" so both the readiness lookback window and the client's
+  // check-in day math can't drift across the UTC boundary for teams west of
+  // Greenwich.
+  const today = todayIsoInTz(await resolveTeamTimezone(supabase, teamId));
+
+  let readiness: BaseballReadinessSummary[] = [];
+  if (canViewReadiness && liftCtx) {
+    const sevenDaysAgo = new Date(`${today}T00:00:00Z`);
+    sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 7);
+    const fromDate = sevenDaysAgo.toISOString().slice(0, 10);

And drop the now-redundant const today = todayIsoInTz(...) block at L236-239 since it's hoisted above.

Also applies to: 236-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/performance/page.tsx around lines 163
- 168, The readiness 7-day lookback window is still being derived from server
UTC instead of the team’s local timezone, causing boundary-day mismatches. In
dashboard/performance/page.tsx, update the readiness query setup around the
canViewReadiness/liftCtx block to use the same team-local today value produced
by todayIsoInTz(resolveTeamTimezone(...)) that is used later for the client
view, and reuse that single value when computing fromDate. Remove the redundant
later today declaration so there is one clear today value with consistent
semantics across the readiness logic.
src/app/lifting/actions/programs.ts (2)

219-224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

updateProgram was not actually migrated onto the new helper — despite the changelog claiming it was.

The line-range change description for this block says updateProgram now calls revalidateProgramPaths(programId), but the code still hand-rolls all 4 revalidatePath() calls verbatim — the exact duplication the new helper exists to eliminate. Functionally equivalent today, but it defeats the DRY purpose of the refactor and will silently drift the moment revalidateProgramPaths gains another path (e.g. a future BASEBALL_LIVE_PATH bust on status change).

♻️ Adopt the helper here too
-    revalidatePath(`${LAB_PATH}/${programId}`);
-    revalidatePath(LAB_PATH);
-    revalidatePath(`${BASEBALL_PROGRAMS_PATH}/${programId}`);
-    revalidatePath(BASEBALL_PROGRAMS_PATH);
+    revalidateProgramPaths(programId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/lifting/actions/programs.ts` around lines 219 - 224, `updateProgram`
is still duplicating cache invalidation instead of using the shared helper.
Update the `updateProgram` flow in `programs.ts` to call
`revalidateProgramPaths(programId)` after a successful save, matching the
refactor used elsewhere, and remove the four inline `revalidatePath()` calls so
the invalidation logic stays centralized and consistent with future path
additions.

601-614: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

publishProgram mutates program status but never revalidates the program's own detail route.

L603-607 flips prog.status from draft to active on first publish, but the revalidation block (L609-613) only busts the list routes (LAB_PATH, /lifting/dashboard/sessions, BASEBALL_PROGRAMS_PATH, BASEBALL_LIVE_PATH, BASEBALL_LIFT_PATH) — never ${LAB_PATH}/${programId} or ${BASEBALL_PROGRAMS_PATH}/${programId}. A coach who publishes from the program's own detail/builder page will keep seeing a stale draft status in the cached RSC payload until an unrelated navigation elsewhere triggers a refetch. updateProgram/duplicateLiftDay/duplicateLiftWeek all correctly bust the per-program detail route on status-affecting writes — publishProgram is the one mutation here that skips it.

As per path instructions, src/app/**/actions/**/*.ts requires "Mutations must revalidatePath() the affected routes."

🐛 Bust the program detail route on publish
-    revalidatePath(LAB_PATH);
-    revalidatePath('/lifting/dashboard/sessions');
-    revalidatePath(BASEBALL_PROGRAMS_PATH);
-    revalidatePath(BASEBALL_LIVE_PATH);
-    revalidatePath(BASEBALL_LIFT_PATH);
+    revalidateProgramPaths(programId);
+    revalidatePath('/lifting/dashboard/sessions');
+    revalidatePath(BASEBALL_LIVE_PATH);
+    revalidatePath(BASEBALL_LIFT_PATH);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/lifting/actions/programs.ts` around lines 601 - 614, publishProgram
updates the program from draft to active but only revalidates list routes, so
add revalidation for the program’s own detail page after the status update. In
the publishProgram flow, ensure revalidatePath is called for the affected
per-program route(s) derived from LAB_PATH and BASEBALL_PROGRAMS_PATH using
programId, alongside the existing list-route invalidations, so the cached
builder/detail view reflects the new status immediately.

Source: Path instructions

src/app/baseball/actions/calendar.ts (1)

182-224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No zod validation on CreateEventInput before it reaches Supabase.

Per path instructions for src/app/**/actions/**/*.ts: "Validate every input with zod before passing to Supabase." input.title, input.eventType, input.startDate, etc. flow straight into the fromUntyped(...).insert(...) call with no schema validation — an unvalidated eventType string, for instance, can silently violate whatever CHECK constraint baseball_events.event_type has and surface as a raw sanitized DB error instead of a clean 400-style validation message. Pre-existing in this file (not introduced by this diff), but since the function is touched here, it's a reasonable point to add a zod schema (z.object({ title: z.string().min(1), eventType: z.enum([...]), startDate: z.string(), ... })) and parse input at the top of the action.

Based on path instructions: "Server action files. Every exported function MUST start with auth check... Validate every input with zod before passing to Supabase."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/actions/calendar.ts` around lines 182 - 224, The
CreateEventInput action currently passes raw user input straight into the
Supabase insert without zod validation, which violates the action input
contract. Add a zod schema at the top of this exported action in
createEvent/CreateEventInput flow, parse input before any use, and validate
fields like title, eventType, startDate, startTime, endDate, timezoneOffset, and
ids before calling fromUntyped(...).insert(...); keep the existing auth check
and only proceed to Supabase after successful schema parsing.

Source: Path instructions

src/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx (1)

34-44: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Any thrown error is silently rendered as "plan not found."

fetchPlan's catch block (L38-41) sets notFound(true) unconditionally regardless of why getDevPlanForCoach threw — a real permissions error, a network timeout, or an RLS denial gets the exact same "This development plan could not be found." copy as an actually-missing plan. This hides real failures from the coach and from anyone triaging a support ticket, and is the same class of error-vs-empty-state conflation the PR description says was fixed elsewhere in this batch — it wasn't fixed here.

🐛 Proposed fix: distinguish real error from not-found
   const [notFound, setNotFound] = useState(false);
+  const [fetchError, setFetchError] = useState<string | null>(null);
   const [isPending, startTransition] = useTransition();
   const [pendingGoalId, setPendingGoalId] = useState<string | null>(null);

   const fetchPlan = useCallback(async () => {
     if (!params?.id) return;
     setLoading(true);

     try {
       const data = await getDevPlanForCoach(params.id);
       setPlan(data);
       setNotFound(false);
+      setFetchError(null);
     } catch (error) {
       console.error('Error fetching dev plan:', error);
       setPlan(null);
-      setNotFound(true);
+      // Only a genuine "no such plan / not visible to this coach" result
+      // should render the not-found state; anything else is a real error.
+      if (error instanceof DevPlanNotFoundError) {
+        setNotFound(true);
+      } else {
+        setFetchError(error instanceof Error ? error.message : 'Could not load this plan.');
+      }
     } finally {
       setLoading(false);
     }
   }, [params?.id]);

Then render a distinct error state (with a retry action) before the notFound || !plan branch, instead of collapsing both into the same message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/dev-plans/[id]/page.tsx around lines
34 - 44, The fetchPlan error handling in the dashboard page is conflating real
failures with a missing plan by unconditionally calling setNotFound(true) in the
catch block. Update the page component around getDevPlanForCoach, fetchPlan, and
the render logic so only an actual “not found” case sets notFound, while other
errors set a separate error state and show a distinct retryable error UI before
the notFound || !plan branch.
src/app/baseball/actions/games.ts (1)

200-219: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Orphaned calendar event when the game insert fails after the event was created.

The new fail-fast at Line 194-196 correctly prevents "a game the coach believes is on the calendar but isn't." The mirror case is unhandled: when create_calendar_event created the event (Line 170-188) but the baseball_games insert errors at Line 219, the event row survives — a calendar event with no backing game. importScheduleImpl already uses the rollback pattern (Line 1763) for exactly this.

Only roll back the event we ourselves created (not a caller-supplied input.event_id).

🐛 Proposed fix
-    if (error) return { success: false, error: sanitizeDbError(error, 'games') };
+    if (error) {
+      // Roll back the calendar event we just created so we don't strand an
+      // event on the calendar with no backing game (mirrors importScheduleImpl).
+      if (input.create_calendar_event && !input.event_id && eventId) {
+        await supabase.from('baseball_events').delete().eq('id', eventId);
+      }
+      return { success: false, error: sanitizeDbError(error, 'games') };
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/actions/games.ts` around lines 200 - 219, The game insert
path in createScheduleGame leaves behind an orphaned calendar event when the
baseball_games insert fails after create_calendar_event succeeds. Update the
create flow around the insert/error handling in games.ts so that, if the insert
returns an error and this request created the event itself, it rolls back that
event before returning the sanitized error. Use the existing rollback pattern
from importScheduleImpl and make sure you only delete the event created in this
function, not any caller-supplied input.event_id.
src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx (1)

62-82: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Week/day badge counts can truncate at the API row cap. src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx:62-77 — both .in() reads are unpaginated, so linked helm_lifting_weeks/helm_lifting_days rows can silently stop at PostgREST’s 1000-row limit and undercount week_count/day_count. Chunk the reads or switch to count-only aggregation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/performance/programs/page.tsx around
lines 62 - 82, The week/day badge counting logic in the programs page can
silently undercount because the `fromUntyped(...).in(...)` queries for
`helm_lifting_weeks` and `helm_lifting_days` are unpaginated and can hit the
PostgREST row cap. Update the counting flow in the programs page so
`weekCountByProgram` and `dayCountByProgram` are built from chunked queries or
count-only aggregation rather than a single bulk fetch, and keep the existing
`programByWeek`/`weeks` handling consistent while doing so.
src/app/baseball/actions/signals.ts (1)

410-419: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a DB-level dedupe constraint for NULL-program lift sessions. src/app/baseball/actions/signals.ts:410-419 only checks (athlete_id, scheduled_date, title) in app code, but supabase/migrations/20260625000020_helm_lifting_data_sessions_readiness.sql:19-29 only enforces UNIQUE (program_assignment_id, athlete_id), which does not protect program_assignment_id IS NULL rows. Add a partial unique index on (athlete_id, scheduled_date, title) for NULL-program sessions and handle the unique-violation path instead of relying on .maybeSingle().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/actions/signals.ts` around lines 410 - 419, The dedupe in
signals.ts is only enforced in app code for NULL-program sessions, so add a
database-level partial unique index for helm_lifting_sessions on athlete_id,
scheduled_date, and title where program_assignment_id IS NULL in the readiness
migration. Then update the conversion flow in signals.ts to rely on that
constraint by catching the unique-violation path instead of using .maybeSingle()
as the only guard, keeping the existing dedupe logic around the
createLiftAssignment/session creation path.
src/app/baseball/(dashboard)/dashboard/lift/page.tsx (1)

12-13: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Apply the same split-query fix in src/app/lifting/(dashboard)/dashboard/lift/page.tsx:103-126. It still uses one combined or(...) query with .limit(20) and a post-filter, so capped results can drop overdue open sessions. Extract the session read into a shared helper to keep both lift pages aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/lift/page.tsx around lines 12 - 13,
The lift dashboard still uses a single combined or(...) query with .limit(20)
and a post-filter, which can omit overdue open sessions when the cap is hit.
Update the `src/app/lifting/(dashboard)/dashboard/lift/page.tsx` flow to use the
same split-query approach as the other lift page by separating the session read
and readiness check reads, then merging results in code. Factor the session
fetch logic into a shared helper so both `page.tsx` implementations stay aligned
and reuse the same overdue-session handling.
src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx (1)

210-231: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

getAssignContext still swallows query failures — inconsistent with the new honest-error handling.

getProgramTree now surfaces DB/RLS failures, but the newly-introduced getAssignContext discards error on both queries (athletes ?? [], groups ?? []). A failed athletes/groups read renders an empty assign roster with no signal, silently blocking the coach from assigning the program. At minimum, log the errors so the failure is observable.

🛠️ Proposed fix (minimal: log on failure)
-  const [{ data: athletes }, { data: groups }] = await Promise.all([
+  const [{ data: athletes, error: athletesError }, { data: groups, error: groupsError }] = await Promise.all([
     fromUntyped(supabase, 'helm_lifting_athletes')
       .select('id, first_name, last_name, position, sport')
       .eq('organization_id', organizationId)
       .eq('team_id', teamId)
       .eq('is_active', true)
       .order('last_name', { ascending: true })
-      .limit(500) as Promise<{ data: Array<Pick<HelmLiftingAthleteRow, 'id' | 'first_name' | 'last_name' | 'position' | 'sport'>> | null }>,
+      .limit(500) as Promise<{ data: Array<Pick<HelmLiftingAthleteRow, 'id' | 'first_name' | 'last_name' | 'position' | 'sport'>> | null; error: unknown }>,
     fromUntyped(supabase, 'helm_lifting_groups')
       .select('id, name, group_type')
       .eq('organization_id', organizationId)
       .eq('team_id', teamId)
       .eq('is_active', true)
       .order('name', { ascending: true })
-      .limit(100) as Promise<{ data: Array<Pick<HelmLiftingGroupRow, 'id' | 'name' | 'group_type'>> | null }>,
+      .limit(100) as Promise<{ data: Array<Pick<HelmLiftingGroupRow, 'id' | 'name' | 'group_type'>> | null; error: unknown }>,
   ]);
+
+  if (athletesError || groupsError) {
+    await logServerError(
+      `[performance/programs] getAssignContext query failed: ${
+        (athletesError as Error)?.message ?? (groupsError as Error)?.message ?? 'unknown'
+      }`,
+      { action: 'baseball.programEditor.getAssignContext', metadata: { organizationId, teamId } },
+    );
+  }

   return { athletes: athletes ?? [], groups: groups ?? [] };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/app/baseball/`(dashboard)/dashboard/performance/programs/[programId]/page.tsx
around lines 210 - 231, `getAssignContext` is still hiding failures from the
athletes/groups queries, so make the error observable instead of returning
silent empty arrays. In `getAssignContext`, inspect the results from the two
`fromUntyped(...).select(...)` calls and, when either query returns an `error`,
log it with enough context to identify whether it came from
`helm_lifting_athletes` or `helm_lifting_groups`. Keep the existing
`getAssignContext` shape, but avoid swallowing the failure without any signal so
the assignment UI can be debugged consistently with `getProgramTree`.
src/app/baseball/(dashboard)/_components/breadcrumbs.ts (1)

1-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unit tests for this module — it's explicitly designed to be unit-tested, and it already had a bug slip through review once.

The header comment (lines 5-8) states this file was split out specifically "so this logic can be unit-tested directly without dragging in the whole shell's client-only dependency graph." No breadcrumbs.test.ts is in this changeset. Given buildBreadcrumbs's hub-vs-subpage label logic was already wrong once (the settings-collapse bug fixed at lines 80-91), lock in the fix with cases like:

  • /dashboard/settingsSettings (hub landing page, best.label used)
  • /dashboard/settings/permissionsPermissions (subpage, toTitle(lastSegment) used)
  • /dashboard/players/<uuid> → parent-based fallback, never the raw UUID
  • /dashboard/players/<uuid>/stats with overrideLabel → override wins
  • a numeric segment case per the sibling comment on isIdShapedSegment

Want me to draft the test file (toTitle/singularize/isIdShapedSegment/buildBreadcrumbs, table-driven) as a follow-up?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/_components/breadcrumbs.ts around lines 1 - 92,
Add a unit test file for breadcrumbs.ts to lock in the pure breadcrumb behavior
that this module was split out for. Cover buildBreadcrumbs, toTitle,
singularize, and isIdShapedSegment with table-driven cases: hub landing pages
should use best.label, deeper subpages should use the trailing segment
title-cased, UUID/numeric segments should never be exposed raw, parent-based
fallback should apply for id-shaped routes, and overrideLabel should win when
present. Reference the existing buildBreadcrumbs and isIdShapedSegment symbols
so the tests stay aligned with the current routing logic.
src/app/baseball/(dashboard)/dashboard/calendar/page.tsx (1)

75-84: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Calendar query is still capped at 500 rows src/app/baseball/(dashboard)/dashboard/calendar/page.tsx:95-101 — teams with >500 events in the 455-day window will silently drop later rows. The repo’s Supabase config already allows 1000 rows, so raise this limit to 1000 or switch to explicit chunking if you need more.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/calendar/page.tsx around lines 75 -
84, The calendar data fetch in the page component is still limited to 500 rows,
which can drop events inside the bounded window. Update the query in the
dashboard calendar page’s event-loading logic to use the repo-supported 1000-row
cap, or replace the hard cap with explicit chunking if the result set may exceed
that; keep the existing window logic in sync with the fetch in the same calendar
page flow.
src/app/baseball/(dashboard)/dashboard/players/[id]/page.tsx (1)

109-133: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Surface season-stat query errors instead of blank stats src/app/baseball/actions/games.ts:1399-1447 ignores Supabase error values and still returns success: true, so src/app/baseball/(dashboard)/dashboard/players/[id]/page.tsx:210-212 can only render null/[] and can’t distinguish “no stats yet” from “stats query failed.” Return { success: false, error } on those query failures; withAdminObserved will then emit the failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/players/[id]/page.tsx around lines
109 - 133, The season-stats fetch in getPlayerSeasonStats is swallowing Supabase
errors and returning success as if the query worked, which makes the player page
in page.tsx render blank stats without knowing it failed. Update the action to
return success: false with the underlying error whenever the stats query fails,
and keep the normal success path only for real results so withAdminObserved can
log the failure and the dashboard can distinguish “no stats” from “query error.”
src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx (1)

123-198: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Residual: post-sessions query errors are still swallowed into a degraded board.

sessionsError now correctly returns { ok: false } (line 97-103), but sessionExercisesError (line 127) and the Promise.all batch errors (athletesError, setResultsError, etc., logged at 191-198) are only logged. If helm_lifting_session_exercises RLS/transient-fails while helm_lifting_sessions succeeds, exerciseIds is [], set-results is skipped, and every athlete renders with exercises: [], current_exercise: null, total_exercises: 0 (line 259-261) — a live board where no coach can see or log a set, indistinguishable from "athletes have no assigned work." That's the same error-vs-empty conflation this PR is standardizing away, applied inconsistently one query later.

At minimum, treat sessionExercisesError like sessionsError (it's the load-bearing query for the write surface):

🛡️ Proposed fix
   await logQueryError('helm_lifting_session_exercises', sessionExercisesError, teamId);
+
+  if (sessionExercisesError) {
+    // Same rationale as sessionsError: a hard failure here yields an
+    // athlete grid with zero loggable exercises — surface it, don't degrade.
+    return { ok: false };
+  }

Consider extending the same treatment to the athletes query in the batch (a null athletes map silently drops names/positions).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/performance/live/page.tsx around
lines 123 - 198, The live board is still collapsing query failures into empty
data, especially for the load-bearing `sessionExercisesError` in the `page.tsx`
data fetch. Update the `load`/query path around `sessionExercisesError` and the
`Promise.all` batch so a failed `helm_lifting_session_exercises` request returns
a non-success state instead of continuing with `exerciseIds = []`; also consider
treating `athletesError` the same way so missing athletes data is not silently
rendered as empty rows. Use the existing `sessionsError` handling pattern and
the identified symbols `sessionExercisesError`, `Promise.all`, and
`logQueryError` to keep the failure visible.
e2e/baseball-box-score.spec.ts (1)

397-415: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

/baseball/dashboard/stats/season is no longer a supported route e2e/baseball-box-score.spec.ts:397-415
page.goto('/baseball/dashboard/stats/season') at :402 still targets a 404. The stats flow now lives at /baseball/dashboard/stats-center (with /baseball/dashboard/stats redirecting there), so this block should be repointed or removed if season stats is no longer intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/baseball-box-score.spec.ts` around lines 397 - 415, The Coach - Season
Stats Dashboard test block is still navigating to a removed route and should be
updated. In the `test.beforeEach` for `Coach - Season Stats Dashboard`, replace
the `page.goto` target from the old season stats path to the supported stats
center route used by the app, and keep the assertions aligned with the new page
content; if the season-specific coverage is no longer valid, remove the `should
display...` and `should show...` tests from this `describe` block instead of
keeping a 404 target.
src/components/baseball/player-profile/PlayerProfileClient.tsx (1)

196-202: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Parse baseball game_date values as local midnight

src/components/baseball/player-profile/PlayerProfileClient.tsx:196-202 uses new Date(iso) for stat.game?.game_date (:1108) and trend labels (:396). baseball_games.game_date is a date column (supabase/migrations/20260527000000_prod_public_baseline.sql:7730), so YYYY-MM-DD values can render a day early in US timezones. Match the existing baseball date pattern used in src/components/baseball/box-score/BoxScoreView.tsx:25 and src/components/baseball/box-score/BoxScoreEntry.tsx:221.

Suggested fix
 function formatDate(iso: string): string {
-  return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
+  const d = /^\d{4}-\d{2}-\d{2}$/.test(iso) ? new Date(`${iso}T00:00:00`) : new Date(iso);
+  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
 }

 function formatShortDate(iso: string): string {
-  return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+  const d = /^\d{4}-\d{2}-\d{2}$/.test(iso) ? new Date(`${iso}T00:00:00`) : new Date(iso);
+  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/baseball/player-profile/PlayerProfileClient.tsx` around lines
196 - 202, The baseball date formatting helpers in PlayerProfileClient treat
YYYY-MM-DD values as UTC, which can shift game_date labels by a day in US
timezones. Update formatDate and formatShortDate to parse baseball game dates as
local midnight using the same pattern already used in BoxScoreView and
BoxScoreEntry, and keep the call sites for stat.game?.game_date and the trend
label formatting unchanged except for using the corrected helpers.
♻️ Duplicate comments (4)
src/app/baseball/actions/calendar.ts (1)

7-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both prior review findings are now fixed.

  • Soft-failure logging: logServerError(..., 'warning') is now called for both the attendance-insert failure (246-254) and the linked-game-insert failure (272-280), so these no longer disappear once the toast is dismissed.
  • Separator bug: warnings.join('; ') (289) no longer garbles concatenated messages when both secondary writes fail.

One item from the prior review is still outstanding: no test asserts createBaseballEvent returns { success: true, warning: '...' } when baseball_event_attendance or baseball_games inserts fail. EventsClient.tsx branches on result.warning, so a regression here (e.g. reverting the join separator, or dropping a logServerError call) wouldn't be caught by tsc/lint alone.

Also applies to: 73-84, 225-233, 242-260, 271-290

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/actions/calendar.ts` at line 7, Add a regression test for
createBaseballEvent to verify that when either the baseball_event_attendance
insert or the baseball_games insert fails, the function still returns { success:
true, warning: '...' } and preserves the warning text used by EventsClient.tsx.
Cover both single-failure and dual-failure cases so the warning aggregation path
(including warnings.join('; ')) and the logServerError calls remain protected
from regressions.
src/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx (1)

90-102: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Header block still copy-pasted 4x — not extracted.

The focus-visible ring was added to all four Link instances (good — addresses half of the prior ask), but the 13-line back-link/title/subtitle block at lines 90-102, 113-125, 140-152, and 168-180 remains a verbatim quadruple copy-paste. Any future wording/behavior change needs 4 synchronized edits.

Extract a local DevPlanHeader component and render it in each of the 4 branches — same fix proposed previously, still applicable to this revision.

Also applies to: 113-125, 140-152, 168-180

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/dev-plans/[id]/page.tsx around lines
90 - 102, The back-link/title/subtitle header block in the dev plan page is
still duplicated across the four conditional branches, so extract it into a
local DevPlanHeader component and reuse it everywhere. Move the repeated Link +
heading markup into that component and render it from each branch in page.tsx,
keeping the existing focus-visible styles intact.
src/app/baseball/(dashboard)/dashboard/academics/AcademicsClient.tsx (1)

234-237: 📐 Maintainability & Code Quality | 🔵 Trivial

Header duplication still unresolved — text drift is fixed, but the root cause isn't.

The previous review flagged this exact 4x-copy-pasted header block because the text had drifted between states. That drift is now fixed (all four now say "Track student-athlete academic progress and eligibility"), but the underlying problem — no shared header component, four independent copies of the same markup — is still there (the Header import was removed and replaced by these inline copies). The next edit to this copy will silently re-introduce drift the same way the last one did.

♻️ Extract a shared header (still applicable)
+function AcademicsHeader() {
+  return (
+    <div className="border-b border-warm-200/60 px-6 pb-5 pt-6 lg:px-8 lg:pt-8">
+      <h1 className="text-h2 font-semibold text-warm-900">Academics</h1>
+      <p className="mt-1 text-body-sm text-warm-500">
+        Track student-athlete academic progress and eligibility
+      </p>
+    </div>
+  );
+}

Replace each of the 4 inline blocks (234-237, 253-256, 310-313, 368-371) with <AcademicsHeader />.

Also applies to: 253-256, 310-313, 368-371

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/academics/AcademicsClient.tsx around
lines 234 - 237, The Academics page still has four duplicated inline header
blocks in AcademicsClient instead of using a shared component, which leaves the
copy prone to drift. Restore a reusable header by extracting the repeated markup
into AcademicsHeader and use it in AcademicsClient for each of the four existing
header sections, replacing the inline h1/p blocks with the shared component so
there is one source of truth.
src/app/baseball/(dashboard)/dashboard/program/page.tsx (1)

214-217: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Header block is still duplicated 3x — extraction not applied.

The diff only fixed the flex-wrap on line 251; the previously-flagged copy-paste of the border-b … px-6 pb-5 pt-6 … header shell across the coach-access, no-org, and main branches (lines 214-217, 232-235, 251-262) is unchanged. Same drift risk as before: a future style tweak has to be applied in 3 places, and a 4th branch could easily reintroduce the missing-flex-wrap bug this PR just fixed on one of the three.

♻️ Proposed local extraction
+function ProgramPageHeader({
+  subtitle,
+  action,
+}: { subtitle: string; action?: React.ReactNode }) {
+  return (
+    <div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-3 border-b border-warm-200/60 px-6 pb-5 pt-6 lg:px-8 lg:pt-8">
+      <div>
+        <h1 className="text-h2 font-semibold text-warm-900">Program Profile</h1>
+        <p className="mt-1 text-body-sm text-warm-500">{subtitle}</p>
+      </div>
+      {action}
+    </div>
+  );
+}

Then replace each inline <div> block at 214-217, 232-235, and 251-262 with <ProgramPageHeader subtitle="..." action={...} />.

Also applies to: 232-235, 251-262

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/program/page.tsx around lines 214 -
217, The `ProgramPage` header shell is still duplicated across the coach-access,
no-org, and main branches instead of being extracted. Move the shared `border-b
... px-6 pb-5 pt-6 ...` layout into a reusable `ProgramPageHeader` component in
`page.tsx` and replace each inline header block with that component, passing the
appropriate subtitle and action/content for each branch so future style changes
only need one edit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/baseball/`(dashboard)/_components/breadcrumbs.ts:
- Around line 89-91: The breadcrumb label logic in breadcrumbs.ts still uses a
non-null assertion on best when computing the label. Refactor the
isHubLandingPage/label flow in the breadcrumbs builder so the best !== null
check is kept in the same expression that accesses best.label, removing best!
entirely while preserving the same Dashboard and overrideLabel behavior.

In `@src/app/baseball/`(dashboard)/dashboard/lift/_lift-athlete-context.ts:
- Around line 38-44: The doc comment in _lift-athlete-context overstates the
dual-membership case as “JUCO + Showcase,” which conflicts with the Baseball
Product user-type matrix. Update the comment above the membership lookup logic
to describe the correct dual-team-capable player types (High School with
optional Showcase, Showcase with optional High School) while keeping the
explanation for returning the full ordered list and iterating through
memberships in place.

In `@src/app/baseball/`(dashboard)/dashboard/scouting/page.tsx:
- Around line 78-96: The `SectionMasthead` plus descriptive paragraph is
duplicated in both the null-context branch and the main return in
`ScoutingPage`; hoist that shared JSX into a local const and reuse it in both
places so the copy stays in sync. Update the `ScoutingPage` render path to
reference the shared masthead content instead of repeating the same block in
each branch.

In `@src/app/baseball/`(dashboard)/dashboard/stats/games/page.tsx:
- Around line 53-60: The season-record fetch failure in the dashboard page is
being ignored, while GamesList only understands the games-level initialError
banner. Update the page component that builds the props for GamesList to carry a
separate record-specific error state such as initialRecordError/recordError
instead of reusing initialError, and wire GamesList to render or otherwise
surface that distinct season-record failure without showing “Couldn't load
games.” Use the existing symbols recordResult, initialError, and GamesList to
locate the affected prop flow.
- Around line 45-60: The season-record fetch failure is only being logged in the
page component and never reaches the UI, so `recordResult` errors are hidden
while `gamesResult` errors are shown through `initialError`. Update `page.tsx`
in the `GamesPage` flow to carry a separate record-error state (or merge it into
the existing error handling) and pass it into the header/list rendering path
alongside `GamesList`, using the existing `getTeamSeasonRecord`, `getTeamGames`,
and `initialError` wiring so DB/RLS failures surface visibly instead of being
silently ignored.

In `@src/components/baseball/settings/PhilosophySettingsClient.tsx`:
- Line 247: The settings route is hardcoded in multiple places in
PhilosophySettingsClient, so extract a shared SETTINGS_PATH constant for
'/baseball/dashboard/settings' and use it in the router.push call and the other
matching reference to prevent drift if the route changes.

---

Outside diff comments:
In `@e2e/baseball-box-score.spec.ts`:
- Around line 397-415: The Coach - Season Stats Dashboard test block is still
navigating to a removed route and should be updated. In the `test.beforeEach`
for `Coach - Season Stats Dashboard`, replace the `page.goto` target from the
old season stats path to the supported stats center route used by the app, and
keep the assertions aligned with the new page content; if the season-specific
coverage is no longer valid, remove the `should display...` and `should show...`
tests from this `describe` block instead of keeping a 404 target.

In `@src/app/baseball/`(dashboard)/_components/breadcrumbs.ts:
- Around line 1-92: Add a unit test file for breadcrumbs.ts to lock in the pure
breadcrumb behavior that this module was split out for. Cover buildBreadcrumbs,
toTitle, singularize, and isIdShapedSegment with table-driven cases: hub landing
pages should use best.label, deeper subpages should use the trailing segment
title-cased, UUID/numeric segments should never be exposed raw, parent-based
fallback should apply for id-shaped routes, and overrideLabel should win when
present. Reference the existing buildBreadcrumbs and isIdShapedSegment symbols
so the tests stay aligned with the current routing logic.

In `@src/app/baseball/`(dashboard)/dashboard/calendar/page.tsx:
- Around line 75-84: The calendar data fetch in the page component is still
limited to 500 rows, which can drop events inside the bounded window. Update the
query in the dashboard calendar page’s event-loading logic to use the
repo-supported 1000-row cap, or replace the hard cap with explicit chunking if
the result set may exceed that; keep the existing window logic in sync with the
fetch in the same calendar page flow.

In `@src/app/baseball/`(dashboard)/dashboard/dev-plans/[id]/page.tsx:
- Around line 34-44: The fetchPlan error handling in the dashboard page is
conflating real failures with a missing plan by unconditionally calling
setNotFound(true) in the catch block. Update the page component around
getDevPlanForCoach, fetchPlan, and the render logic so only an actual “not
found” case sets notFound, while other errors set a separate error state and
show a distinct retryable error UI before the notFound || !plan branch.

In `@src/app/baseball/`(dashboard)/dashboard/lift/page.tsx:
- Around line 12-13: The lift dashboard still uses a single combined or(...)
query with .limit(20) and a post-filter, which can omit overdue open sessions
when the cap is hit. Update the
`src/app/lifting/(dashboard)/dashboard/lift/page.tsx` flow to use the same
split-query approach as the other lift page by separating the session read and
readiness check reads, then merging results in code. Factor the session fetch
logic into a shared helper so both `page.tsx` implementations stay aligned and
reuse the same overdue-session handling.

In `@src/app/baseball/`(dashboard)/dashboard/performance/live/page.tsx:
- Around line 123-198: The live board is still collapsing query failures into
empty data, especially for the load-bearing `sessionExercisesError` in the
`page.tsx` data fetch. Update the `load`/query path around
`sessionExercisesError` and the `Promise.all` batch so a failed
`helm_lifting_session_exercises` request returns a non-success state instead of
continuing with `exerciseIds = []`; also consider treating `athletesError` the
same way so missing athletes data is not silently rendered as empty rows. Use
the existing `sessionsError` handling pattern and the identified symbols
`sessionExercisesError`, `Promise.all`, and `logQueryError` to keep the failure
visible.

In `@src/app/baseball/`(dashboard)/dashboard/performance/page.tsx:
- Around line 163-168: The readiness 7-day lookback window is still being
derived from server UTC instead of the team’s local timezone, causing
boundary-day mismatches. In dashboard/performance/page.tsx, update the readiness
query setup around the canViewReadiness/liftCtx block to use the same team-local
today value produced by todayIsoInTz(resolveTeamTimezone(...)) that is used
later for the client view, and reuse that single value when computing fromDate.
Remove the redundant later today declaration so there is one clear today value
with consistent semantics across the readiness logic.

In
`@src/app/baseball/`(dashboard)/dashboard/performance/programs/[programId]/page.tsx:
- Around line 210-231: `getAssignContext` is still hiding failures from the
athletes/groups queries, so make the error observable instead of returning
silent empty arrays. In `getAssignContext`, inspect the results from the two
`fromUntyped(...).select(...)` calls and, when either query returns an `error`,
log it with enough context to identify whether it came from
`helm_lifting_athletes` or `helm_lifting_groups`. Keep the existing
`getAssignContext` shape, but avoid swallowing the failure without any signal so
the assignment UI can be debugged consistently with `getProgramTree`.

In `@src/app/baseball/`(dashboard)/dashboard/performance/programs/page.tsx:
- Around line 62-82: The week/day badge counting logic in the programs page can
silently undercount because the `fromUntyped(...).in(...)` queries for
`helm_lifting_weeks` and `helm_lifting_days` are unpaginated and can hit the
PostgREST row cap. Update the counting flow in the programs page so
`weekCountByProgram` and `dayCountByProgram` are built from chunked queries or
count-only aggregation rather than a single bulk fetch, and keep the existing
`programByWeek`/`weeks` handling consistent while doing so.

In `@src/app/baseball/`(dashboard)/dashboard/players/[id]/page.tsx:
- Around line 109-133: The season-stats fetch in getPlayerSeasonStats is
swallowing Supabase errors and returning success as if the query worked, which
makes the player page in page.tsx render blank stats without knowing it failed.
Update the action to return success: false with the underlying error whenever
the stats query fails, and keep the normal success path only for real results so
withAdminObserved can log the failure and the dashboard can distinguish “no
stats” from “query error.”

In `@src/app/baseball/actions/calendar.ts`:
- Around line 182-224: The CreateEventInput action currently passes raw user
input straight into the Supabase insert without zod validation, which violates
the action input contract. Add a zod schema at the top of this exported action
in createEvent/CreateEventInput flow, parse input before any use, and validate
fields like title, eventType, startDate, startTime, endDate, timezoneOffset, and
ids before calling fromUntyped(...).insert(...); keep the existing auth check
and only proceed to Supabase after successful schema parsing.

In `@src/app/baseball/actions/games.ts`:
- Around line 200-219: The game insert path in createScheduleGame leaves behind
an orphaned calendar event when the baseball_games insert fails after
create_calendar_event succeeds. Update the create flow around the insert/error
handling in games.ts so that, if the insert returns an error and this request
created the event itself, it rolls back that event before returning the
sanitized error. Use the existing rollback pattern from importScheduleImpl and
make sure you only delete the event created in this function, not any
caller-supplied input.event_id.

In `@src/app/baseball/actions/signals.ts`:
- Around line 410-419: The dedupe in signals.ts is only enforced in app code for
NULL-program sessions, so add a database-level partial unique index for
helm_lifting_sessions on athlete_id, scheduled_date, and title where
program_assignment_id IS NULL in the readiness migration. Then update the
conversion flow in signals.ts to rely on that constraint by catching the
unique-violation path instead of using .maybeSingle() as the only guard, keeping
the existing dedupe logic around the createLiftAssignment/session creation path.

In `@src/app/lifting/actions/programs.ts`:
- Around line 219-224: `updateProgram` is still duplicating cache invalidation
instead of using the shared helper. Update the `updateProgram` flow in
`programs.ts` to call `revalidateProgramPaths(programId)` after a successful
save, matching the refactor used elsewhere, and remove the four inline
`revalidatePath()` calls so the invalidation logic stays centralized and
consistent with future path additions.
- Around line 601-614: publishProgram updates the program from draft to active
but only revalidates list routes, so add revalidation for the program’s own
detail page after the status update. In the publishProgram flow, ensure
revalidatePath is called for the affected per-program route(s) derived from
LAB_PATH and BASEBALL_PROGRAMS_PATH using programId, alongside the existing
list-route invalidations, so the cached builder/detail view reflects the new
status immediately.

In `@src/components/baseball/player-profile/PlayerProfileClient.tsx`:
- Around line 196-202: The baseball date formatting helpers in
PlayerProfileClient treat YYYY-MM-DD values as UTC, which can shift game_date
labels by a day in US timezones. Update formatDate and formatShortDate to parse
baseball game dates as local midnight using the same pattern already used in
BoxScoreView and BoxScoreEntry, and keep the call sites for stat.game?.game_date
and the trend label formatting unchanged except for using the corrected helpers.

---

Duplicate comments:
In `@src/app/baseball/`(dashboard)/dashboard/academics/AcademicsClient.tsx:
- Around line 234-237: The Academics page still has four duplicated inline
header blocks in AcademicsClient instead of using a shared component, which
leaves the copy prone to drift. Restore a reusable header by extracting the
repeated markup into AcademicsHeader and use it in AcademicsClient for each of
the four existing header sections, replacing the inline h1/p blocks with the
shared component so there is one source of truth.

In `@src/app/baseball/`(dashboard)/dashboard/dev-plans/[id]/page.tsx:
- Around line 90-102: The back-link/title/subtitle header block in the dev plan
page is still duplicated across the four conditional branches, so extract it
into a local DevPlanHeader component and reuse it everywhere. Move the repeated
Link + heading markup into that component and render it from each branch in
page.tsx, keeping the existing focus-visible styles intact.

In `@src/app/baseball/`(dashboard)/dashboard/program/page.tsx:
- Around line 214-217: The `ProgramPage` header shell is still duplicated across
the coach-access, no-org, and main branches instead of being extracted. Move the
shared `border-b ... px-6 pb-5 pt-6 ...` layout into a reusable
`ProgramPageHeader` component in `page.tsx` and replace each inline header block
with that component, passing the appropriate subtitle and action/content for
each branch so future style changes only need one edit.

In `@src/app/baseball/actions/calendar.ts`:
- Line 7: Add a regression test for createBaseballEvent to verify that when
either the baseball_event_attendance insert or the baseball_games insert fails,
the function still returns { success: true, warning: '...' } and preserves the
warning text used by EventsClient.tsx. Cover both single-failure and
dual-failure cases so the warning aggregation path (including warnings.join(';
')) and the logServerError calls remain protected from regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5acffa08-3bea-4b94-a9f3-671abc9a573b

📥 Commits

Reviewing files that changed from the base of the PR and between da3d420 and 1297b57.

📒 Files selected for processing (51)
  • docs/audits/PRODUCTION_READINESS_MISSION_2026-07-09.md
  • e2e/baseball-box-score.spec.ts
  • scripts/backfill-baseball-slash-lines.ts
  • scripts/e2e-supabase-admin.ts
  • src/app/baseball/(dashboard)/_components/breadcrumbs.ts
  • src/app/baseball/(dashboard)/dashboard/academics/AcademicsClient.tsx
  • src/app/baseball/(dashboard)/dashboard/calendar/page.tsx
  • src/app/baseball/(dashboard)/dashboard/dev-plan/DevPlanClient.tsx
  • src/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/documents/page.tsx
  • src/app/baseball/(dashboard)/dashboard/events/EventsClient.tsx
  • src/app/baseball/(dashboard)/dashboard/lift/[sessionId]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/lift/_lift-athlete-context.ts
  • src/app/baseball/(dashboard)/dashboard/lift/page.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/builder/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/groups/page.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/page.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx
  • src/app/baseball/(dashboard)/dashboard/players/[id]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/program/page.tsx
  • src/app/baseball/(dashboard)/dashboard/readiness/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/scouting/page.tsx
  • src/app/baseball/(dashboard)/dashboard/stats/games/page.tsx
  • src/app/baseball/actions/__tests__/player-today-lift-timezone.test.ts
  • src/app/baseball/actions/calendar.ts
  • src/app/baseball/actions/games.ts
  • src/app/baseball/actions/signals.ts
  • src/app/lifting/actions/__tests__/groups.test.ts
  • src/app/lifting/actions/__tests__/player-sessions.test.ts
  • src/app/lifting/actions/__tests__/sessions.test.ts
  • src/app/lifting/actions/groups.ts
  • src/app/lifting/actions/programs.ts
  • src/app/lifting/actions/sessions.ts
  • src/components/baseball/calendar/CalendarFairway.tsx
  • src/components/baseball/documents/DocumentsFairway.tsx
  • src/components/baseball/games/EditGameModal.tsx
  • src/components/baseball/games/GameDetailHeader.tsx
  • src/components/baseball/games/GamesList.tsx
  • src/components/baseball/player-profile/PlayerProfileClient.tsx
  • src/components/baseball/settings/ImportSourcesClient.tsx
  • src/components/baseball/settings/PhilosophySettingsClient.tsx
  • src/components/baseball/stats-center/StatsCenterClient.tsx
  • src/components/baseball/travel/TravelClient.tsx
  • src/components/lifting/programs/ProgramListClient.tsx
  • src/lib/baseball/__tests__/nav-manifest.test.ts
  • src/lib/baseball/__tests__/resolve-active-hub.test.ts
  • src/lib/types/index.ts
  • src/test/coachhelm/v3/chat-tools-visibility.test.ts
  • supabase/migrations/20260708141900_baseball_event_source_columns_converge.sql
💤 Files with no reviewable changes (1)
  • src/app/baseball/(dashboard)/dashboard/documents/page.tsx

Comment thread src/app/baseball/(dashboard)/_components/breadcrumbs.ts Outdated
Comment on lines +78 to +96
return (
<div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6 lg:px-8">
<SectionMasthead eyebrow="THE WAR ROOM · SCOUTING" title="Scouting" ink="pursuit">
<p className="max-w-2xl font-annual text-body text-text-secondary">
The evaluation toolkit — shortlists, side-by-side comparisons, and exportable packets —
once a prospect is worth a closer look.
</p>
</SectionMasthead>
<PaperCard className="mt-8 p-6">
<Eyebrow ink="pursuit">Unable to load your team</Eyebrow>
<p className="mt-2 font-annual text-body text-text-secondary">
We couldn&apos;t resolve your active team just now — this isn&apos;t a permissions
issue. Refresh the page, or switch teams from the sidebar if you coach more than one
program.
</p>
</PaperCard>
</div>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dedupe the repeated masthead block.

Lines 80-85 (null-context branch) and 105-110 (main return) render an identical SectionMasthead/paragraph pair. Hoist it into a local const and reuse it in both branches so future copy edits to this masthead can't drift out of sync.

+  const masthead = (
+    <SectionMasthead eyebrow="THE WAR ROOM · SCOUTING" title="Scouting" ink="pursuit">
+      <p className="max-w-2xl font-annual text-body text-text-secondary">
+        The evaluation toolkit — shortlists, side-by-side comparisons, and exportable packets —
+        once a prospect is worth a closer look.
+      </p>
+    </SectionMasthead>
+  );
+
   if (!navContext) {
     return (
       <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6 lg:px-8">
-        <SectionMasthead eyebrow="THE WAR ROOM · SCOUTING" title="Scouting" ink="pursuit">
-          <p className="max-w-2xl font-annual text-body text-text-secondary">
-            The evaluation toolkit — shortlists, side-by-side comparisons, and exportable packets —
-            once a prospect is worth a closer look.
-          </p>
-        </SectionMasthead>
+        {masthead}
         <PaperCard className="mt-8 p-6">

Same substitution for the main return's masthead block at lines 105-110.

Also applies to: 103-110

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/scouting/page.tsx around lines 78 -
96, The `SectionMasthead` plus descriptive paragraph is duplicated in both the
null-context branch and the main return in `ScoutingPage`; hoist that shared JSX
into a local const and reuse it in both places so the copy stays in sync. Update
the `ScoutingPage` render path to reference the shared masthead content instead
of repeating the same block in each branch.

Comment thread src/app/baseball/(dashboard)/dashboard/stats/games/page.tsx
Comment thread src/app/baseball/(dashboard)/dashboard/stats/games/page.tsx
Comment thread src/components/baseball/settings/PhilosophySettingsClient.tsx Outdated
…ing, DB dedupe index)

- performance page: single team-local today (todayIsoInTz) reused for the
  7-day readiness lookback AND the client prop — no more UTC drift
- stats/games: season-record fetch failure surfaced distinctly in UI
- dev-plans/[id]: real error vs not-found distinguished, retryable error UI
- games.ts: createGame rolls back its own orphaned calendar event on failure;
  getPlayerSeasonStats returns honest errors (PGRST116 stays empty-state)
- signals.ts: DB-level dedupe for NULL-program lift sessions — new partial
  unique index migration 20260709000000 (APPLIED to prod check-first: zero
  existing dupes) + 23505 catch-and-requery
- lifting programs: revalidateProgramPaths on update + publish detail bust
- calendar action: zod input validation (v4 .issues); calendar page row cap
  1000; performance/programs paginate via fetchAllRowsResult
- performance/live + [programId]: loader errors fail honestly, logServerError
- breadcrumbs: no non-null assertion + new unit tests
- PlayerProfileClient: date-only strings parse as local midnight
- lift page (lifting shell): split current/overdue queries matching baseball
- PhilosophySettingsClient: settings route constant; lift-athlete-context doc
  matrix corrected; e2e dead stats/season spec removed

Skipped (W4 deferral): scouting masthead dedupe — replaced wholesale by the
Living Annual masthead migration.

Gates: tsc=0, eslint touched files=0, unit 4519 passed (14 new).

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

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Round 2 addressed in 6af6433: all 22 findings — 21 fixed (incl. the UTC readiness window, orphaned-event rollback, DB-level NULL-program session dedupe via a new partial unique index applied to prod check-first, honest error surfacing across performance/live/programs/dev-plans/stats), 1 skipped per the documented W4 masthead deferral.

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

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

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git log --oneline -5
echo "---commit message 6af6433c---"
git show -s --format=%B 6af6433c
echo "---stat---"
git show --stat 6af6433c | head -150

Length of output: 5766


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== migration file ==="
fd "20260709000000" -t f
cat supabase/migrations/20260709000000_helm_lifting_sessions_null_program_dedupe.sql 2>/dev/null || fd "null_program_dedupe" -t f -x cat {}

echo "=== signals.ts dedupe/catch ==="
rg -n "23505|onConflict|helm_lifting_sessions" src/app/baseball/actions/signals.ts | head -40

echo "=== games.ts orphan rollback ==="
rg -n "createGame|calendar_event|orphan|rollback|PGRST116" src/app/baseball/actions/games.ts | head -60

echo "=== performance/page.tsx today reuse ==="
sed -n '1,80p' 'src/app/baseball/(dashboard)/dashboard/performance/page.tsx'

echo "=== breadcrumbs.ts non-null assertion ==="
sed -n '1,100p' 'src/app/baseball/(dashboard)/_components/breadcrumbs.ts'

echo "=== PlayerProfileClient date parsing ==="
rg -n "local midnight|parseLocal|new Date\(" src/components/baseball/player-profile/PlayerProfileClient.tsx | head -20

echo "=== calendar.ts zod ==="
rg -n "zod|\.issues|safeParse" src/app/baseball/actions/calendar.ts | head -30

echo "=== lift page split queries (lifting shell) ==="
sed -n '1,80p' 'src/app/lifting/(dashboard)/dashboard/lift/page.tsx' 2>/dev/null || fd -p "lifting/.*dashboard/lift/page.tsx"

Length of output: 16806


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== games.ts createGame full context ==="
sed -n '140,236p' src/app/baseball/actions/games.ts

echo "=== signals.ts dedupe context ==="
sed -n '400,470p' src/app/baseball/actions/signals.ts

echo "=== performance/page.tsx today usage further down ==="
sed -n '80,160p' 'src/app/baseball/(dashboard)/dashboard/performance/page.tsx'

echo "=== dev-plans/[id]/page.tsx error handling ==="
sed -n '1,120p' 'src/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx'

Length of output: 16405


@njrini99-code Verified. Spot-checked the highest-risk items in 6af6433c:

  • Orphaned calendar event rollbacksrc/app/baseball/actions/games.ts:222-227: the delete is correctly scoped to input.create_calendar_event && !input.event_id && eventId, so it only ever deletes the event this call created, never a caller-supplied event_id. Correct.
  • NULL-program session dedupe — this is a proper two-layer fix, not just an app-level patch:
    • App layer: src/app/baseball/actions/signals.ts:413-420 check-then-insert on (athlete_id, scheduled_date, title) scoped to program_assignment_id IS NULL.
    • DB layer: supabase/migrations/20260709000000_helm_lifting_sessions_null_program_dedupe.sql — partial unique index closing the race window Postgres' NULL != NULL semantics leaves open for the existing uq_helm_lifting_session constraint.
    • Race handling: signals.ts:439-455 catches sessionErr?.code === '23505' and re-queries rather than surfacing a spurious error to the caller. This is correct Postgres unique-violation handling.
  • UTC readiness driftsrc/app/baseball/(dashboard)/dashboard/performance/page.tsx:93: today is computed once via todayIsoInTz(resolveTeamTimezone(...)) and reused for both the 7-day lookback and the client prop, eliminating the split-source drift.
  • dev-plans/[id] honest errorssrc/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx:44-58: isGenuineNotFound is scoped to the permission-denied message and PGRST116/no-rows patterns only; everything else now sets fetchError instead of silently rendering "not found."
  • Breadcrumbs non-null assertion removedsrc/app/baseball/(dashboard)/_components/breadcrumbs.ts:71 now uses best?.label ?? fallback, no !.
  • Date-only local-midnight parsingsrc/components/baseball/player-profile/PlayerProfileClient.tsx:203 appends T00:00:00 for bare YYYY-MM-DD strings before constructing Date, avoiding the UTC-midnight-renders-as-prior-day bug.

No blocking findings (security/RLS/type-safety/a11y) in this batch. The one deferred item (scouting masthead dedupe → W4 Living Annual migration) is a reasonable scope call since it's superseded scope, not skipped work.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot removed the security Auth, secrets, RLS, PII, webhooks label Jul 9, 2026
@njrini99-code

Copy link
Copy Markdown
Owner Author

🤖 Mission Control — PR summary

What this changes
Baseball statsync verification + coherence remediation. ~189 files, +8.7k / −11.3k (large net deletion = dead-route / manifest / phantom-column cleanup). Main surfaces:

  • Box score ↔ profile sync: save_baseball_full_box_score RPC rewritten from the banned DELETE-then-INSERT to per-row UPSERT (ON CONFLICT game_id,player_id) with an isolated recalc loop so one player's failure can't roll back the game; migration 20260708150000 (already applied to prod). Shared format-stat helper gives box score / game detail / player profile stat parity.
  • Migrations: converge 20260708141900 (source_trust_level / source_visibility, no-op vs prod) and a lifting NULL-program dedupe partial unique index 20260709000000 (+ 23505 catch-and-requery), both applied. ⚠️ 20260708141000 is committed file-only and HELD (not applied) — flagged golf-function + invite-redemption gating risk.
  • Correctness fixes: team-local "today" / week bounds (was server-UTC), cross-New-Year season_year parse, PostgREST 1000-row pagination in the slash-line backfill, honest error-vs-empty-state surfacing across stats / games / performance / dev-plans, EditGameModal clearable fields, off-palette color tokens → canonical.
  • Shell / nav: single BaseballFairwayShell + nav-registry consistency, dead-route / stat-layer-manifest cleanup, exit-velocity phantom columns dropped; lifting-program revalidation across baseball routes.

Risk / areas to watch

  • DB migrations on a Golf-shared production DB — verify the box-score UPSERT path is idempotent and that branch code does not assume the HELD 20260708141000 is applied.
  • Very large deletion set — confirm no still-referenced code / routes were removed.
  • Box-score → player-profile stat parity; lifting-session dedupe (23505 requery path).

CI (as of this comment)

  • 🔴 check (advisory — routes + owner issues)failing, but self-labeled advisory (BaseballHelm Readiness Matrix; non-gating).
  • 🟡 Required checks still running: Next build, Smoke checks (Playwright E2E); CodeRabbit review in progress.
  • 🟢 Green: TypeScript, ESLint, Unit tests, Business contracts, Supabase lint + RLS, Schema / Route / Feature-knowledge gates, Review Gate all, CodeQL; Vercel build skipped by the cost-control ignore-step (expected).
  • Merge state BLOCKED — review decision CHANGES_REQUESTED (CodeRabbit re-review requested ~06:00 UTC) plus the pending required checks above.

Posted by the Helm Mission Control sweep — read-only automation. No merge, deploy, or rerun action was taken.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx (1)

61-64: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

getAssignContext swallows query errors while getProgramTree on the same page surfaces them.

Lines 234-241 log the failure but still return { athletes: [], groups: [] } at line 243, identical to the legitimate "nothing configured" case. AssignContext has no error field (61-64) for the page to react to, unlike ProgramTreeResult (66-69). A transient RLS/DB failure here renders as "no athletes/groups exist" with zero UI signal, while the same failure class in getProgramTree triggers ReadModelStateNotice.

🐛 Proposed fix: propagate the error like `ProgramTreeResult` does
 interface AssignContext {
   athletes: Array<Pick<HelmLiftingAthleteRow, 'id' | 'first_name' | 'last_name' | 'position' | 'sport'>>;
   groups: Array<Pick<HelmLiftingGroupRow, 'id' | 'name' | 'group_type'>>;
+  error?: boolean;
 }
-  return { athletes: athletes ?? [], groups: groups ?? [] };
+  return { athletes: athletes ?? [], groups: groups ?? [], error: Boolean(athletesError || groupsError) };

Then in the page, either render the same ReadModelStateNotice when assign.error is true, or pass the flag into ProgramEditorClient so the assign panel can show an inline warning instead of a blank roster.

Also applies to: 210-244, 264-289

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/app/baseball/`(dashboard)/dashboard/performance/programs/[programId]/page.tsx
around lines 61 - 64, `getAssignContext` currently hides query failures by
logging and returning empty athletes/groups, which makes real DB/RLS errors
indistinguishable from valid empty state. Update `AssignContext` and
`getAssignContext` to propagate an error flag/status like `ProgramTreeResult`
does, using the existing `getProgramTree`/`ReadModelStateNotice` pattern as a
reference. Then update the page and `ProgramEditorClient` call site to react to
that flag by surfacing the same warning or an inline assign-panel alert instead
of rendering a blank roster.
src/components/baseball/player-profile/PlayerProfileClient.tsx (1)

184-184: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Rename the stats filter value from practice to scrimmage.

src/components/baseball/player-profile/PlayerProfileClient.tsx:184 still defines StatFilter as 'practice', but the changed profile stats are sourced from baseball_games.game_type, which uses scrimmage. The Scrimmages filter will not match those rows.

🐛 Proposed fix
-type StatFilter = 'all' | 'game' | 'practice';
+type StatFilter = 'all' | 'game' | 'scrimmage';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/baseball/player-profile/PlayerProfileClient.tsx` at line 184,
The StatFilter union in PlayerProfileClient should use scrimmage instead of
practice so the filter matches baseball_games.game_type values. Update the
StatFilter type and any related filter option labels/logic in
PlayerProfileClient to reference scrimmage consistently, ensuring the Scrimmages
view selects the correct rows.
src/app/baseball/(dashboard)/dashboard/calendar/page.tsx (1)

85-121: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not collapse calendar query failures into an empty calendar.

src/app/baseball/(dashboard)/dashboard/calendar/page.tsx:85-121 never checks eventsResult.error. A PostgREST/RLS/schema failure still becomes events = [], which is indistinguishable from a real empty calendar.

🐛 Proposed fix
     ]);
+
+    if (eventsResult.error) {
+      throw new Error('Could not load calendar events.');
+    }
 
     // Map baseball_events → CalendarEvent. Row is annotated because the query
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/calendar/page.tsx around lines 85 -
121, The calendar data fetch in the `Promise.all` block is swallowing
`eventsResult.error`, so query failures look like a valid empty state. Update
the `page.tsx` calendar loader to check the result from `fromUntyped(supabase,
'baseball_events')` before mapping `events`, and if `eventsResult.error` is
present, surface that failure instead of assigning `events = []`. Keep the
existing `eventsResult`, `membersResult`, and `teamOrgResult` flow, but add
explicit error handling around the `eventsResult` path so PostgREST/RLS/schema
issues are distinguishable from a truly empty calendar.
src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx (2)

248-252: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pick the assigned exercise before completed work.

src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx:251 returns the first assigned or completed row in order, so after exercise 1 is completed and exercise 2 is assigned, the live row can keep showing exercise 1 as current.

🐛 Proposed fix
-    const currentSe = seList.find((e) => e.status === 'assigned' || e.status === 'completed');
+    const currentSe =
+      seList.find((e) => e.status === 'assigned') ??
+      [...seList].reverse().find((e) => e.status === 'completed');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/performance/live/page.tsx around
lines 248 - 252, The current live-session selection in liveAthletes uses the
first exercise with either assigned or completed status, which can leave an
older completed exercise displayed instead of the newly assigned one. Update the
selection logic in page.tsx around liveAthletes/currentSe so it prefers an
assigned exercise over any completed one, using the existing exercisesBySession
and latestSetBySeId flow without changing the surrounding mapping structure.

298-316: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not render the writable live room with an empty org id.

src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx:298-316 converts a missing liftCtx into { ok: true }, then renders LiveWeightRoomClient with orgId="" and canEdit=true. That exposes a broken write surface instead of the same explicit setup failure used by the programs page.

🐛 Proposed fix
   const liftCtx = await resolveBaseballLiftingOrg(teamId);
-  const liveRoomResult: LiveRoomResult = liftCtx
-    ? await buildLiveRoomData(liftCtx.organizationId, teamId, canViewReadiness)
-    : { ok: true, athletes: [], exerciseLibrary: [] };
+  if (!liftCtx) {
+    return (
+      <div className="mx-auto max-w-7xl px-4 py-6">
+        <ReadModelStateNotice state="empty" title="Lift Lab access not set up" />
+      </div>
+    );
+  }
+
+  const liveRoomResult: LiveRoomResult = await buildLiveRoomData(liftCtx.organizationId, teamId, canViewReadiness);
@@
-      orgId={liftCtx?.organizationId ?? ''}
+      orgId={liftCtx.organizationId}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/performance/live/page.tsx around
lines 298 - 316, The live performance page is treating a missing lifting context
as a successful empty result, which then renders LiveWeightRoomClient with an
empty orgId and writable access. Update the logic around
resolveBaseballLiftingOrg/buildLiveRoomData to fail closed when liftCtx is
missing, matching the explicit setup/error handling used elsewhere instead of
returning { ok: true } with empty data. Ensure the render path only reaches
LiveWeightRoomClient when a valid organizationId is present, and otherwise
return the same error notice/state used for the failed live room load.
src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx (1)

52-92: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Propagate program/count query errors instead of rendering partial counts.

src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx:52-92 ignores errors from the programs query and both fetchAllRowsResult calls. Because fetchAllRowsResult can return partial data with error, week/day counts can silently undercount again.

🐛 Proposed fix
-  const { data: programs } = (await fromUntyped(supabase, 'helm_lifting_programs')
+  const { data: programs, error: programsError } = (await fromUntyped(supabase, 'helm_lifting_programs')
@@
-    .limit(200)) as { data: HelmLiftingProgramRow[] | null };
+    .limit(200)) as { data: HelmLiftingProgramRow[] | null; error: { message: string } | null };
+
+  if (programsError) throw new Error('Could not load lifting programs.');
@@
-  const { data: weeksData } = await fetchAllRowsResult<{ id: string; program_id: string }>(
+  const { data: weeksData, error: weeksError } = await fetchAllRowsResult<{ id: string; program_id: string }>(
@@
   );
+  if (weeksError) throw new Error('Could not load lifting program weeks.');
@@
-    const { data: daysData } = await fetchAllRowsResult<{ week_id: string }>((from, to) =>
+    const { data: daysData, error: daysError } = await fetchAllRowsResult<{ week_id: string }>((from, to) =>
@@
     );
+    if (daysError) throw new Error('Could not load lifting program days.');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/performance/programs/page.tsx around
lines 52 - 92, The programs/count aggregation in the dashboard page is dropping
query failures and can render partial counts. Update the programs query and both
fetchAllRowsResult calls in the page component to check for returned error
values and fail fast instead of continuing with whatever data was fetched.
Propagate or throw the error from the programs, weeks, and days queries so the
page does not silently undercount. Use the existing aggregation flow around
programs, weeksData, and daysData to locate the fix.
src/app/baseball/actions/games.ts (1)

1409-1416: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle season lookup errors before returning an empty log. src/app/baseball/actions/games.ts:1409-1416 drops the baseball_games query error, so a DB/RLS failure can collapse into seasonGameIds = [] and hit the no-games path. Return sanitizeDbError(...) before mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/actions/games.ts` around lines 1409 - 1416, The season
lookup in the baseball games flow is swallowing database errors, causing query
failures to look like an empty result. In the `seasonGames` fetch inside
`games.ts`, check the `db.from('baseball_games')` response for an error before
building `seasonGameIds`, and return `sanitizeDbError(...)` immediately when the
query fails. Keep the existing mapping logic only for successful responses so
the no-games path is reserved for true empty results.
src/app/baseball/actions/signals.ts (1)

487-499: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Handle the seed query errors src/app/baseball/actions/signals.ts:487-499
fromUntyped() returns a result object (src/lib/supabase/untyped.ts:69-71), so try/catch will not catch a failed select() or insert(). Check both error values and log/abort the seed; otherwise a converted lift can persist without its matching helm_lifting_session_exercises row.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/actions/signals.ts` around lines 487 - 499, The seed logic
in the lifting session exercise creation block is ignoring Supabase result
errors because fromUntyped() returns a result object rather than throwing. In
the flow that checks existingSe and inserts the HelmLiftingSessionExerciseInsert
payload, inspect and handle the error field for both the select and insert
operations, and abort/log the seed if either fails so a converted lift cannot
continue without its matching helm_lifting_session_exercises row.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/baseball/`(dashboard)/dashboard/dev-plans/[id]/page.tsx:
- Around line 157-189: The error state in the dev plan detail page is using raw
red utility classes instead of the design-system destructive tokens. Update the
`fetchError` return block in `page.tsx` to replace the `border-red-*`,
`bg-red-*`, and `text-red-*` classes with the canonical `destructive` color
family while keeping the same structure and messaging. Use the existing error
card and the `Button` retry action as the place to apply the tokenized styles.

In `@src/app/lifting/`(dashboard)/dashboard/lift/page.tsx:
- Around line 124-129: The upcoming sessions query in the lift dashboard is
currently including all statuses for current and future dates, so non-open
sessions can leak into the upcoming list. Update the query in the page component
around the fromUntyped(... 'helm_lifting_sessions') call to apply the same
OPEN_STATUSES filter used by the overdue query, keeping only open sessions for
scheduled_date >= today. Make the change in the lift page logic that builds the
upcoming sessions data so the dashboard only renders actionable sessions.

In `@src/components/baseball/games/GamesList.tsx`:
- Around line 166-170: The error state text in GamesList uses a raw red utility
class instead of the design-system destructive token. Update the conditional
season-record message in GamesList to use the canonical destructive color family
for the paragraph styling, keeping the existing layout logic tied to
recordError, seasonRecord, and title unchanged.

---

Outside diff comments:
In `@src/app/baseball/`(dashboard)/dashboard/calendar/page.tsx:
- Around line 85-121: The calendar data fetch in the `Promise.all` block is
swallowing `eventsResult.error`, so query failures look like a valid empty
state. Update the `page.tsx` calendar loader to check the result from
`fromUntyped(supabase, 'baseball_events')` before mapping `events`, and if
`eventsResult.error` is present, surface that failure instead of assigning
`events = []`. Keep the existing `eventsResult`, `membersResult`, and
`teamOrgResult` flow, but add explicit error handling around the `eventsResult`
path so PostgREST/RLS/schema issues are distinguishable from a truly empty
calendar.

In `@src/app/baseball/`(dashboard)/dashboard/performance/live/page.tsx:
- Around line 248-252: The current live-session selection in liveAthletes uses
the first exercise with either assigned or completed status, which can leave an
older completed exercise displayed instead of the newly assigned one. Update the
selection logic in page.tsx around liveAthletes/currentSe so it prefers an
assigned exercise over any completed one, using the existing exercisesBySession
and latestSetBySeId flow without changing the surrounding mapping structure.
- Around line 298-316: The live performance page is treating a missing lifting
context as a successful empty result, which then renders LiveWeightRoomClient
with an empty orgId and writable access. Update the logic around
resolveBaseballLiftingOrg/buildLiveRoomData to fail closed when liftCtx is
missing, matching the explicit setup/error handling used elsewhere instead of
returning { ok: true } with empty data. Ensure the render path only reaches
LiveWeightRoomClient when a valid organizationId is present, and otherwise
return the same error notice/state used for the failed live room load.

In
`@src/app/baseball/`(dashboard)/dashboard/performance/programs/[programId]/page.tsx:
- Around line 61-64: `getAssignContext` currently hides query failures by
logging and returning empty athletes/groups, which makes real DB/RLS errors
indistinguishable from valid empty state. Update `AssignContext` and
`getAssignContext` to propagate an error flag/status like `ProgramTreeResult`
does, using the existing `getProgramTree`/`ReadModelStateNotice` pattern as a
reference. Then update the page and `ProgramEditorClient` call site to react to
that flag by surfacing the same warning or an inline assign-panel alert instead
of rendering a blank roster.

In `@src/app/baseball/`(dashboard)/dashboard/performance/programs/page.tsx:
- Around line 52-92: The programs/count aggregation in the dashboard page is
dropping query failures and can render partial counts. Update the programs query
and both fetchAllRowsResult calls in the page component to check for returned
error values and fail fast instead of continuing with whatever data was fetched.
Propagate or throw the error from the programs, weeks, and days queries so the
page does not silently undercount. Use the existing aggregation flow around
programs, weeksData, and daysData to locate the fix.

In `@src/app/baseball/actions/games.ts`:
- Around line 1409-1416: The season lookup in the baseball games flow is
swallowing database errors, causing query failures to look like an empty result.
In the `seasonGames` fetch inside `games.ts`, check the
`db.from('baseball_games')` response for an error before building
`seasonGameIds`, and return `sanitizeDbError(...)` immediately when the query
fails. Keep the existing mapping logic only for successful responses so the
no-games path is reserved for true empty results.

In `@src/app/baseball/actions/signals.ts`:
- Around line 487-499: The seed logic in the lifting session exercise creation
block is ignoring Supabase result errors because fromUntyped() returns a result
object rather than throwing. In the flow that checks existingSe and inserts the
HelmLiftingSessionExerciseInsert payload, inspect and handle the error field for
both the select and insert operations, and abort/log the seed if either fails so
a converted lift cannot continue without its matching
helm_lifting_session_exercises row.

In `@src/components/baseball/player-profile/PlayerProfileClient.tsx`:
- Line 184: The StatFilter union in PlayerProfileClient should use scrimmage
instead of practice so the filter matches baseball_games.game_type values.
Update the StatFilter type and any related filter option labels/logic in
PlayerProfileClient to reference scrimmage consistently, ensuring the Scrimmages
view selects the correct rows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 08992724-9b79-4877-9eb6-3f5152c1c572

📥 Commits

Reviewing files that changed from the base of the PR and between 1297b57 and 6af6433.

📒 Files selected for processing (20)
  • e2e/baseball-box-score.spec.ts
  • src/app/baseball/(dashboard)/_components/breadcrumbs.test.ts
  • src/app/baseball/(dashboard)/_components/breadcrumbs.ts
  • src/app/baseball/(dashboard)/dashboard/calendar/page.tsx
  • src/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/lift/_lift-athlete-context.ts
  • src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/page.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx
  • src/app/baseball/(dashboard)/dashboard/stats/games/page.tsx
  • src/app/baseball/actions/calendar.ts
  • src/app/baseball/actions/games.ts
  • src/app/baseball/actions/signals.ts
  • src/app/lifting/(dashboard)/dashboard/lift/page.tsx
  • src/app/lifting/actions/programs.ts
  • src/components/baseball/games/GamesList.tsx
  • src/components/baseball/player-profile/PlayerProfileClient.tsx
  • src/components/baseball/settings/PhilosophySettingsClient.tsx
  • supabase/migrations/20260709000000_helm_lifting_sessions_null_program_dedupe.sql

Comment thread src/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx
Comment thread src/app/lifting/(dashboard)/dashboard/lift/page.tsx
Comment thread src/components/baseball/games/GamesList.tsx
…s filter, destructive tokens)

- calendar/page.tsx + performance/programs pages + games.ts season lookup +
  performance/live loaders: query failures now surface (throw to boundary or
  sanitizeDbError) instead of collapsing into empty states
- performance/live: prefer newly-assigned exercise over last completed; fail
  closed on missing lifting org (no writable room with empty org id)
- performance/programs/[programId]: assign-context errors surfaced inline
- lifting lift page: current/future query filters to open statuses
- signals.ts: seed select/insert errors actually checked + logServerError
  (the try/catch never fired — supabase resolves errors, doesn't throw)
- PlayerProfileClient: StatFilter union renamed practice→scrimmage, dead
  translation layer removed
- dev-plans/[id] + GamesList: raw red classes → destructive tokens

Gates: tsc=0, lint=0, unit 4519 passed.

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

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Round 3 addressed in 761cacb — all 11 findings fixed (error-honesty across calendar/performance/games/signals loaders, open-status filter, assigned-exercise preference, fail-closed live room, destructive tokens, StatFilter cleanup). Requesting a full from-scratch review per docs/operations/coderabbit-review-workflow.md so the decision reflects the whole current diff rather than incremental deltas. The only intentionally-open items remain the W4/W7 deferrals documented in docs/audits/PRODUCTION_READINESS_MISSION_2026-07-09.md (Living Annual masthead migration replaces the duplicated headers; the readiness matrix gets a full truthful refresh).

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

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

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git log --oneline -8
echo "---commit message 761cacb3---"
git show -s --format=%B 761cacb3
echo "---stat---"
git show --stat 761cacb3 | head -150

Length of output: 4302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== calendar/page.tsx diff ==="
git show 761cacb3 -- 'src/app/baseball/(dashboard)/dashboard/calendar/page.tsx'

echo "=== dev-plans/[id]/page.tsx diff ==="
git show 761cacb3 -- 'src/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx'

echo "=== performance/live/page.tsx diff ==="
git show 761cacb3 -- 'src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx'

echo "=== performance/programs/[programId]/page.tsx diff ==="
git show 761cacb3 -- 'src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx'

echo "=== performance/programs/page.tsx diff ==="
git show 761cacb3 -- 'src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx'

echo "=== games.ts diff ==="
git show 761cacb3 -- 'src/app/baseball/actions/games.ts'

echo "=== signals.ts diff ==="
git show 761cacb3 -- 'src/app/baseball/actions/signals.ts'

echo "=== lifting lift page diff ==="
git show 761cacb3 -- 'src/app/lifting/(dashboard)/dashboard/lift/page.tsx'

echo "=== GamesList.tsx diff ==="
git show 761cacb3 -- 'src/components/baseball/games/GamesList.tsx'

echo "=== PlayerProfileClient.tsx diff ==="
git show 761cacb3 -- 'src/components/baseball/player-profile/PlayerProfileClient.tsx'

Length of output: 32023


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== OPEN_STATUSES definition ==="
rg -n "OPEN_STATUSES" 'src/app/lifting/(dashboard)/dashboard/lift/page.tsx'

echo "=== sanitizeDbError import in games.ts ==="
rg -n "sanitizeDbError" src/app/baseball/actions/games.ts | head -10

echo "=== getPlayerSeasonStatsImpl full context ==="
sed -n '1380,1460p' src/app/baseball/actions/games.ts

echo "=== calendar/page.tsx error.tsx presence ==="
fd -p 'error.tsx' 'src/app/baseball/(dashboard)'

echo "=== check dashboard error boundary exists for calendar route group ==="
fd -p 'error.tsx' src/app/baseball

echo "=== signals.ts surrounding try/catch full block ==="
sed -n '460,545p' src/app/baseball/actions/signals.ts

Length of output: 19991


@njrini99-code Verified 761cacb3 against all 11 claimed fixes. File:line citations below; no blocking findings.

Confirmed correct:

  • src/app/baseball/(dashboard)/dashboard/calendar/page.tsx:117-120eventsResult.error now throws instead of silently rendering events=[]. Error boundary exists at src/app/baseball/(dashboard)/dashboard/calendar/error.tsx, so this fails loud, not blank.
  • src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx:251-256currentSe selection bug fixed: seList.find(e => e.status === 'assigned') ?? [...seList].reverse().find(e => e.status === 'completed') correctly prefers the newly-assigned exercise over a stale completed one.
  • Same file, :305-320 — fail-closed branch added when liftCtx is null, before ever calling buildLiveRoomData. This also removes the orgId={liftCtx?.organizationId ?? ''} type-unsafe fallback at the old call site — good, that empty-string org id was a real footgun for a writable surface.
  • src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx:63-66,247,285-291AssignContext.error is now distinguishable from "no athletes/groups yet" and surfaced inline. Correct.
  • src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx:52-92programsError/weeksError/daysError are now checked and thrown; previously undercounted week/day totals on silent failure.
  • src/app/baseball/actions/games.ts:1406-1416 (getPlayerSeasonStatsImpl) — seasonGamesError now returns sanitizeDbError(...) instead of proceeding with an empty game-id list on a real DB failure. Confirmed sanitizeDbError was already imported (line 6), no new import risk.
  • src/app/baseball/actions/signals.ts:487-517 — this is the most substantive fix in the batch: existingSeError and insertSeError are now checked explicitly. The comment nails the actual root cause — fromUntyped() resolves { data, error }, it doesn't throw, so the surrounding try/catch was structurally dead code for real query failures. Good catch on your part.
  • src/app/lifting/(dashboard)/dashboard/lift/page.tsx:127.in('status', OPEN_STATUSES) added to the today/future query; OPEN_STATUSES = ['assigned','started','modified'] (line 100) is applied consistently to both the today/future and overdue branches (confirmed both call sites at lines 127 and 134).
  • src/components/baseball/player-profile/PlayerProfileClient.tsx:184,416,454,1037StatFilter union now uses 'scrimmage' directly, dead wantType translation removed from both filteredStats and statSummary, and the filter-button array updated to match. No remaining 'practice' references in this filter path.
  • Destructive-token swaps (dev-plans/[id]/page.tsx:174-176, GamesList.tsx:167) — cosmetic only, red-*destructive/*, correct and consistent with the design-token migration elsewhere in the PR.

No regressions or new blocking issues (security/RLS/type-safety/a11y) found in this round. The deferred W4/W7 items remain a reasonable scope call as previously assessed — they're superseded by planned follow-on waves, not silently dropped.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added the security Auth, secrets, RLS, PII, webhooks label Jul 9, 2026
18 Owner Issue cells updated: closed issues (368-416 range, all verified
CLOSED via gh) removed; only open issues 377/379/394 remain linked. Cells
left with no open issue now say so explicitly and point at the W7 full
matrix refresh for the row re-grade (the matrix's row content is known
stale — several rows describe already-fixed defects, e.g. the box-score
delete-then-insert replaced by the upsert RPC applied 2026-07-09).

check:readiness-matrix passes with GITHUB_TOKEN (routes + issue states).
Clears the advisory 'check (routes + owner issues)' CI failure.

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

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

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

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

Labels

database Schema, migrations, indexes, SQL security Auth, secrets, RLS, PII, webhooks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant