Skip to content

BaseballHelm coherence: one shell, capped nav IA, one Lift Lab, data honesty#792

Merged
njrini99-code merged 19 commits into
mainfrom
fix/baseballhelm-coherence-2026-07-08
Jul 9, 2026
Merged

BaseballHelm coherence: one shell, capped nav IA, one Lift Lab, data honesty#792
njrini99-code merged 19 commits into
mainfrom
fix/baseballhelm-coherence-2026-07-08

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

What this is

The overnight consolidation that ends the two-workflow era. Ruling doc: docs/baseball/COHERENCE_RULING_2026-07-08.md. 24 focused commits (deliberately decomposed per the 2026-06-30 shell postmortem's contrast pattern — each commit is one surface).

The four structural fixes

  1. ONE shell. BaseballFairwayShell renders unconditionally; every isRedesignEnabled() fork in baseball is gone. Deleted: (coach-dashboard) route group (zero pages), BaseballShellLayout, dashboard-shell.tsx, layout/sidebar.tsx + team-switcher.tsx (zero importers — golf renders GolfSidebar.tsx, never these). The env-flag regression path to the 31-item flat sidebar no longer exists.
  2. Navigation IA capped. 8 hubs (College 7 / HS 6 / JUCO 8), every hub ≤3 subtabs, test-locked in nav-manifest.test.ts for every coach type + players. Stats' 13-tab row → Stats Center · Games · Postgame (Season/Upload/Import fold inside). Management's 13-tab row → Decision Room · Settings · Organization (Settings = card-grid landing, splice deleted). New Operations and Scouting landings inherit registry gating. Announcements lives in Messages. Players hitting /dashboard/practice land on their own practice surface.
  3. ONE Lift Lab. Six baseball routes repointed at canonical src/components/lifting/*; six legacy duplicates deleted; zero baseball_lift_* writes remain in baseball surfaces. Coach edit access preserved via helm_lifting_org_viewers bridge (migration 20260708010000, trigger-maintained; 9/10 coaches verified — 10th has no org). Lifting mutations now revalidate both portals. Dynamic groups (both prod rows) render first-class read-only.
  4. Data honesty. Team-local "today" everywhere (readiness, command center, week bounds); tri-state eligibility (no more everyone-is-Ineligible); phantom EXIT V column removed; UUID breadcrumbs impossible (id-shape guard + real entity names); calendar bounded/pluralized with null-end_time defaults; swallowed insert errors surfaced; dev-plan cold-URL bounce fixed; E2E spec cleans up after itself.

Prod operations already executed (independent of this deploy)

  • Purged 160 E2E Created Opponent% junk games + 160 linked events (86% of the games table)
  • Repaired 5 zero-duration midnight demo events to real times
  • Backfilled career_obp/slg/ops (20/22 rows) via new idempotent script
  • Applied both migrations in this PR (20260708010000, 20260708011000)

Gates (all green)

tsc --noEmit 0 · eslint 0 warnings · lint-ratchet 0 · unit 4501 passed (445 files; +39 new tests) · next build 0 · sweeps: 0 legacy nav arrays, 0 baseball_lift_ writes in baseball surfaces, remaining shell refs comment-only.

Known follow-ups (documented in ruling doc)

E2E still targets prod Supabase (cleanup added; isolated project needs owner). Dynamic-group rule authoring not rebuilt (read-only + nothing recomputes memberships). Travel card redesign deferred (PR #555 collision). (player-dashboard)(dashboard) fold deferred (URLs load-bearing).

🤖 Generated with Claude Code

https://claude.ai/code/session_01LGqvhig8njW38q9PAwDXgu

Fable Integrator and others added 17 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
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

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

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

@cursor

cursor Bot commented Jul 8, 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 8, 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 8, 2026 7:32am

Request Review

@supabase

supabase Bot commented Jul 8, 2026

Copy link
Copy Markdown

Updates to Preview Branch (fix/baseballhelm-coherence-2026-07-08) ↗︎

Deployments Status Updated
Database Wed, 08 Jul 2026 07:33:07 UTC
Services Wed, 08 Jul 2026 07:33:07 UTC
APIs Wed, 08 Jul 2026 07:33:07 UTC

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

Tasks Status Updated
Configurations Wed, 08 Jul 2026 07:33:07 UTC
Migrations Wed, 08 Jul 2026 07:33:07 UTC
Seeding Wed, 08 Jul 2026 07:33:07 UTC
Edge Functions ⚠️ Wed, 08 Jul 2026 07:33:07 UTC

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


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

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added new dashboard areas for Operations and Scouting, plus a dedicated Messages hub.
    • Unified baseball dashboard and player views into a single shell with updated navigation and breadcrumbs.
  • Bug Fixes

    • Fixed calendar, readiness, and event timing to respect team-local timezones.
    • Improved event creation feedback with non-blocking warnings when partial saves occur.
    • Updated academic eligibility to show “Not on file” when no record exists.
  • Other Updates

    • Refreshed several baseball pages to use the newer shared layouts and empty states.

Walkthrough

This PR applies the BaseballHelm coherence ruling: it unifies the shell and navigation model, adds pathname-based breadcrumb labels, migrates several dashboard surfaces to Living Annual components, tightens date and data correctness, and repoints baseball lift/performance flows onto canonical helm_lifting_* data and actions.

Changes

BaseballHelm Coherence Ruling

Layer / File(s) Summary
Ruling document
docs/baseball/COHERENCE_RULING_2026-07-08.md
Adds the ruling document covering shell unification, navigation limits, lifting unification, correctness rules, dead-code removals, and out-of-scope items.
Shell and navigation
src/app/baseball/(dashboard)/BaseballFairwayShell.tsx, src/app/baseball/(dashboard)/layout.tsx, src/app/baseball/(player-dashboard)/player/layout.tsx, src/app/baseball/(dashboard)/_components/*, src/lib/baseball/{nav-registry.ts,nav-manifest.ts,route-contract.ts}, src/lib/baseball/__tests__/*, src/app/baseball/(dashboard)/dashboard/{operations,scouting,practice,players,...}
Unifies shell rendering, adds breadcrumb overrides, restructures hubs/subtabs and landing pages, and updates related route-contract tests.
Living Annual presentation
src/app/baseball/(dashboard)/dashboard/*, src/components/baseball/{calendar,documents,travel,performance,stats-center,messages,ReadModelStateNotice.tsx,NotificationBell.tsx}
Replaces legacy headers, cards, and empty states with Living Annual components and slot-based layouts.
Data correctness
src/app/baseball/actions/*, src/app/baseball/(dashboard)/dashboard/{academics,calendar,roster,readiness}/*, src/hooks/useCalendarEvents.ts, src/lib/baseball/read-models/*, scripts/*, supabase/migrations/*
Updates timezone handling, eligibility semantics, roster output, event create handling, seed/backfill scripts, and cleanup/test coverage.
Lifting unification
src/app/baseball/(dashboard)/dashboard/lift/*, src/app/baseball/(dashboard)/dashboard/performance/*, src/app/lifting/actions/*, src/components/lifting/groups/*, src/components/baseball/performance/*, supabase/migrations/*
Repoints baseball lift and performance surfaces to canonical lifting tables, extends cache invalidation to baseball routes, and adds dynamic-group restrictions and coach access bridging.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client as BaseballCalendarWrapper
  participant Action as createBaseballEventAction
  participant DB as Supabase

  Client->>Action: createEvent(payload)
  Action->>DB: insert baseball_events
  Action->>DB: insert baseball_event_attendance
  DB-->>Action: attendance insert error
  Action->>DB: insert baseball_games (if game/scrimmage)
  Action-->>Client: success + warning
  Client->>Client: toast.warning(warning)
Loading
sequenceDiagram
  participant Page as lift/page.tsx
  participant Ctx as resolvePlayerLiftAthleteContext
  participant DB as Supabase
  participant Client as PlayerLiftHomeClient

  Page->>Ctx: resolve athlete context
  Ctx-->>Page: organizationId, athleteId, teamId
  Page->>DB: fetch helm_lifting_sessions
  DB-->>Page: upcoming and recent sessions
  Page->>Client: render session data
Loading

Possibly related PRs

Suggested labels: database, design-system


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
No Service-Role In Client Bundles ❌ Error scripts/e2e-supabase-admin.ts:23-25 and scripts/backfill-baseball-slash-lines.ts:41,73-75 read SUPABASE_SERVICE_ROLE_KEY and create service-role clients outside allowed admin paths. Move service-role client creation into src/lib/supabase/admin* or src/app/api//admin/, or refactor these scripts to avoid direct service-role env/key usage.
No Destructive Writes ❌ Error scripts/seed-rini-baseball-demo.ts:198-201 deletes baseball_team_coach_staff/baseball_teams, then :221-240 upserts those same tables; no rebuild/transaction comment. Add an explicit rebuild/transaction comment or stop deleting before reseeding; if this is intentional, wrap the delete+seed sequence in one DB transaction.
Title check ⚠️ Warning The title is related, but it does not follow Conventional Commits or include a required scope. Rewrite it as a Conventional Commit with a required scope, for example docs(baseball): ... or feat(baseball): ....
Conventional Commits ❓ Inconclusive HEAD subject matches the Conventional Commits regex, but the PR title isn’t available in this snapshot to verify independently. Provide the PR title (or PR metadata) so it can be checked against the regex alongside the conventional squash subject.
✅ Passed checks (8 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the PR scope and accurately summarizes the shell, nav, Lift Lab, and data-honesty changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Rls Coverage On New Tables ✅ Passed PASS: The only changed migrations insert/define a trigger on existing tables or alter existing columns; no CREATE TABLE appears, so the RLS/new-table gate is satisfied. `supabase/migrations/2026070...
Auth Check In Server Actions ✅ Passed No changed action file has an exported async fn with .from/.rpc before auth.getUser(); calendar, games, and player-sessions authenticate first, others have no direct DB calls.
Sport-Prefixed Table Names ✅ Passed Changed TS/TSX files only query sport tables with baseball_/golf_ prefixes; no bare coaches/players/teams/rounds/events table targets were added.
No Edits To Historical Migrations ✅ Passed Only supabase/migrations/20260708010000_* and 20260708011000_* are present; no modified migration is at or below 20260527120000.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/baseballhelm-coherence-2026-07-08
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)

ast-grep could not parse rule config: /ast-grep-rules/../git/.coderabbit/ast-grep/no-explicit-any.yml

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

e2e/baseball-box-score.spec.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

scripts/backfill-baseball-slash-lines.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

scripts/e2e-supabase-admin.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 76 others

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

@coderabbitai coderabbitai Bot added database Schema, migrations, indexes, SQL design-system labels Jul 8, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

BaseballHelm coherence: unify shell/nav, one Lifting Lab, and data-honesty fixes

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

Grey Divider

AI Description

• Render BaseballFairwayShell everywhere and delete dead legacy shell/routes.
• Cap hub navigation to ≤3 subtabs via registry folding, with new landing pages.
• Repoint baseball lifting routes to canonical helm_lifting stack with coach-edit bridge.
• Fix timezone/ID/bounded-calendar issues and add regression/unit/E2E cleanup tests.
Diagram

graph TD
  A["Next.js route layouts"] --> B["BaseballFairwayShell"] --> C["Nav registry/resolve"] --> D["Hub tabs & landings"]
  B --> E["Breadcrumbs + overrides"]
  F["Baseball performance routes"] --> G["Canonical Lifting components"] --> H[("Supabase helm_lifting_* tables")]
  I["Supabase migrations"] --> H
  subgraph Legend
    direction LR
    _page["Page/Layout"] ~~~ _svc["UI shell/component"] ~~~ _lib["Lib/logic"] ~~~ _db[("Database")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep legacy baseball lifting read-model adapters as a compatibility layer
  • ➕ Less direct SQL in baseball pages; reuse existing baseball read-model functions
  • ➕ Potentially fewer moving parts during the portal swap
  • ➖ Continues divergence from the canonical /lifting portal shapes
  • ➖ Would preserve the legacy BaseballLift* DTOs and delay deletion of duplicated components/writes
  • ➖ Harder to guarantee “zero baseball_lift_* writes” and maintain one source of truth
2. Implement navigation caps purely in UI (truncate/hide tabs) without registry folding
  • ➕ Smaller change to nav-registry types; fewer invariants to test
  • ➖ Deep links and command palette/breadcrumb resolution become inconsistent
  • ➖ Hidden routes risk becoming effectively orphaned without explicit landing surfaces
  • ➖ Does not provide a durable, testable contract for IA limits
3. Create a dedicated E2E Supabase project now and stop writing to shared/prod-like DB
  • ➕ Eliminates pollution risk entirely; safer destructive test coverage
  • ➖ Requires infra ownership/time and secret management; higher coordination cost
  • ➖ Not strictly necessary to ship the shell/nav/lifting consolidation

Recommendation: The chosen approach (registry-level folding + landing pages + canonical lifting repoint + DB bridge) is the most coherent long-term direction because it eliminates duplicated shells and lifting stacks while making the navigation cap enforceable via tests and resolve-active-hub. The main trade-off is a large diff with more direct DB querying in some baseball pages, but the added unit/E2E tests and explicit migrations mitigate regression risk. Deferring the dedicated E2E Supabase project remains reasonable as a follow-up.

Files changed (100) +4776 / -9297

Enhancement (32) +2262 / -605
backfill-baseball-slash-lines.tsAdd idempotent backfill for career OBP/SLG/OPS aggregates +182/-0

Add idempotent backfill for career OBP/SLG/OPS aggregates

• Adds a one-time (dry-run by default) script to compute and update career_obp/career_slg/career_ops in baseball_player_aggregates based on baseball_player_stats using the shared computeCareerSlashLine helper. Avoids explicit selects that 400 due to schema drift and updates only the three slash columns.

scripts/backfill-baseball-slash-lines.ts

BaseballFairwayShell.tsxMake Fairway shell the sole shell; integrate breadcrumbs + Messages hub +81/-73

Make Fairway shell the sole shell; integrate breadcrumbs + Messages hub

• Updates shell documentation and behavior to reflect unconditional mounting. Adjusts bottom nav preferences to 3 primary destinations, moves Messages into hub-derived items with unread badge support, and extracts breadcrumb building to a pure module with label overrides.

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

breadcrumb-label.tsxAdd pathname-keyed breadcrumb label override channel +88/-0

Add pathname-keyed breadcrumb label override channel

• Introduces a small context provider and helper component to let dynamic detail pages supply real entity names to the shell breadcrumb trail. Prevents stale label leakage by keying overrides by pathname and clearing on unmount.

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

hub-definitions.tsCap hubs to ≤3 subtabs via folding; add Messages hub + landings +118/-227

Cap hubs to ≤3 subtabs via folding; add Messages hub + landings

• Reworks coach hub tab derivation to exclude folded entries, introduces Messages as a real hub (Messages + Announcements), and reshapes Stats/Development/Recruiting/Management to meet the ≤3 rule. Trims player hub subtabs to the same cap and updates ordering/labels (e.g., Performance→Training in hub strip).

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

resolve-active-hub.tsExtend hub resolver for new folding/labeling contract +1/-0

Extend hub resolver for new folding/labeling contract

• Small adjustment to active hub resolution to align with the updated hub/tab definitions and folded route matching behavior.

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

page.tsxWire announcements to Messages hub structure +2/-1

Wire announcements to Messages hub structure

• Minor change to align the announcements route with the new Messages hub information architecture.

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

page.tsxAlign documents page with new Team>Operations nav behavior +6/-6

Align documents page with new Team>Operations nav behavior

• Small adjustments to keep the documents route working cleanly under the new hub/landing IA and single-shell environment.

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

_lift-athlete-context.tsAdd server helpers to resolve canonical lifting athlete/org context +86/-0

Add server helpers to resolve canonical lifting athlete/org context

• Introduces server-only helpers to map baseball player -> team -> lifting org -> helm_lifting_athlete id and to check readiness submission today. Shared by player lift home and session pages now using canonical lifting components.

src/app/baseball/(dashboard)/dashboard/lift/_lift-athlete-context.ts

page.tsxRepoint player Lift home to canonical lifting component + helm tables +84/-14

Repoint player Lift home to canonical lifting component + helm tables

• Replaces legacy baseball PlayerLiftHomeClient/read-model usage with direct helm_lifting_sessions reads and the canonical lifting PlayerLiftHomeClient. Preserves onboarding overlay and degrades gracefully when the athlete/org context is missing.

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

page.tsxRepoint lift session execution to canonical lifting component + helm tables +88/-6

Repoint lift session execution to canonical lifting component + helm tables

• Fetches helm_lifting session/exercise/set-result data directly (with athlete/org guards) and renders canonical PlayerLiftSessionClient. Redirects to lift home when the lifting athlete context is unavailable and keeps readinessSubmittedToday computed via helm checkins.

src/app/baseball/(dashboard)/dashboard/lift/[sessionId]/page.tsx

page.tsxAdd Team>Operations landing card grid for folded routes +117/-0

Add Team>Operations landing card grid for folded routes

• Introduces a server-rendered landing page that links to Documents/Travel/Practice Planner/Practice Effectiveness based on registry visibility. Supports the ≤3 Team hub subtab cap while keeping deep links and gating consistent with nav-registry.

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

page.tsxRepoint Live Weight Room to canonical lifting sessions client +200/-49

Repoint Live Weight Room to canonical lifting sessions client

• Replaces the legacy baseball LiveWeightRoom read-model/component with canonical LiveWeightRoomClient and direct helm_lifting_* queries scoped to team sessions. Fetches readiness checkins only when can_view_readiness is granted and assembles HelmLiftingLiveAthleteRow payload server-side.

src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx

page.tsxRepoint Strength Groups page to canonical lifting groups client +90/-15

Repoint Strength Groups page to canonical lifting groups client

• Replaces legacy strength group board read-model/component with direct helm_lifting_groups and group_members reads and renders canonical StrengthGroupsClient. Keeps server gating via can_manage_lifting and resolves lifting org context via resolveBaseballLiftingOrg.

src/app/baseball/(dashboard)/dashboard/performance/groups/page.tsx

page.tsxRepoint Programs list to canonical lifting programs client +74/-9

Repoint Programs list to canonical lifting programs client

• Replaces legacy program list read-model/component with direct helm_lifting_programs + week/day count queries and renders canonical ProgramListClient. Threads orgId and canEdit for action gating consistency.

src/app/baseball/(dashboard)/dashboard/performance/programs/page.tsx

page.tsxRepoint Program editor to canonical lifting ProgramEditorClient +168/-16

Repoint Program editor to canonical lifting ProgramEditorClient

• Replaces baseball lift-program tree/read-models with direct helm_lifting_* program/week/day/section/prescription reads and a team-scoped assign context. Adds notFound safeguards for org/team mismatches and passes orgId/canEdit into the canonical editor.

src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx

page.tsx(duplicate entry retained intentionally) Canonical program editor wiring +168/-16

(duplicate entry retained intentionally) Canonical program editor wiring

• Same file: ensures program tree assembly and assign context are fetched from helm_lifting_* tables and wired into canonical ProgramEditorClient props.

src/app/baseball/(dashboard)/dashboard/performance/programs/[programId]/page.tsx

RosterFairway.tsxRoster Fairway alignment with updated stats/aggregate fields +4/-4

Roster Fairway alignment with updated stats/aggregate fields

• Adjusts roster fairway rendering to align with newly surfaced slash-line fields and removed phantom columns.

src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx

page.tsxAdd Recruiting>Scouting landing card grid for folded routes +122/-0

Add Recruiting>Scouting landing card grid for folded routes

• Introduces a server-rendered landing page that links to Watchlist/Compare/Saved Comparisons/Scout Packets/Camps based on registry visibility. Supports the ≤3 Recruiting hub subtab cap while preserving deep links and gating via nav-registry.

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

page.tsxTravel page alignment with Team>Operations folding +9/-5

Travel page alignment with Team>Operations folding

• Minor update to keep Travel working cleanly as a folded destination under Operations while retaining its deep link URL.

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

groups.tsSmall lifting groups action enhancements +11/-0

Small lifting groups action enhancements

• Minor updates/additions to canonical lifting group actions to support new call sites and test coverage.

src/app/lifting/actions/groups.ts

player-sessions.tsSmall lifting player-sessions action enhancements +15/-0

Small lifting player-sessions action enhancements

• Minor updates/additions to canonical lifting player session actions to support the baseball portal repoint.

src/app/lifting/actions/player-sessions.ts

programs.tsTighten programs actions behavior/signatures +26/-1

Tighten programs actions behavior/signatures

• Small update to canonical lifting program actions consistent with the new consumers and tests.

src/app/lifting/actions/programs.ts

sessions.tsEnhance sessions actions for canonical Live Weight Room usage +26/-0

Enhance sessions actions for canonical Live Weight Room usage

• Minor updates/additions to canonical lifting session actions to support the migrated Live Weight Room surface.

src/app/lifting/actions/sessions.ts

DocumentsFairway.tsxDocuments Fairway refinements for single-path rendering +44/-35

Documents Fairway refinements for single-path rendering

• Adjusts DocumentsFairway to serve as the sole documents UI (post fork deletion), aligning interactions and presentation with the consolidated shell.

src/components/baseball/documents/DocumentsFairway.tsx

PlayerLiftToday.tsxAlign Player Today lift card with canonical lifting context +15/-13

Align Player Today lift card with canonical lifting context

• Adjusts the player today lifting card to stay consistent with the one-lab/canonical data direction while preserving the daily loop surface.

src/components/baseball/performance/PlayerLiftToday.tsx

StatsCenterClient.tsxAdjust Stats Center to support folded subroutes/CTAs +22/-1

Adjust Stats Center to support folded subroutes/CTAs

• Updates Stats Center client to align with the new IA where Season/Upload/Import are views/CTAs under Stats Center rather than separate tabs.

src/components/baseball/stats-center/StatsCenterClient.tsx

TravelClient.tsxTravel client alignment with Operations folding +28/-33

Travel client alignment with Operations folding

• Refines Travel client UI behavior for its new folded placement under the Team>Operations landing.

src/components/baseball/travel/TravelClient.tsx

StrengthGroupsClient.tsxRender dynamic lifting groups read-only with rule summary +186/-51

Render dynamic lifting groups read-only with rule summary

• Adds defensive summarization of rule_json for dynamic groups and disables archive/remove actions with tooltips explaining rule-managed membership. Prevents misleading edits that would be overwritten by rule recompute.

src/components/lifting/groups/StrengthGroupsClient.tsx

EmptyChatState.tsxRefine empty chat state UX +20/-18

Refine empty chat state UX

• Adjusts EmptyChatState behavior/layout for consistency with the unified MessagesFairway rendering path.

src/components/messages/EmptyChatState.tsx

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

Include new hub tab arrays in nav manifest generation

• Adds coach dashboard/messages tabs into the manifest’s hub-tab inventory to keep route resolution and manifest coverage consistent with the updated IA.

src/lib/baseball/nav-manifest.ts

nav-registry.tsAdd messages hub and folding/label override fields to nav registry +171/-12

Add messages hub and folding/label override fields to nav registry

• Introduces 'messages' as a hub, adds foldedUnder and hubTabLabel to support the ≤3-subtab cap while keeping deep-link/command-palette routes registered, and updates matchPrefixes for folded routes (Stats Center CTAs, Operations/Scouting folds). Renames command-center nav label to 'Overview' for UI coherence.

src/lib/baseball/nav-registry.ts

route-contract.tsTighten baseball route contract surface for unified shell +6/-0

Tighten baseball route contract surface for unified shell

• Adds/updates route-contract definitions to reflect the consolidated shell and navigation invariants used by tests and routing logic.

src/lib/baseball/route-contract.ts

Bug fix (18) +539 / -1016
seed-rini-baseball-demo.tsSeed realistic team-local event times (no zero-duration rows) +56/-3

Seed realistic team-local event times (no zero-duration rows)

• Adds DST-safe team-local wall-clock to UTC conversion and updates seeded baseball_events to have realistic start/end times. Fixes prior zero-duration seeded events and aligns seeding with team-local time semantics.

scripts/seed-rini-baseball-demo.ts

breadcrumbs.tsAdd pure breadcrumb builder with UUID/numeric guard +81/-0

Add pure breadcrumb builder with UUID/numeric guard

• Adds a unit-testable breadcrumb builder that derives labels from the nav registry and prevents raw UUID/numeric route segments from appearing in breadcrumbs. Supports an override label for dynamic detail pages and falls back to singularized parent segments when needed.

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

AcademicsClient.tsxTri-state academics eligibility rendering +29/-10

Tri-state academics eligibility rendering

• Updates eligibility UI to distinguish unknown/not-on-file from explicitly ineligible, avoiding the previous everyone-is-ineligible failure mode.

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

page.tsxBound calendar query window and default missing end_time for display +41/-113

Bound calendar query window and default missing end_time for display

• Removes redesign gating and always renders CalendarFairway. Adds a bounded fetch window (90d back/365d forward) with a hard limit, and maps null end_time to a display-only +1h default to avoid invisible zero-duration events.

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

page.tsxConvert dev-plan page to server gate + client mount +23/-744

Convert dev-plan page to server gate + client mount

• Replaces the prior all-client dev-plan page with a server-validated player route guard to eliminate cold-URL bounce/500 race with the dashboard session guard. Mounts the extracted DevPlanClient after auth is synchronously resolved.

src/app/baseball/(dashboard)/dashboard/dev-plan/page.tsx

page.tsxProvide breadcrumb override label for dev-plan detail route +5/-0

Provide breadcrumb override label for dev-plan detail route

• Adds BreadcrumbLabel wiring so the shell breadcrumb shows the plan title instead of a raw UUID segment on dynamic dev plan pages.

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

EventsClient.tsxSurface calendar-action partial-success warnings; migrate chrome +56/-40

Surface calendar-action partial-success warnings; migrate chrome

• Shows toast warnings when createBaseballEvent succeeds but secondary writes fail, and migrates page header/error/empty states to Living Annual components. Removes legacy Header usage and standardizes error rendering.

src/app/baseball/(dashboard)/dashboard/events/EventsClient.tsx

page.tsxProvide breadcrumb override label for player detail route +33/-26

Provide breadcrumb override label for player detail route

• Adds BreadcrumbLabel so the shell breadcrumb shows the player’s real name instead of a UUID-like segment on dynamic player pages.

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

page.tsxRedirect players from /dashboard/practice to player practice surface +16/-1

Redirect players from /dashboard/practice to player practice surface

• Ensures player sessions landing on the coach/team practice URL are routed to the canonical player practice page, matching the new IA contract.

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

page.tsxAlign readiness page with team-local “today” semantics +11/-2

Align readiness page with team-local “today” semantics

• Small update to ensure readiness computations use team-local day boundaries rather than server UTC day assumptions.

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

RosterClient.tsxRemove phantom roster column and tighten honesty in UI +1/-11

Remove phantom roster column and tighten honesty in UI

• Drops UI references to a non-existent/unsupported roster metric (e.g., phantom EXIT V) to avoid presenting data that cannot be sourced honestly.

src/app/baseball/(dashboard)/dashboard/roster/RosterClient.tsx

academics.tsEligibility/academics logic adjustment for tri-state display +4/-3

Eligibility/academics logic adjustment for tri-state display

• Minor changes to support the updated eligibility semantics (null/true/false) surfaced in the Academics UI.

src/app/baseball/actions/academics.ts

calendar.tsSurface partial-success warnings for secondary calendar writes +35/-4

Surface partial-success warnings for secondary calendar writes

• Extends ActionResult with a warning field and collects errors from RSVP attendance and linked game inserts without falsely failing the primary event creation. Revalidates stats games path on successful game creation and returns warnings for UI surfacing.

src/app/baseball/actions/calendar.ts

games.tsStop swallowing insert/update errors in game flows +48/-4

Stop swallowing insert/update errors in game flows

• Fails loudly when calendar event creation requested by createGame fails, preventing phantom “linked event” assumptions. Improves box-score upload bookkeeping: fails if upload tracking row is required for unmatched resolution, otherwise surfaces a non-fatal warning; logs inability to mark uploads completed after successful save.

src/app/baseball/actions/games.ts

CalendarFairway.tsxCalendarFairway enhancements for bounded/pluralized/honest display +48/-37

CalendarFairway enhancements for bounded/pluralized/honest display

• Improves calendar fairway rendering to reflect bounded event windows, correct pluralization, and display-only defaults for missing end_time without mutating DB values.

src/components/baseball/calendar/CalendarFairway.tsx

useCalendarEvents.tsDefault missing end_time to +1h for display (client hook) +18/-1

Default missing end_time to +1h for display (client hook)

• Ensures calendar events with null end_time display with a non-zero duration by using a display-only +1 hour default for timed events. Mirrors server-side calendar mapping and avoids writing any default back to the DB.

src/hooks/useCalendarEvents.ts

command-center.tsCompute “today” and week bounds in team-local timezone +28/-12

Compute “today” and week bounds in team-local timezone

• Fixes UTC-day and server-clock week-boundary bugs by resolving team timezone and using team-local day bounds expressed in UTC for event filtering. Uses tz-safe date arithmetic to compute Sunday–Saturday windows anchored on team-local “today”.

src/lib/baseball/read-models/command-center.ts

player-snapshot-cards.tsPrefer career OBP/SLG/OPS from aggregates when season missing +6/-5

Prefer career OBP/SLG/OPS from aggregates when season missing

• Extends the aggregates select to include career_obp/career_slg/career_ops and uses them as fallbacks when season stat rows don’t provide OBP/SLG/OPS, improving “data honesty” on snapshot cards.

src/lib/baseball/read-models/player-snapshot-cards.ts

Refactor (31) +796 / -7622
baseballhelm-command-center.mjsRemove obsolete command-center script +0/-818

Remove obsolete command-center script

• Deletes a legacy BaseballHelm script superseded by the new consolidated approach and/or Helm Bridge tooling.

scripts/baseballhelm-command-center.mjs

baseballhelm-loc.mjsRemove obsolete LOC script +0/-160

Remove obsolete LOC script

• Deletes a legacy BaseballHelm LOC script no longer used post-consolidation.

scripts/baseballhelm-loc.mjs

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

Delete dead (coach-dashboard) layout

• Removes an unused route-group layout from the deprecated (coach-dashboard) tree (no pages existed). Part of the one-shell consolidation.

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

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

Delete dead (coach-dashboard) template

• Removes an unused template component from the deprecated (coach-dashboard) route group. Part of the one-shell consolidation.

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

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

Delete dead (coach-dashboard) error boundary

• Removes an unused error component from the deprecated (coach-dashboard) route group. Part of the one-shell consolidation.

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

DevPlanClient.tsxExtract player dev-plan UI into stable client component +617/-0

Extract player dev-plan UI into stable client component

• Moves the large client implementation out of page.tsx, preserving behavior while reducing cold-navigation races. Migrates presentation to Living Annual chrome and fixes touch-target sizing for goal toggles.

src/app/baseball/(dashboard)/dashboard/dev-plan/DevPlanClient.tsx

documents-client.tsxRemove redesign fork; always render DocumentsFairway +86/-272

Remove redesign fork; always render DocumentsFairway

• Deletes the legacy non-Fairway documents UI path and leaves a single Fairway-based rendering. Keeps fairwayScope usage while removing isRedesignEnabled branching and associated legacy UI imports.

src/app/baseball/(dashboard)/dashboard/documents/documents-client.tsx

loading.tsxMinor loading behavior alignment for unified Messages hub +7/-7

Minor loading behavior alignment for unified Messages hub

• Small adjustments to Messages loading UI to match the consolidated Fairway-only rendering and new hub placement.

src/app/baseball/(dashboard)/dashboard/messages/loading.tsx

page.tsxRemove redesign gating; always use MessagesFairway +18/-102

Remove redesign gating; always use MessagesFairway

• Deletes the legacy messages layout fork and consistently renders MessagesFairway with list/chat/modal slots. Removes isRedesignEnabled and related conditional skeleton/layout code.

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

page.tsxRemove duplicate legacy header inside Settings page +7/-2

Remove duplicate legacy header inside Settings page

• Replaces the old shared <Header> with a simple title block to avoid duplicating the shell’s own top bar/breadcrumbs, aligning with the unified shell and IA.

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

page.tsxMinor routing/breadcrumb alignment for game detail pages +8/-3

Minor routing/breadcrumb alignment for game detail pages

• Small adjustments to keep game detail routing aligned with updated breadcrumbs/nav resolution behavior.

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

layout.tsxRender BaseballFairwayShell unconditionally (remove redesign fork) +11/-25

Render BaseballFairwayShell unconditionally (remove redesign fork)

• Deletes isRedesignEnabled gating and legacy BaseballShellLayout usage, always mounting BaseballFairwayShell after the existing session guard. Keeps the guard behavior but removes the regression-prone dual-shell path.

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

layout.tsxRender BaseballFairwayShell unconditionally for players +1/-9

Render BaseballFairwayShell unconditionally for players

• Removes redesign gating and legacy shell for player route group, always mounting BaseballFairwayShell with requiredRole='player'. Preserves PWA manifest link.

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

messages.tsMinor messages action alignment for consolidated hub +1/-1

Minor messages action alignment for consolidated hub

• Small adjustment to messages action wiring consistent with the new Messages hub placement and Fairway-only rendering.

src/app/baseball/actions/messages.ts

BaseballShellLayout.tsxDelete legacy BaseballShellLayout +0/-87

Delete legacy BaseballShellLayout

• Removes the legacy shell component now that BaseballFairwayShell is mounted unconditionally.

src/components/baseball/BaseballShellLayout.tsx

NotificationBell.tsxMinor notification bell adjustments +2/-3

Minor notification bell adjustments

• Small tweaks to align notifications UI behavior with consolidated shell/nav expectations.

src/components/baseball/NotificationBell.tsx

ReadModelStateNotice.tsxAdjust read-model notice presentation for Living Annual conventions +18/-14

Adjust read-model notice presentation for Living Annual conventions

• Updates the notice component styling/behavior to fit the new “no amber box” and Living Annual presentation patterns used in migrated pages.

src/components/baseball/ReadModelStateNotice.tsx

BaseballCalendarWrapper.tsxCalendar wrapper adjustments for unified calendar rendering +18/-2

Calendar wrapper adjustments for unified calendar rendering

• Minor updates to calendar wrapper behavior consistent with always-on CalendarFairway usage and new server-side mapping defaults.

src/components/baseball/calendar/BaseballCalendarWrapper.tsx

dashboard-shell.tsxDelete legacy BaseballDashboardShell +0/-268

Delete legacy BaseballDashboardShell

• Removes the legacy dashboard shell component now superseded by BaseballFairwayShell.

src/components/baseball/dashboard-shell.tsx

dashboard-types.tsDelete orphaned dashboard types +0/-61

Delete orphaned dashboard types

• Removes unused dashboard types module confirmed orphaned after consolidation.

src/components/baseball/dashboard/dashboard-types.ts

RuledStatLine.tsxMinor RuledStatLine tweak +1/-1

Minor RuledStatLine tweak

• Small change to Living Annual stat line rendering behavior to support updated stats/aggregate presentation.

src/components/baseball/living-annual/RuledStatLine.tsx

LiveWeightRoom.tsxDelete legacy baseball LiveWeightRoom component +0/-1203

Delete legacy baseball LiveWeightRoom component

• Removes the duplicated baseball-specific Live Weight Room implementation now replaced by canonical lifting components.

src/components/baseball/performance/LiveWeightRoom.tsx

PlayerLiftHomeClient.tsxDelete legacy baseball PlayerLiftHomeClient +0/-206

Delete legacy baseball PlayerLiftHomeClient

• Removes the duplicated baseball-specific player lift home component now replaced by canonical lifting UI.

src/components/baseball/performance/PlayerLiftHomeClient.tsx

PlayerLiftSessionClient.tsxDelete legacy baseball PlayerLiftSessionClient +0/-616

Delete legacy baseball PlayerLiftSessionClient

• Removes the duplicated baseball-specific lift execution client now replaced by canonical lifting UI.

src/components/baseball/performance/PlayerLiftSessionClient.tsx

ProgramEditorClient.tsxDelete legacy baseball ProgramEditorClient +0/-1252

Delete legacy baseball ProgramEditorClient

• Removes the duplicated baseball program editor implementation now replaced by canonical lifting program editor.

src/components/baseball/performance/ProgramEditorClient.tsx

ProgramListClient.tsxDelete legacy baseball ProgramListClient +0/-323

Delete legacy baseball ProgramListClient

• Removes the duplicated baseball program list implementation now replaced by canonical lifting program list.

src/components/baseball/performance/ProgramListClient.tsx

StrengthGroupsClient.tsxDelete legacy baseball StrengthGroupsClient +0/-766

Delete legacy baseball StrengthGroupsClient

• Removes the duplicated baseball strength groups implementation now replaced by canonical lifting groups client.

src/components/baseball/performance/StrengthGroupsClient.tsx

MatchScoreBadge.tsxDelete orphaned MatchScoreBadge component +0/-267

Delete orphaned MatchScoreBadge component

• Removes an unused recruiting philosophy UI component confirmed orphaned.

src/components/baseball/recruiting-philosophy/MatchScoreBadge.tsx

sidebar.tsxDelete legacy shared sidebar implementation (baseball nav arrays) +0/-795

Delete legacy shared sidebar implementation (baseball nav arrays)

• Removes the old shared sidebar file containing legacy baseball navigation arrays and related composition. Part of eliminating the redesign fork and ensuring baseball navigation comes from the registry + Fairway shell.

src/components/layout/sidebar.tsx

team-switcher.tsxDelete unused team switcher component +0/-227

Delete unused team switcher component

• Removes an unreferenced component from the legacy layout stack, reducing dead code post-shell unification.

src/components/layout/team-switcher.tsx

use-baseball-auth.tsMinor baseball auth hook alignment +1/-1

Minor baseball auth hook alignment

• Small change to keep auth hook behavior consistent with the now-unconditional Fairway shell mounting.

src/hooks/use-baseball-auth.ts

Documentation (1) +88 / -0
COHERENCE_RULING_2026-07-08.mdAdd governing coherence ruling document +88/-0

Add governing coherence ruling document

• Introduces the authoritative consolidation ruling for shell unification, capped nav IA, one Lifting Lab, and data-honesty fixes. Defines the hub/subtab table, folding rules, and deletion scope used throughout the PR.

docs/baseball/COHERENCE_RULING_2026-07-08.md

Other (18) +1091 / -54
README.mdDocument service-role teardown for E2E game creation +26/-11

Document service-role teardown for E2E game creation

• Updates E2E docs to describe how the box-score spec now cleans up created games/events via a service-role Supabase client. Records prior DB pollution and the rationale for teardown not failing runs when secrets are absent.

e2e/README.md

baseball-box-score.spec.tsAdd afterAll teardown to delete E2E-created games/events +57/-4

Add afterAll teardown to delete E2E-created games/events

• Tracks opponent names created by the spec and deletes the corresponding baseball_games rows and linked baseball_events rows in afterAll. Uses a service-role Supabase client when env vars are available, otherwise no-ops to avoid failing CI.

e2e/baseball-box-score.spec.ts

page.test.tsxAdd regression tests for readiness day semantics +99/-0

Add regression tests for readiness day semantics

• Adds test coverage to lock team-local “today” behavior and related readiness page invariants.

src/app/baseball/(dashboard)/dashboard/readiness/tests/page.test.tsx

route-shell-contract.test.tsUpdate route-shell contract test for unified shell naming +0/-1

Update route-shell contract test for unified shell naming

• Adjusts a test expectation/comment to align with BaseballFairwayShell as the single shell.

src/app/baseball/actions/tests/route-shell-contract.test.ts

groups.test.tsAdd test coverage for lifting group actions +125/-0

Add test coverage for lifting group actions

• Adds unit tests around canonical lifting group action behavior, supporting the increased usage from BaseballHelm routes.

src/app/lifting/actions/tests/groups.test.ts

player-sessions.test.tsAdd test coverage for lifting player-session actions +32/-0

Add test coverage for lifting player-session actions

• Adds unit tests around self-service lifting session mutations used by baseball player lift flows.

src/app/lifting/actions/tests/player-sessions.test.ts

programs.test.tsAdd test coverage for lifting program actions +27/-0

Add test coverage for lifting program actions

• Adds unit tests around canonical lifting program mutations used by baseball coach program tooling.

src/app/lifting/actions/tests/programs.test.ts

sessions.test.tsAdd test coverage for lifting session actions +31/-0

Add test coverage for lifting session actions

• Adds unit tests around canonical lifting session lifecycle and logging actions used by Live Weight Room.

src/app/lifting/actions/tests/sessions.test.ts

StrengthGroupsClient.dynamic.test.tsxAdd tests for dynamic group read-only UI behavior +168/-0

Add tests for dynamic group read-only UI behavior

• Adds test coverage ensuring dynamic lifting groups render rule summaries and disable mutation affordances with explanatory tooltips.

src/components/lifting/groups/tests/StrengthGroupsClient.dynamic.test.tsx

use-baseball-auth.test.tsxUpdate baseball auth hook tests for unified shell usage +3/-3

Update baseball auth hook tests for unified shell usage

• Minor test updates to match the unified shell/layout behavior without redesign gating.

src/hooks/tests/use-baseball-auth.test.tsx

breadcrumbs.test.tsAdd unit tests for breadcrumb ID-guard and override precedence +122/-0

Add unit tests for breadcrumb ID-guard and override precedence

• Introduces tests ensuring UUID/numeric segments never render raw, parent-segment fallback is singularized, and override labels win. Adds regressions for moved routes (announcements/documents/travel) and home crumb behavior.

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

nav-manifest.test.tsLock ≤3 subtabs per hub and folded-route resolvability +107/-2

Lock ≤3 subtabs per hub and folded-route resolvability

• Extends nav-manifest tests to include the new Messages hub and adds a permanent anti-regression suite: raw and filtered coach hubs must be ≤3 tabs, player hubs ≤3, and every foldedUnder destination must still resolve to a hub via resolveActiveHub.

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

resolve-active-hub.test.tsUpdate active hub tests for Ruling 2 folding and Messages hub +137/-27

Update active hub tests for Ruling 2 folding and Messages hub

• Updates tests to reflect Performance moving to Development as Training, Stats Center folding of Season/Upload/Import, Team>Operations folds, Messages hub behavior, and Management settings consolidation into a single settings tab.

src/lib/baseball/tests/resolve-active-hub.test.ts

command-center.test.tsAdd tests for team-local week window in command center model +109/-1

Add tests for team-local week window in command center model

• Mocks Supabase server client and verifies includeWeekEvents computes Sunday–Saturday boundaries in team timezone (not server UTC), ensuring boundary events include/exclude correctly. Adds coverage for omitting weekEvents when disabled.

src/lib/baseball/read-models/tests/command-center.test.ts

database.tsUpdate generated DB types for new stats columns +6/-0

Update generated DB types for new stats columns

• Adds hit_by_pitch and sacrifice_flies to baseball_player_stats table types to match the new drift-fix migration and prevent PostgREST select mismatches.

src/lib/types/database.ts

baseball-route-inventory.tsAdjust route inventory helper for shell/nav consolidation +0/-5

Adjust route inventory helper for shell/nav consolidation

• Small update to the baseball route inventory helper to align with the updated route structure and consolidation expectations.

src/test/helpers/baseball-route-inventory.ts

20260708010000_bridge_baseball_coach_lifting_edit.sqlBridge baseball coaches into helm_lifting_org_viewers with edit access +36/-0

Bridge baseball coaches into helm_lifting_org_viewers with edit access

• Adds a migration that upserts helm_lifting_org_viewers rows granting baseball coaches edit access for their org and installs a trigger to keep the bridge updated on baseball_coaches inserts/updates. Revokes public execute on the trigger function for safety.

supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql

20260708011000_baseball_player_stats_drift_columns.sqlAdd missing baseball_player_stats columns (schema drift repair) +6/-0

Add missing baseball_player_stats columns (schema drift repair)

• Adds hit_by_pitch and sacrifice_flies columns with NOT NULL default 0 to match TypeScript expectations and prevent PostgREST 400s when selecting declared fields.

supabase/migrations/20260708011000_baseball_player_stats_drift_columns.sql

…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
@cursor

cursor Bot commented Jul 8, 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

qodo-code-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 97 rules

Grey Divider


Action required

1. supabase as any in calendar 📘 Rule violation ⚙ Maintainability
Description
The server action uses an explicit any cast (supabase as any) for database inserts, bypassing
type safety. This violates the project rule disallowing any and can mask incorrect payload shapes
or query usage.
Code

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

+      const { error: attendanceError } = await (supabase as any)
+        .from('baseball_event_attendance')
+        .insert(attendanceRecords);
Relevance

⭐⭐⭐ High

Team previously accepted removing (supabase as any) casts once DB types exist (recruit-documents).
Likely same here.

PR-#296

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519320 disallows using any. The changed server action performs inserts via
(supabase as any) in the added lines, introducing explicit any into the action code path.

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

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

## Issue description
`src/app/baseball/actions/calendar.ts` introduces explicit `any` casts (`supabase as any`) for inserts, violating the `any` ban and weakening type safety.

## Issue Context
The action already imports `fromUntyped` and can avoid `any` by using typed helpers and/or narrow, explicit row payload types for `insert()` calls.

## Fix Focus Areas
- src/app/baseball/actions/calendar.ts[240-253]

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


2. Lifting access not revoked 🐞 Bug ⛨ Security
Description
The new bridge_baseball_coach_lifting_access trigger only UPSERTs
helm_lifting_org_viewers(can_edit=true) and never removes the old viewer row when a coach’s
organization_id/user_id changes or is cleared, so users can retain edit access to orgs they no
longer belong to. Because Lifting Lab edit authorization is derived from
helm_lifting_org_viewers.can_edit, these orphaned rows become an authorization bug.
Code

supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql[R7-36]

+INSERT INTO public.helm_lifting_org_viewers (organization_id, user_id, sport, can_edit, granted_by)
+SELECT bc.organization_id, bc.user_id, 'baseball', true, 'migration:baseball-coach-lifting-bridge'
+FROM public.baseball_coaches bc
+WHERE bc.organization_id IS NOT NULL AND bc.user_id IS NOT NULL
+ON CONFLICT (organization_id, user_id, sport)
+DO UPDATE SET can_edit = true;
+
+CREATE OR REPLACE FUNCTION public.bridge_baseball_coach_lifting_access()
+RETURNS trigger
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path TO 'public', 'pg_temp'
+AS $$
+BEGIN
+  IF NEW.organization_id IS NOT NULL AND NEW.user_id IS NOT NULL THEN
+    INSERT INTO public.helm_lifting_org_viewers (organization_id, user_id, sport, can_edit, granted_by)
+    VALUES (NEW.organization_id, NEW.user_id, 'baseball', true, 'trigger:baseball-coach-lifting-bridge')
+    ON CONFLICT (organization_id, user_id, sport)
+    DO UPDATE SET can_edit = true;
+  END IF;
+  RETURN NEW;
+END;
+$$;
+
+REVOKE EXECUTE ON FUNCTION public.bridge_baseball_coach_lifting_access() FROM PUBLIC, anon, authenticated;
+
+DROP TRIGGER IF EXISTS trg_bridge_baseball_coach_lifting ON public.baseball_coaches;
+CREATE TRIGGER trg_bridge_baseball_coach_lifting
+AFTER INSERT OR UPDATE OF organization_id, user_id ON public.baseball_coaches
+FOR EACH ROW EXECUTE FUNCTION public.bridge_baseball_coach_lifting_access();
Relevance

⭐⭐⭐ High

Team has fixed “stale access persists after change” issues before (coach status guard). Likely
accept revocation logic.

PR-#611
PR-#517

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The migration only performs INSERT/UPSERT into helm_lifting_org_viewers on coach insert/update and
never deletes prior grants; resolveLiftingAccess treats any helm_lifting_org_viewers row with
can_edit=true as edit permission, so stale bridge rows directly grant ongoing edit rights.

supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql[7-36]
src/lib/lifting/access.ts[74-105]
src/lib/types/database.ts[2501-2546]

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 migration introduces a SECURITY DEFINER trigger that grants `helm_lifting_org_viewers(can_edit=true)` for baseball coaches, but it never revokes access when a coach leaves/changes org (organization_id/user_id changed, nulled, or row deleted). This can leave stale `helm_lifting_org_viewers` rows that still grant edit access.

## Issue Context
- The bridge is implemented via an UPSERT-only trigger on `public.baseball_coaches`.
- Lifting Lab edit access is computed from `helm_lifting_org_viewers.can_edit`.

## Fix approach
1. Add a follow-up migration (preferred, since this migration was already applied to prod per comments) that:
  - Replaces `public.bridge_baseball_coach_lifting_access()` to also handle updates where `OLD.organization_id/user_id` differ from `NEW` (delete the OLD mapping for sport='baseball').
  - Adds an `AFTER DELETE` trigger on `public.baseball_coaches` to delete the corresponding `helm_lifting_org_viewers` row for sport='baseball'.
  - Optionally limits deletions to rows where `granted_by` matches the bridge markers (e.g., `trigger:baseball-coach-lifting-bridge` / `migration:baseball-coach-lifting-bridge`) to avoid interfering with any future non-bridge grants.
2. Add a one-time cleanup statement to remove orphaned bridge-granted viewer rows that no longer have a matching `baseball_coaches` row for the same (user_id, organization_id).

## Fix Focus Areas
- supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql[7-36]
- src/lib/lifting/access.ts[74-105]

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


3. E2E teardown too broad 🐞 Bug ☼ Reliability
Description
The new Playwright teardown deletes baseball_games/baseball_events using a service-role client
by first selecting rows via opponent_name IN createdOpponents, which is not a guaranteed-unique
identifier across teams/environments. In a shared database, this can delete unrelated real rows if
an opponent_name collides or gets reused.
Code

e2e/baseball-box-score.spec.ts[R108-123]

+    const { data: games } = await supabase
+      .from('baseball_games')
+      .select('id, event_id')
+      .in('opponent_name', createdOpponents);
+
+    const gameIds = (games ?? []).map((g) => g.id);
+    const eventIds = (games ?? [])
+      .map((g) => g.event_id)
+      .filter((id): id is string => Boolean(id));
+
+    if (gameIds.length > 0) {
+      await supabase.from('baseball_games').delete().in('id', gameIds);
+    }
+    if (eventIds.length > 0) {
+      await supabase.from('baseball_events').delete().in('id', eventIds);
+    }
Relevance

⭐⭐ Medium

No historical evidence on E2E teardown scoping; repo recently stabilized E2E but cleanup patterns
unclear.

PR-#768
PR-#783

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The spec’s teardown uses a service-role Supabase client and identifies rows to delete by
opponent_name, then deletes the resulting IDs; the README confirms CI runs seeding/cleanup with
SUPABASE_SERVICE_ROLE_KEY, increasing the blast radius if the match is overly broad.

e2e/baseball-box-score.spec.ts[59-123]
e2e/baseball-box-score.spec.ts[145-158]
e2e/README.md[181-210]

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 E2E cleanup uses the service role key and finds rows to delete by matching `opponent_name`, which is not a safe uniqueness boundary. This creates a risk of deleting unrelated rows in shared DBs.

## Issue Context
- The test creates opponents via `E2E Created Opponent ${Date.now()}` and later deletes by opponent_name.
- The README documents CI uses `SUPABASE_SERVICE_ROLE_KEY` for seeding/cleanup.

## Fix approach
1. Track the created **game IDs** instead of opponent names:
  - After clicking "Create Game" and waiting for the redirect, parse the `gameId` from `page.url()` and push it into `createdGameIds`.
2. Teardown should:
  - Select `event_id` from `baseball_games` by `id IN createdGameIds` (optionally also `eq('team_id', SEEDED_TEAM_ID)` if available).
  - Delete by `id` for both tables.
3. As a safety belt, consider adding an additional filter (team_id, created_by, or a dedicated `e2e_run_id` column if you have one) so cleanup is provably scoped.

## Fix Focus Areas
- e2e/baseball-box-score.spec.ts[59-123]
- e2e/baseball-box-score.spec.ts[145-158]
- e2e/README.md[181-210]

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


View more (2)
4. Direct Supabase client creation 📘 Rule violation ⌂ Architecture
Description
New tooling code creates Supabase clients by importing createClient from @supabase/supabase-js
instead of using the shared server-side factory. This violates the compliance requirement for
centralized server-side Supabase client creation and increases risk of inconsistent auth/config
handling.
Code

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

+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';
+
+loadEnv({ path: '.env.local' });
+
+const DRY = !process.argv.includes('--confirm');
+
+interface AggregateRow {
+  player_id: string;
+  team_id: string;
+  career_avg: number | null;
+  career_obp: number | null;
+  career_slg: number | null;
+  career_ops: number | null;
+}
+
+function fmt(v: number | null): string {
+  return v == null ? '—' : v.toFixed(3);
+}
+
+function valuesEqual(a: number | null, b: number | null): boolean {
+  if (a == null && b == null) return true;
+  if (a == null || b == null) return false;
+  // Compare at the same 3-decimal precision the column/formula use.
+  return Math.abs(a - b) < 0.0005;
+}
+
+async function main() {
+  const url = (process.env.NEXT_PUBLIC_SUPABASE_URL ?? '').trim();
+  const key = (process.env.SUPABASE_SERVICE_ROLE_KEY ?? '').trim();
+  if (!url || !key) throw new Error('Missing NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY');
+  const supabase: SupabaseClient = createClient(url, key, {
+    auth: { persistSession: false, autoRefreshToken: false },
+  });
Relevance

⭐⭐ Medium

Repo hardens Supabase env/secret usage in scripts, but no prior enforcement of centralized
createClient factory for scripts.

PR-#112
PR-#774

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519229 requires server-side database access to use the shared server-side Supabase
client factory (@/lib/supabase/server) and forbids direct @supabase/supabase-js client creation.
The added script and E2E spec both import createClient from @supabase/supabase-js and
instantiate clients directly with URL/key.

Rule 1519229: Use the shared server-side Supabase client factory for all database access
scripts/backfill-baseball-slash-lines.ts[43-77]
e2e/baseball-box-score.spec.ts[1-64]

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 Supabase clients are being instantiated directly from `@supabase/supabase-js` in new tooling/test code, rather than using the shared server-side factory.

## Issue Context
The compliance rule requires server-side DB access to obtain clients via `createClient` from `@/lib/supabase/server` (`await createClient()`). For service-role workflows, centralize the service-role construction behind the shared factory (or a wrapper in that module) so scripts/tests don’t reimplement client creation.

## Fix Focus Areas
- scripts/backfill-baseball-slash-lines.ts[43-77]
- e2e/baseball-box-score.spec.ts[1-64]

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


5. builder: any in groups test 📘 Rule violation ⚙ Maintainability
Description
A new lifting actions test declares const builder: any = {}, introducing an explicit any type.
This violates the rule disallowing any and reduces the test’s ability to catch shape mistakes in
the mocked query builder.
Code

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

+function makeQueryBuilder(finalResult: FinalResult) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const builder: any = {};
+  const passthrough = ['select', 'eq', 'is'];
+  for (const m of passthrough) {
+    builder[m] = vi.fn(() => builder);
+  }
Relevance

⭐⭐ Medium

Some type/style suggestions in tests were rejected; no clear precedent specifically for removing
explicit any in test mocks.

PR-#776

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519320 disallows explicit any. The added test code introduces `const builder:
any = {}` as part of a new query-builder mock.

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

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 new test uses an explicit `any` type (`const builder: any = {}`), violating the `any` ban.

## Issue Context
The builder can be typed as a minimal interface (methods returning itself) or as `Record<string, unknown>` plus explicit method signatures, avoiding `any` while keeping the mock flexible.

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

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



Informational

6. helm_lifting_org_viewers lacks prefix 📘 Rule violation ⚙ Maintainability
Description
A new migration writes to public.helm_lifting_org_viewers, which does not follow the required
golf_/baseball_ table-name prefix convention. This violates the sport-specific table prefix rule
for SQL migrations.
Code

supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql[R7-12]

+INSERT INTO public.helm_lifting_org_viewers (organization_id, user_id, sport, can_edit, granted_by)
+SELECT bc.organization_id, bc.user_id, 'baseball', true, 'migration:baseball-coach-lifting-bridge'
+FROM public.baseball_coaches bc
+WHERE bc.organization_id IS NOT NULL AND bc.user_id IS NOT NULL
+ON CONFLICT (organization_id, user_id, sport)
+DO UPDATE SET can_edit = true;
Relevance

⭐ Low

Repo already uses shared helm_lifting_* tables in migrations; prefix-only baseball_/golf_ convention
likely not enforced for helm tables.

PR-#768
PR-#419

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519306 requires table identifiers referenced in migrations (including `INSERT
INTO) to start with golf_ or baseball_`. The new migration inserts into
public.helm_lifting_org_viewers, which does not match the required prefix pattern.

Rule 1519306: Enforce sport-specific table name prefixes
supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql[7-12]

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 migration references `public.helm_lifting_org_viewers`, which does not start with `golf_` or `baseball_` as required by the sport-specific prefix policy.

## Issue Context
To comply, the migration’s DML should target a table name that follows the required prefix convention (or introduce a prefixed table/view/synonym and update the trigger/function to use it).

## Fix Focus Areas
- supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql[7-26]

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


Grey Divider

Qodo Logo

Comment on lines +241 to +243
const { error: attendanceError } = await (supabase as any)
.from('baseball_event_attendance')
.insert(attendanceRecords);

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 as any in calendar 📘 Rule violation ⚙ Maintainability

The server action uses an explicit any cast (supabase as any) for database inserts, bypassing
type safety. This violates the project rule disallowing any and can mask incorrect payload shapes
or query usage.
Agent Prompt
## Issue description
`src/app/baseball/actions/calendar.ts` introduces explicit `any` casts (`supabase as any`) for inserts, violating the `any` ban and weakening type safety.

## Issue Context
The action already imports `fromUntyped` and can avoid `any` by using typed helpers and/or narrow, explicit row payload types for `insert()` calls.

## Fix Focus Areas
- src/app/baseball/actions/calendar.ts[240-253]

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

Comment on lines +22 to +28
function makeQueryBuilder(finalResult: FinalResult) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const builder: any = {};
const passthrough = ['select', 'eq', 'is'];
for (const m of passthrough) {
builder[m] = vi.fn(() => builder);
}

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 groups test 📘 Rule violation ⚙ Maintainability

A new lifting actions test declares const builder: any = {}, introducing an explicit any type.
This violates the rule disallowing any and reduces the test’s ability to catch shape mistakes in
the mocked query builder.
Agent Prompt
## Issue description
The new test uses an explicit `any` type (`const builder: any = {}`), violating the `any` ban.

## Issue Context
The builder can be typed as a minimal interface (methods returning itself) or as `Record<string, unknown>` plus explicit method signatures, avoiding `any` while keeping the mock flexible.

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

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

Comment on lines +43 to +77
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';

loadEnv({ path: '.env.local' });

const DRY = !process.argv.includes('--confirm');

interface AggregateRow {
player_id: string;
team_id: string;
career_avg: number | null;
career_obp: number | null;
career_slg: number | null;
career_ops: number | null;
}

function fmt(v: number | null): string {
return v == null ? '—' : v.toFixed(3);
}

function valuesEqual(a: number | null, b: number | null): boolean {
if (a == null && b == null) return true;
if (a == null || b == null) return false;
// Compare at the same 3-decimal precision the column/formula use.
return Math.abs(a - b) < 0.0005;
}

async function main() {
const url = (process.env.NEXT_PUBLIC_SUPABASE_URL ?? '').trim();
const key = (process.env.SUPABASE_SERVICE_ROLE_KEY ?? '').trim();
if (!url || !key) throw new Error('Missing NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY');
const supabase: SupabaseClient = createClient(url, key, {
auth: { persistSession: false, autoRefreshToken: false },
});

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

3. Direct supabase client creation 📘 Rule violation ⌂ Architecture

New tooling code creates Supabase clients by importing createClient from @supabase/supabase-js
instead of using the shared server-side factory. This violates the compliance requirement for
centralized server-side Supabase client creation and increases risk of inconsistent auth/config
handling.
Agent Prompt
## Issue description
Server-side Supabase clients are being instantiated directly from `@supabase/supabase-js` in new tooling/test code, rather than using the shared server-side factory.

## Issue Context
The compliance rule requires server-side DB access to obtain clients via `createClient` from `@/lib/supabase/server` (`await createClient()`). For service-role workflows, centralize the service-role construction behind the shared factory (or a wrapper in that module) so scripts/tests don’t reimplement client creation.

## Fix Focus Areas
- scripts/backfill-baseball-slash-lines.ts[43-77]
- e2e/baseball-box-score.spec.ts[1-64]

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

Comment on lines +7 to +36
INSERT INTO public.helm_lifting_org_viewers (organization_id, user_id, sport, can_edit, granted_by)
SELECT bc.organization_id, bc.user_id, 'baseball', true, 'migration:baseball-coach-lifting-bridge'
FROM public.baseball_coaches bc
WHERE bc.organization_id IS NOT NULL AND bc.user_id IS NOT NULL
ON CONFLICT (organization_id, user_id, sport)
DO UPDATE SET can_edit = true;

CREATE OR REPLACE FUNCTION public.bridge_baseball_coach_lifting_access()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $$
BEGIN
IF NEW.organization_id IS NOT NULL AND NEW.user_id IS NOT NULL THEN
INSERT INTO public.helm_lifting_org_viewers (organization_id, user_id, sport, can_edit, granted_by)
VALUES (NEW.organization_id, NEW.user_id, 'baseball', true, 'trigger:baseball-coach-lifting-bridge')
ON CONFLICT (organization_id, user_id, sport)
DO UPDATE SET can_edit = true;
END IF;
RETURN NEW;
END;
$$;

REVOKE EXECUTE ON FUNCTION public.bridge_baseball_coach_lifting_access() FROM PUBLIC, anon, authenticated;

DROP TRIGGER IF EXISTS trg_bridge_baseball_coach_lifting ON public.baseball_coaches;
CREATE TRIGGER trg_bridge_baseball_coach_lifting
AFTER INSERT OR UPDATE OF organization_id, user_id ON public.baseball_coaches
FOR EACH ROW EXECUTE FUNCTION public.bridge_baseball_coach_lifting_access();

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. Lifting access not revoked 🐞 Bug ⛨ Security

The new bridge_baseball_coach_lifting_access trigger only UPSERTs
helm_lifting_org_viewers(can_edit=true) and never removes the old viewer row when a coach’s
organization_id/user_id changes or is cleared, so users can retain edit access to orgs they no
longer belong to. Because Lifting Lab edit authorization is derived from
helm_lifting_org_viewers.can_edit, these orphaned rows become an authorization bug.
Agent Prompt
## Issue description
The migration introduces a SECURITY DEFINER trigger that grants `helm_lifting_org_viewers(can_edit=true)` for baseball coaches, but it never revokes access when a coach leaves/changes org (organization_id/user_id changed, nulled, or row deleted). This can leave stale `helm_lifting_org_viewers` rows that still grant edit access.

## Issue Context
- The bridge is implemented via an UPSERT-only trigger on `public.baseball_coaches`.
- Lifting Lab edit access is computed from `helm_lifting_org_viewers.can_edit`.

## Fix approach
1. Add a follow-up migration (preferred, since this migration was already applied to prod per comments) that:
   - Replaces `public.bridge_baseball_coach_lifting_access()` to also handle updates where `OLD.organization_id/user_id` differ from `NEW` (delete the OLD mapping for sport='baseball').
   - Adds an `AFTER DELETE` trigger on `public.baseball_coaches` to delete the corresponding `helm_lifting_org_viewers` row for sport='baseball'.
   - Optionally limits deletions to rows where `granted_by` matches the bridge markers (e.g., `trigger:baseball-coach-lifting-bridge` / `migration:baseball-coach-lifting-bridge`) to avoid interfering with any future non-bridge grants.
2. Add a one-time cleanup statement to remove orphaned bridge-granted viewer rows that no longer have a matching `baseball_coaches` row for the same (user_id, organization_id).

## Fix Focus Areas
- supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql[7-36]
- src/lib/lifting/access.ts[74-105]

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

Comment on lines +108 to +123
const { data: games } = await supabase
.from('baseball_games')
.select('id, event_id')
.in('opponent_name', createdOpponents);

const gameIds = (games ?? []).map((g) => g.id);
const eventIds = (games ?? [])
.map((g) => g.event_id)
.filter((id): id is string => Boolean(id));

if (gameIds.length > 0) {
await supabase.from('baseball_games').delete().in('id', gameIds);
}
if (eventIds.length > 0) {
await supabase.from('baseball_events').delete().in('id', eventIds);
}

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. E2e teardown too broad 🐞 Bug ☼ Reliability

The new Playwright teardown deletes baseball_games/baseball_events using a service-role client
by first selecting rows via opponent_name IN createdOpponents, which is not a guaranteed-unique
identifier across teams/environments. In a shared database, this can delete unrelated real rows if
an opponent_name collides or gets reused.
Agent Prompt
## Issue description
The E2E cleanup uses the service role key and finds rows to delete by matching `opponent_name`, which is not a safe uniqueness boundary. This creates a risk of deleting unrelated rows in shared DBs.

## Issue Context
- The test creates opponents via `E2E Created Opponent ${Date.now()}` and later deletes by opponent_name.
- The README documents CI uses `SUPABASE_SERVICE_ROLE_KEY` for seeding/cleanup.

## Fix approach
1. Track the created **game IDs** instead of opponent names:
   - After clicking "Create Game" and waiting for the redirect, parse the `gameId` from `page.url()` and push it into `createdGameIds`.
2. Teardown should:
   - Select `event_id` from `baseball_games` by `id IN createdGameIds` (optionally also `eq('team_id', SEEDED_TEAM_ID)` if available).
   - Delete by `id` for both tables.
3. As a safety belt, consider adding an additional filter (team_id, created_by, or a dedicated `e2e_run_id` column if you have one) so cleanup is provably scoped.

## Fix Focus Areas
- e2e/baseball-box-score.spec.ts[59-123]
- e2e/baseball-box-score.spec.ts[145-158]
- e2e/README.md[181-210]

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

…-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
@cursor

cursor Bot commented Jul 8, 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 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: 21

Caution

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

⚠️ Outside diff range comments (1)
src/app/baseball/actions/calendar.ts (1)

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

Use fromUntyped() for the secondary baseball writes too. src/app/baseball/actions/calendar.ts:240-251 already has the helper in scope, and src/lib/supabase/untyped.ts accepts these table names, so the (supabase as any) casts and eslint-disable-next-line comments are redundant.

🤖 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 240 - 267, The secondary
baseball insert paths in the calendar action still cast `supabase` to `any` and
disable linting, but `fromUntyped()` is already available in `calendar.ts` and
should be used for both the `baseball_event_attendance` and `baseball_games`
writes. Update those insert calls to use `fromUntyped()` directly, remove the
redundant `eslint-disable-next-line` comments and `any` casts, and keep the
existing error handling and revalidation behavior unchanged.
♻️ Duplicate comments (1)
src/app/baseball/(dashboard)/dashboard/operations/page.tsx (1)

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

Same duplication concern as scouting/page.tsx.

See the refactor comment on src/app/baseball/(dashboard)/dashboard/scouting/page.tsx — this file is the second (of at least two, likely three counting /dashboard/settings) byte-for-byte-structural copy of the hub-landing-grid pattern. Same HubLandingGrid extraction applies here; only OPERATIONS_CARD_IDS, ink="team", and the copy differ from the Scouting page.

🤖 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/operations/page.tsx around lines 1 -
117, This page is duplicating the same hub-landing card-grid pattern seen in the
Scouting landing, so extract the shared rendering into the existing
HubLandingGrid-style component or helper and reuse it here. Keep
Operations-specific data only in OPERATIONS_CARD_IDS, the masthead copy, and the
team ink/theme; move the shared requireBaseballCoachRoute,
getBaseballNavContext, getBaseballNavEntry, isBaseballNavEntryVisible, and
cards-to-PaperCard/Link rendering logic into the common abstraction. Use
OperationsPage and the shared grid component to locate the split points cleanly.
🤖 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 95-119: Teardown is still identifying rows by opponent_name
instead of the created game’s primary key, which can delete the wrong record
when names collide across runs. Update the cleanup logic in test.afterAll to
track the inserted baseball_games id from the create flow in
e2e/baseball-box-score.spec.ts, then query/delete by that id rather than using
.in('opponent_name', createdOpponents). Keep the linked baseball_events cleanup,
but derive event deletion from the captured game record so the teardown only
removes rows created by this test.

In `@scripts/backfill-baseball-slash-lines.ts`:
- Around line 146-161: The per-row update failures in main() are only counted in
errors and logged, but they never affect the script’s exit status. Update the
end-of-run flow in scripts/backfill-baseball-slash-lines.ts so that the main()
summary checks errors after the loop and exits non-zero when any updates failed,
while preserving the existing DRY run behavior. Use the existing errors counter
and the main()/process.exit handling near the bottom of the script to make the
failure signal explicit for CI and ops tooling.
- Around line 81-90: The scan of baseball_player_aggregates in the backfill
script is unbounded and can hit the PostgREST 1000-row cap, causing later rows
to be skipped. Update the backfill flow in
scripts/backfill-baseball-slash-lines.ts to fetch rows in pages with a stable
.order() (or switch to the shared fetchAllRowsResult helper) before entering the
backfill loop, so all AggregateRow records are processed consistently across
reruns.

In `@scripts/e2e-supabase-admin.ts`:
- Around line 21-26: getE2eAdminClient currently reads NEXT_PUBLIC_SUPABASE_URL
and SUPABASE_SERVICE_ROLE_KEY directly from process.env, which skips .env.local
for local runs. Update the env resolution in getE2eAdminClient so it loads
values from .env.local first and falls back to process.env, then keep the
existing null guard and createClient call unchanged.

In `@src/app/baseball/`(dashboard)/_components/breadcrumb-label.tsx:
- Around line 26-55: The BreadcrumbLabelContext value in BreadcrumbLabelProvider
causes all consumers to re-render on any label change because labels is shared
in a single context value. If this pattern will be reused with multiple
simultaneous consumers, split read/write concerns or provide a more selective
subscription so only the affected pathname consumer updates; use
BreadcrumbLabelProvider, BreadcrumbLabelContext, and the setLabel/clearLabel
state updates as the main touchpoints.

In `@src/app/baseball/`(dashboard)/dashboard/calendar/page.tsx:
- Around line 22-26: The page-level defaultEndTime helper is duplicated with the
same logic in useCalendarEvents, so consolidate it into
src/hooks/useCalendarEvents.ts by exporting the shared helper there and update
the calendar page to import and use that exported function instead of defining
its own copy. Keep the existing CalendarEvent import usage aligned with the
shared module so the fallback logic lives in one place and can’t drift.

In `@src/app/baseball/`(dashboard)/dashboard/dev-plan/DevPlanClient.tsx:
- Around line 70-90: getDaysUntil is still parsing date-only strings through new
Date(dateStr), which causes UTC-to-local drift and off-by-one labels for non-UTC
users. Update getDaysUntil in DevPlanClient to parse the YYYY-MM-DD components
directly and build the target date in local time before calculating diffDays,
instead of mutating a UTC-parsed Date with setHours. Keep the existing return
shape and label logic, but make sure the day calculation is based on a locally
constructed date so overdue/due-today status stays accurate.

In `@src/app/baseball/`(dashboard)/dashboard/lift/_lift-athlete-context.ts:
- Around line 33-44: The team lookup in resolvePlayerTeamId is too ambiguous
because it queries baseball_team_members by player_id alone and then relies on
maybeSingle, which can fail when a player has multiple memberships. Update this
helper to explicitly select the active/latest membership (using the existing
team membership fields/ordering in baseball_team_members) before returning
team_id, so the Lift context resolves a single team deterministically instead of
collapsing to null.

In `@src/app/baseball/`(dashboard)/dashboard/lift/[sessionId]/page.tsx:
- Around line 35-79: `fetchSessionWithExercises` is ignoring Supabase `error`
values on the session, exercise, and set-result queries, which can turn real DB
failures into false “missing” or empty results. Update the reads in
`fetchSessionWithExercises` to destructure and check `error` separately from
`data`, log failures, and return/throw appropriately instead of treating them as
empty data. Keep `notFound()` behavior only for the successful no-row case when
the primary session lookup returns null, and make sure the downstream page logic
that calls `fetchSessionWithExercises` still distinguishes errors from a truly
missing session.

In `@src/app/baseball/`(dashboard)/dashboard/lift/page.tsx:
- Around line 79-109: The LiftOnboardingGate usage in the dashboard lift page is
passing through a possibly-null athleteId from getPlayerLiftOnboardingState,
which can cause readLiftOnboardingFlag/writeLiftOnboardingFlag to share state
under a literal null key. Update the render logic around PlayerLiftHomeClient
and LiftOnboardingGate so the gate only mounts when a real athleteId is
available, and otherwise skip the onboarding gate entirely for unresolved
athletes; use the existing getPlayerLiftOnboardingState and LiftOnboardingGate
symbols to keep the fix localized.
- Around line 37-70: The fetchPlayerSessions helper is swallowing Supabase
failures by ignoring the error from both helm_lifting_sessions queries, which
makes RLS denials, timeouts, or bad filters look like genuine empty results.
Update fetchPlayerSessions in dashboard/lift/page.tsx to check the error
returned by each fromUntyped call, log or propagate it with clear context, and
avoid returning empty upcoming/recent arrays when the query actually failed;
keep the successful-path behavior for real no-data cases.

In `@src/app/baseball/`(dashboard)/dashboard/performance/live/page.tsx:
- Around line 104-109: The query in live/page.tsx is filtering
helm_lifting_set_results by session_id even though the available row shape and
current live-session data use session_exercise_id. Update the data flow around
the performance live page to collect the relevant session_exercise ids from
helm_lifting_session_exercises and use those ids in the fromUntyped(...,
'helm_lifting_set_results') query instead of sessionIds. Keep the existing
select/order shape, but make the filter align with HelmLiftingSetResultRow and
the surrounding session_exercises logic so actual_load and rpe populate
correctly.
- Around line 63-69: The live performance query is using UTC-based “today,”
which can shift `scheduled_date`, `checkin_date`, and availability filtering for
non-UTC teams. Update
`src/app/baseball/(dashboard)/dashboard/performance/live/page.tsx` to derive the
date using the team’s timezone via `resolveTeamTimezone()` and `todayIsoInTz()`
from `src/lib/baseball/daily-contract/contract-day.ts` instead of `new
Date().toISOString()`. Apply the team-local date consistently before the
`fromUntyped(...).select('*')` query and any related filters so `today` matches
the team’s calendar day.

In `@src/app/baseball/`(dashboard)/dashboard/readiness/__tests__/page.test.tsx:
- Around line 44-59: The test is re-implementing the Supabase client inline
instead of using the shared fake. Replace the local vi.mock for createClient in
page.test.tsx with createFakeSupabase from src/test/fixtures/fake-supabase, and
seed baseball_teams there so the test uses the shared
.from().select().eq().maybeSingle contract instead of duplicating it.

In `@src/app/baseball/`(dashboard)/dashboard/scouting/page.tsx:
- Around line 1-122: The Scouting page duplicates the same hub-landing grid
pattern already used by the Operations landing, so extract the shared
presentational logic into a reusable helper such as HubLandingGrid in the Living
Annual components area. Move the common card grid,
Link/PaperCard/IconChevronRight rendering, empty state, and nav-context
filtering pattern out of ScoutingPage and let each hub page pass only its own
card list, ink/theme classes, and empty-state copy. Keep the route guard and
page-specific registry/card-id setup in ScoutingPage, and reference the shared
BaseballNavEntry-based card shape so the extracted component stays compatible
with the existing nav registry.

In `@src/app/lifting/actions/__tests__/groups.test.ts`:
- Around line 92-125: Add regression coverage for the member-mutation actions in
the groups test suite: the current `createGroup`, `updateGroup`, and
`deleteGroup` checks do not verify the `addGroupMember` and `removeGroupMember`
revalidation behavior mentioned by the suite header. Extend this test file with
cases that call `addGroupMember` and `removeGroupMember`, then assert
`revalidatePath` includes both the lifting groups path and the baseball
performance groups path, matching the existing pattern used for the other
`groups` actions.

In `@src/components/baseball/calendar/CalendarFairway.tsx`:
- Around line 79-98: The recruiting-empty state in CalendarFairway is missing
the required icon, so update the `recruitingEmpty` branch to satisfy the compact
empty-state pattern. Either pass an icon into `EditorsLetter` alongside the
existing `title`, `body`, and `action`, or, if this pattern is meant to apply
broadly, add an `icon` prop support to the shared `EditorsLetter` component and
use it here. Keep the location tied to `CalendarFairway` and `EditorsLetter` so
the empty state remains “icon + short title + 1-sentence + 1 CTA.”

In `@src/components/lifting/groups/StrengthGroupsClient.tsx`:
- Around line 412-435: The UI in StrengthGroupsClient only blocks
Archive/Add/Remove for dynamic groups, but the server actions still allow direct
mutations. Update the server-side guards in groups.ts, specifically the
archive/add/remove action handlers, to check selected group/group_type before
any writes to helm_lifting_groups or helm_lifting_group_members. Reuse the same
dynamic-group check used by StrengthGroupsClient so rule-managed groups are
rejected even when actions are called directly.

In `@src/lib/baseball/__tests__/resolve-active-hub.test.ts`:
- Around line 212-221: The invariant test in resolve-active-hub.test.ts only
covers four coach hubs, so expand the parameterized cases in the COACH_*_TABS
check to include all hubs defined by COACH_HUB_ORDER from hub-definitions.ts,
including dashboard, team, recruiting, and academics. Update the test data
imports and assertions so every hub’s rendered tab list is verified to stay at 3
or fewer subtabs, not just the four currently listed.

In `@supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql`:
- Around line 17-19: The SECURITY DEFINER function in this migration uses the
wrong search_path setting and lacks the required inline justification. Update
the function definition to use the empty search path form required by the
migration rules, and add a brief inline comment next to the SECURITY DEFINER
declaration explaining why the elevation is necessary. Use the existing function
body reference and schema-qualified object usage in this migration to keep the
change minimal.
- Around line 14-36: The bridge trigger in
public.bridge_baseball_coach_lifting_access only grants can_edit on
insert/update and never revokes stale access when a coach changes org or is
deleted. Update the trigger logic around trg_bridge_baseball_coach_lifting so it
also handles OLD vs NEW organization_id/user_id on UPDATE to remove or downgrade
the previous helm_lifting_org_viewers grant, and add an AFTER DELETE trigger on
public.baseball_coaches to revoke the corresponding grant for the deleted row.
Be careful to scope revocation to only the bridge-derived permission so existing
independent access is not removed.

---

Outside diff comments:
In `@src/app/baseball/actions/calendar.ts`:
- Around line 240-267: The secondary baseball insert paths in the calendar
action still cast `supabase` to `any` and disable linting, but `fromUntyped()`
is already available in `calendar.ts` and should be used for both the
`baseball_event_attendance` and `baseball_games` writes. Update those insert
calls to use `fromUntyped()` directly, remove the redundant
`eslint-disable-next-line` comments and `any` casts, and keep the existing error
handling and revalidation behavior unchanged.

---

Duplicate comments:
In `@src/app/baseball/`(dashboard)/dashboard/operations/page.tsx:
- Around line 1-117: This page is duplicating the same hub-landing card-grid
pattern seen in the Scouting landing, so extract the shared rendering into the
existing HubLandingGrid-style component or helper and reuse it here. Keep
Operations-specific data only in OPERATIONS_CARD_IDS, the masthead copy, and the
team ink/theme; move the shared requireBaseballCoachRoute,
getBaseballNavContext, getBaseballNavEntry, isBaseballNavEntryVisible, and
cards-to-PaperCard/Link rendering logic into the common abstraction. Use
OperationsPage and the shared grid component to locate the split points cleanly.
🪄 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: 0d024fe4-118e-4768-bcd8-2e51743ccde4

📥 Commits

Reviewing files that changed from the base of the PR and between e63de60 and 063382f.

⛔ Files ignored due to path filters (1)
  • src/lib/types/database.ts is excluded by !src/lib/types/database.ts
📒 Files selected for processing (106)
  • docs/baseball/COHERENCE_RULING_2026-07-08.md
  • e2e/README.md
  • e2e/baseball-box-score.spec.ts
  • memory/projects/golfhelm.md
  • scripts/backfill-baseball-slash-lines.ts
  • scripts/baseballhelm-command-center.mjs
  • scripts/baseballhelm-loc.mjs
  • scripts/e2e-supabase-admin.ts
  • scripts/seed-rini-baseball-demo.ts
  • src/app/baseball/(coach-dashboard)/coach/layout.tsx
  • src/app/baseball/(coach-dashboard)/coach/template.tsx
  • src/app/baseball/(coach-dashboard)/error.tsx
  • src/app/baseball/(dashboard)/BaseballFairwayShell.tsx
  • src/app/baseball/(dashboard)/_components/breadcrumb-label.tsx
  • src/app/baseball/(dashboard)/_components/breadcrumbs.ts
  • src/app/baseball/(dashboard)/_components/hub-definitions.ts
  • src/app/baseball/(dashboard)/_components/resolve-active-hub.ts
  • src/app/baseball/(dashboard)/dashboard/academics/AcademicsClient.tsx
  • src/app/baseball/(dashboard)/dashboard/announcements/page.tsx
  • src/app/baseball/(dashboard)/dashboard/calendar/page.tsx
  • src/app/baseball/(dashboard)/dashboard/dev-plan/DevPlanClient.tsx
  • src/app/baseball/(dashboard)/dashboard/dev-plan/page.tsx
  • src/app/baseball/(dashboard)/dashboard/dev-plans/[id]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/documents/documents-client.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/messages/loading.tsx
  • src/app/baseball/(dashboard)/dashboard/messages/page.tsx
  • src/app/baseball/(dashboard)/dashboard/operations/page.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/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/players/[id]/profile/error.tsx
  • src/app/baseball/(dashboard)/dashboard/players/[id]/profile/page.tsx
  • src/app/baseball/(dashboard)/dashboard/players/[id]/stats/page.tsx
  • src/app/baseball/(dashboard)/dashboard/practice/page.tsx
  • src/app/baseball/(dashboard)/dashboard/readiness/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/readiness/page.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/RosterClient.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx
  • src/app/baseball/(dashboard)/dashboard/scouting/page.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/page.tsx
  • src/app/baseball/(dashboard)/dashboard/stats/games/[gameId]/page.tsx
  • src/app/baseball/(dashboard)/dashboard/travel/page.tsx
  • src/app/baseball/(dashboard)/layout.tsx
  • src/app/baseball/(player-dashboard)/player/layout.tsx
  • src/app/baseball/actions/__tests__/route-shell-contract.test.ts
  • src/app/baseball/actions/academics.ts
  • src/app/baseball/actions/calendar.ts
  • src/app/baseball/actions/games.ts
  • src/app/baseball/actions/messages.ts
  • src/app/lifting/actions/__tests__/groups.test.ts
  • src/app/lifting/actions/__tests__/player-sessions.test.ts
  • src/app/lifting/actions/__tests__/programs.test.ts
  • src/app/lifting/actions/__tests__/sessions.test.ts
  • src/app/lifting/actions/groups.ts
  • src/app/lifting/actions/player-sessions.ts
  • src/app/lifting/actions/programs.ts
  • src/app/lifting/actions/sessions.ts
  • src/components/baseball/BaseballShellLayout.tsx
  • src/components/baseball/NotificationBell.tsx
  • src/components/baseball/ReadModelStateNotice.tsx
  • src/components/baseball/calendar/BaseballCalendarWrapper.tsx
  • src/components/baseball/calendar/CalendarFairway.tsx
  • src/components/baseball/dashboard-shell.tsx
  • src/components/baseball/dashboard/dashboard-types.ts
  • src/components/baseball/documents/DocumentsFairway.tsx
  • src/components/baseball/living-annual/RuledStatLine.tsx
  • src/components/baseball/performance/LiveWeightRoom.tsx
  • src/components/baseball/performance/PlayerLiftHomeClient.tsx
  • src/components/baseball/performance/PlayerLiftSessionClient.tsx
  • src/components/baseball/performance/PlayerLiftToday.tsx
  • src/components/baseball/performance/ProgramEditorClient.tsx
  • src/components/baseball/performance/ProgramListClient.tsx
  • src/components/baseball/performance/StrengthGroupsClient.tsx
  • src/components/baseball/recruiting-philosophy/MatchScoreBadge.tsx
  • src/components/baseball/stats-center/StatsCenterClient.tsx
  • src/components/baseball/travel/TravelClient.tsx
  • src/components/layout/sidebar.tsx
  • src/components/layout/team-switcher.tsx
  • src/components/lifting/groups/StrengthGroupsClient.tsx
  • src/components/lifting/groups/__tests__/StrengthGroupsClient.dynamic.test.tsx
  • src/components/messages/EmptyChatState.tsx
  • src/contracts/baseball/product-trust.contract.test.ts
  • src/hooks/__tests__/use-baseball-auth.test.tsx
  • src/hooks/use-baseball-auth.ts
  • src/hooks/useCalendarEvents.ts
  • src/lib/baseball/__tests__/breadcrumbs.test.ts
  • src/lib/baseball/__tests__/nav-manifest.test.ts
  • src/lib/baseball/__tests__/resolve-active-hub.test.ts
  • src/lib/baseball/nav-manifest.ts
  • src/lib/baseball/nav-registry.ts
  • src/lib/baseball/read-models/__tests__/command-center.test.ts
  • src/lib/baseball/read-models/command-center.ts
  • src/lib/baseball/read-models/player-snapshot-cards.ts
  • src/lib/baseball/route-contract.ts
  • src/lib/baseball/stat-layer-manifest.ts
  • src/lib/recruiting/match-calculator.ts
  • src/test/helpers/baseball-route-inventory.ts
  • supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql
  • supabase/migrations/20260708011000_baseball_player_stats_drift_columns.sql
💤 Files with no reviewable changes (22)
  • src/components/baseball/performance/LiveWeightRoom.tsx
  • src/components/baseball/performance/ProgramListClient.tsx
  • src/components/baseball/performance/ProgramEditorClient.tsx
  • src/components/baseball/dashboard/dashboard-types.ts
  • src/components/baseball/performance/PlayerLiftHomeClient.tsx
  • scripts/baseballhelm-loc.mjs
  • src/components/baseball/BaseballShellLayout.tsx
  • src/app/baseball/actions/tests/route-shell-contract.test.ts
  • src/components/baseball/recruiting-philosophy/MatchScoreBadge.tsx
  • src/app/baseball/(dashboard)/dashboard/players/[id]/profile/page.tsx
  • src/app/baseball/(coach-dashboard)/error.tsx
  • src/test/helpers/baseball-route-inventory.ts
  • src/components/layout/team-switcher.tsx
  • src/components/baseball/performance/PlayerLiftSessionClient.tsx
  • src/app/baseball/(coach-dashboard)/coach/layout.tsx
  • src/components/baseball/dashboard-shell.tsx
  • src/app/baseball/(dashboard)/dashboard/players/[id]/profile/error.tsx
  • scripts/baseballhelm-command-center.mjs
  • src/lib/recruiting/match-calculator.ts
  • src/app/baseball/(coach-dashboard)/coach/template.tsx
  • src/components/baseball/performance/StrengthGroupsClient.tsx
  • src/components/layout/sidebar.tsx

Comment on lines +95 to +119
test.afterAll(async () => {
if (createdOpponents.length === 0) return;
const supabase = getServiceRoleClient();
if (!supabase) return;

// The create form defaults `create_calendar_event` to true, so each
// created game also has a linked baseball_events row (event_id) —
// delete both so no orphaned event survives the game's deletion.
const { data: games } = await supabase
.from('baseball_games')
.select('id, event_id')
.in('opponent_name', createdOpponents);

const gameIds = (games ?? []).map((g) => g.id);
const eventIds = (games ?? [])
.map((g) => g.event_id)
.filter((id): id is string => Boolean(id));

if (gameIds.length > 0) {
await supabase.from('baseball_games').delete().in('id', gameIds);
}
if (eventIds.length > 0) {
await supabase.from('baseball_events').delete().in('id', eventIds);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the opponent-name generator embeds enough entropy to avoid cross-worker collisions.
rg -n "opponent" e2e/baseball-box-score.spec.ts -B2 -A10 | rg -n "const opponent"

Repository: njrini99-code/helmv3

Length of output: 224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant section with line numbers and surrounding context.
sed -n '130,170p' e2e/baseball-box-score.spec.ts | cat -n

# Check whether the teardown already has any team-scoping data available nearby.
rg -n "team_id|opponent_name|createdOpponents|create_calendar_event|baseball_games|baseball_events" e2e/baseball-box-score.spec.ts

Repository: njrini99-code/helmv3

Length of output: 3035


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,130p' e2e/baseball-box-score.spec.ts | cat -n

Repository: njrini99-code/helmv3

Length of output: 6535


Delete teardown rows by game ID, not opponent_name. e2e/baseball-box-score.spec.ts:103-106 still matches on opponent_name, while e2e/baseball-box-score.spec.ts:141 only adds Date.now() entropy. That isn’t collision-proof across parallel runs, so teardown can delete the wrong game; capture the created game ID and delete by id instead.

🤖 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 95 - 119, Teardown is still
identifying rows by opponent_name instead of the created game’s primary key,
which can delete the wrong record when names collide across runs. Update the
cleanup logic in test.afterAll to track the inserted baseball_games id from the
create flow in e2e/baseball-box-score.spec.ts, then query/delete by that id
rather than using .in('opponent_name', createdOpponents). Keep the linked
baseball_events cleanup, but derive event deletion from the captured game record
so the teardown only removes rows created by this test.

Comment on lines +81 to +90
const { data: rows, error: fetchError } = await supabase
.from('baseball_player_aggregates')
.select('player_id, team_id, career_avg, career_obp, career_slg, career_ops')
.returns<AggregateRow[]>();

if (fetchError) throw fetchError;
if (!rows || rows.length === 0) {
console.log('No baseball_player_aggregates rows found — nothing to backfill.');
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm actual row count and the project's PostgREST row cap before deciding severity.
echo "Run against the target Supabase project:"
echo "select count(*) from baseball_player_aggregates;"

Repository: njrini99-code/helmv3

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspecting scripts/backfill-baseball-slash-lines.ts and nearby references"

file="scripts/backfill-baseball-slash-lines.ts"
if [ -f "$file" ]; then
  echo "== $file (lines 1-220) =="
  cat -n "$file" | sed -n '1,220p'
else
  echo "Missing: $file"
fi

echo
echo "== References to baseball_player_aggregates =="
rg -n "baseball_player_aggregates|db-max-rows|range\\(|select\\(" scripts src supabase memory .github -g '!**/node_modules/**' || true

echo
echo "== Files that look related to backfills or row-paging =="
fd -a "backfill-baseball-slash-lines.ts|baseball.*backfill|backfill" scripts src supabase memory .github || true

Repository: njrini99-code/helmv3

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="scripts/backfill-baseball-slash-lines.ts"

echo "== $file =="
cat -n "$file" | sed -n '1,220p'

echo
echo "== baseball_player_aggregates usages with context =="
rg -n -A3 -B3 "baseball_player_aggregates" \
  scripts src/lib/baseball src/app -g '!**/node_modules/**' || true

echo
echo "== any explicit pagination helpers used for Supabase row fetches =="
rg -n "range\\(|fetch-all-rows|db-max-rows|1000-row cap|paginate past PostgREST" \
  scripts src/lib/baseball src/app -g '!**/node_modules/**' || true

Repository: njrini99-code/helmv3

Length of output: 50376


Paginate the baseball_player_aggregates scan (scripts/backfill-baseball-slash-lines.ts:81-90)

select('player_id, team_id, career_avg, career_obp, career_slg, career_ops') is unbounded, so PostgREST will stop at its 1000-row cap. Any aggregates past the first page never reach the backfill loop, and reruns keep missing the same rows. Use a paged query with a stable .order() (or the shared fetchAllRowsResult helper) before iterating.

🤖 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 `@scripts/backfill-baseball-slash-lines.ts` around lines 81 - 90, The scan of
baseball_player_aggregates in the backfill script is unbounded and can hit the
PostgREST 1000-row cap, causing later rows to be skipped. Update the backfill
flow in scripts/backfill-baseball-slash-lines.ts to fetch rows in pages with a
stable .order() (or switch to the shared fetchAllRowsResult helper) before
entering the backfill loop, so all AggregateRow records are processed
consistently across reruns.

Comment on lines +146 to +161
if (!DRY) {
const { error: updateError } = await supabase
.from('baseball_player_aggregates')
.update({ career_obp: obp, career_slg: slg, career_ops: ops })
.eq('player_id', row.player_id)
.eq('team_id', row.team_id);

if (updateError) {
console.warn(` ⚠ ${label}: update failed — ${updateError.message}`);
errors++;
changed--;
}
}
}

console.log(`\n${DRY ? '[DRY RUN] ' : ''}Done. scanned=${scanned} changed=${changed} unchanged=${unchanged} errors=${errors}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Script exits 0 even when per-row updates fail.

errors is tracked (line 97, incremented at 119/155) but never affects the process exit code — only an uncaught exception in main() triggers process.exit(1) (line 179-182). A run with errors > 0 still exits cleanly, so CI/ops tooling invoking this script has no signal that some rows were skipped.

🚦 Fail loudly on partial errors
   console.log(`\n${DRY ? '[DRY RUN] ' : ''}Done. scanned=${scanned} changed=${changed} unchanged=${unchanged} errors=${errors}`);
+
+  if (errors > 0) {
+    console.error(`\n${errors} row(s) failed — exiting non-zero.`);
+    process.exitCode = 1;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!DRY) {
const { error: updateError } = await supabase
.from('baseball_player_aggregates')
.update({ career_obp: obp, career_slg: slg, career_ops: ops })
.eq('player_id', row.player_id)
.eq('team_id', row.team_id);
if (updateError) {
console.warn(` ⚠ ${label}: update failed — ${updateError.message}`);
errors++;
changed--;
}
}
}
console.log(`\n${DRY ? '[DRY RUN] ' : ''}Done. scanned=${scanned} changed=${changed} unchanged=${unchanged} errors=${errors}`);
if (!DRY) {
const { error: updateError } = await supabase
.from('baseball_player_aggregates')
.update({ career_obp: obp, career_slg: slg, career_ops: ops })
.eq('player_id', row.player_id)
.eq('team_id', row.team_id);
if (updateError) {
console.warn(` ⚠ ${label}: update failed — ${updateError.message}`);
errors++;
changed--;
}
}
}
console.log(`\n${DRY ? '[DRY RUN] ' : ''}Done. scanned=${scanned} changed=${changed} unchanged=${unchanged} errors=${errors}`);
if (errors > 0) {
console.error(`\n${errors} row(s) failed — exiting non-zero.`);
process.exitCode = 1;
}
🤖 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 `@scripts/backfill-baseball-slash-lines.ts` around lines 146 - 161, The per-row
update failures in main() are only counted in errors and logged, but they never
affect the script’s exit status. Update the end-of-run flow in
scripts/backfill-baseball-slash-lines.ts so that the main() summary checks
errors after the loop and exits non-zero when any updates failed, while
preserving the existing DRY run behavior. Use the existing errors counter and
the main()/process.exit handling near the bottom of the script to make the
failure signal explicit for CI and ops tooling.

Comment on lines +21 to +26
export function getE2eAdminClient() {
const url = (process.env.NEXT_PUBLIC_SUPABASE_URL ?? '').trim();
const key = (process.env.SUPABASE_SERVICE_ROLE_KEY ?? '').trim();
if (!url || !key) return null;
return createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } });
}

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

getE2eAdminClient reads process.env directly, bypassing .env.local.

Path instructions for scripts/**/*.{ts,mjs,js,sh} require: "Node scripts must read env from .env.local first, then process.env." Lines 22-23 go straight to process.env.NEXT_PUBLIC_SUPABASE_URL / process.env.SUPABASE_SERVICE_ROLE_KEY. Fine in CI (env exported directly per the file's own doc comment), but breaks local .env.local-only setups for anyone running these specs outside CI.

🔧 Proposed fix
+import { config } from 'dotenv';
+config({ path: '.env.local' });
+
 export function getE2eAdminClient() {
   const url = (process.env.NEXT_PUBLIC_SUPABASE_URL ?? '').trim();
   const key = (process.env.SUPABASE_SERVICE_ROLE_KEY ?? '').trim();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getE2eAdminClient() {
const url = (process.env.NEXT_PUBLIC_SUPABASE_URL ?? '').trim();
const key = (process.env.SUPABASE_SERVICE_ROLE_KEY ?? '').trim();
if (!url || !key) return null;
return createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } });
}
import { config } from 'dotenv';
config({ path: '.env.local' });
export function getE2eAdminClient() {
const url = (process.env.NEXT_PUBLIC_SUPABASE_URL ?? '').trim();
const key = (process.env.SUPABASE_SERVICE_ROLE_KEY ?? '').trim();
if (!url || !key) return null;
return createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } });
}
🤖 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 `@scripts/e2e-supabase-admin.ts` around lines 21 - 26, getE2eAdminClient
currently reads NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY directly
from process.env, which skips .env.local for local runs. Update the env
resolution in getE2eAdminClient so it loads values from .env.local first and
falls back to process.env, then keep the existing null guard and createClient
call unchanged.

Source: Path instructions

Comment on lines +26 to +55
interface BreadcrumbLabelContextValue {
labels: Readonly<Record<string, string>>;
setLabel: (pathname: string, name: string) => void;
clearLabel: (pathname: string) => void;
}

const BreadcrumbLabelContext = createContext<BreadcrumbLabelContextValue | null>(null);

/** Mounted once by BaseballFairwayShell, above both the shell chrome (reader)
* and `{children}` (writer) so both sides share one context instance. */
export function BreadcrumbLabelProvider({ children }: { children: React.ReactNode }) {
const [labels, setLabels] = useState<Readonly<Record<string, string>>>({});

const setLabel = useCallback((pathname: string, name: string) => {
setLabels((prev) => (prev[pathname] === name ? prev : { ...prev, [pathname]: name }));
}, []);

const clearLabel = useCallback((pathname: string) => {
setLabels((prev) => {
if (!(pathname in prev)) return prev;
const next = { ...prev };
delete next[pathname];
return next;
});
}, []);

const value = useMemo(() => ({ labels, setLabel, clearLabel }), [labels, setLabel, clearLabel]);

return <BreadcrumbLabelContext.Provider value={value}>{children}</BreadcrumbLabelContext.Provider>;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Context value re-renders all consumers on every label mutation.

value gets a new identity whenever any pathname's label changes (shared labels state), so useContext re-renders both the shell chrome reader and the currently-mounted page writer even when their own label didn't change. Harmless today since only one dynamic detail page is mounted at a time, but worth knowing if this pattern gets reused for a surface with multiple simultaneous consumers (e.g. parallel routes).

🤖 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/breadcrumb-label.tsx around lines
26 - 55, The BreadcrumbLabelContext value in BreadcrumbLabelProvider causes all
consumers to re-render on any label change because labels is shared in a single
context value. If this pattern will be reused with multiple simultaneous
consumers, split read/write concerns or provide a more selective subscription so
only the affected pathname consumer updates; use BreadcrumbLabelProvider,
BreadcrumbLabelContext, and the setLabel/clearLabel state updates as the main
touchpoints.

Comment on lines +79 to 98
<div className="flex-shrink-0 px-4 pt-4 md:px-6 md:pt-6">
<SectionMasthead eyebrow="THE WAR ROOM · CALENDAR" title="Calendar" ink="pursuit" />
</div>
<div className="flex flex-1 items-center justify-center p-6">
<EmptyState
icon={CalendarIcon}
{/* Not the `calendar` EmptyIssue preset ("No dates on the card") —
this is the recruiting-specific narrative (camp/official-visit
windows), so it stays a bespoke EditorsLetter like the other
Batch A surfaces' non-preset states (e.g. TasksFairway/
AnnouncementsFairway "no team selected"). */}
<EditorsLetter
ink="pursuit"
title="Your recruiting calendar is empty"
description="Camp visits and official visit windows will appear here as you schedule recruiting activity."
body="Camp visits and official visit windows will appear here as you schedule recruiting activity."
action={
<Button asChild variant="primary">
<Link href="/baseball/dashboard/discover">Browse prospects</Link>
</Button>
}
className="max-w-md"
/>

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

Recruiting-empty state has no icon — violates the "icon + short title + 1-sentence + 1 CTA" empty-state rule.

EditorsLetter (used here for the recruitingEmpty branch) has no icon prop at all — only title/body/signoff/action/live. This replaced an EmptyState that (per the AI summary) previously rendered a CalendarIcon. As per coding guidelines, src/components/**/*.{ts,tsx} requires: "Empty states must be compact: icon + short title + 1-sentence + 1 CTA." As currently written, this empty state has title + sentence + CTA but no icon.

If this bespoke editorial pattern (mirroring other Batch A "no team selected" states) is an intentional design-system exception for narrative empty states, that's fine — but it's worth confirming with the design-system doc rather than silently dropping the icon requirement here.

💡 One way to keep the icon without touching the shared EditorsLetter component
           <EditorsLetter
             ink="pursuit"
             title="Your recruiting calendar is empty"
             body="Camp visits and official visit windows will appear here as you schedule recruiting activity."
+            signoff="— add a camp or visit window from Discover to get started"
             action={
               <Button asChild variant="primary">
                 <Link href="/baseball/dashboard/discover">Browse prospects</Link>
               </Button>
             }
             className="max-w-md"
           />

Alternatively, add an icon prop to the shared EditorsLetter component if this pattern is meant to satisfy the compact-empty-state rule everywhere it's used.

🤖 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/calendar/CalendarFairway.tsx` around lines 79 - 98,
The recruiting-empty state in CalendarFairway is missing the required icon, so
update the `recruitingEmpty` branch to satisfy the compact empty-state pattern.
Either pass an icon into `EditorsLetter` alongside the existing `title`, `body`,
and `action`, or, if this pattern is meant to apply broadly, add an `icon` prop
support to the shared `EditorsLetter` component and use it here. Keep the
location tied to `CalendarFairway` and `EditorsLetter` so the empty state
remains “icon + short title + 1-sentence + 1 CTA.”

Source: Path instructions

Comment on lines 412 to 435
{canEdit && (
<Button
size="sm"
variant="ghost"
className="text-warm-400 hover:text-red-600"
disabled={isPending}
onClick={() => handleArchive(selectedGroup.id)}
>
<IconTrash size={14} className="mr-1" /> Archive
</Button>
isSelectedDynamic ? (
<Tooltip content={DYNAMIC_GROUP_TOOLTIP}>
<Button
size="sm"
variant="ghost"
className="text-warm-300"
disabled
>
<IconTrash size={14} className="mr-1" /> Archive
</Button>
</Tooltip>
) : (
<Button
size="sm"
variant="ghost"
className="text-warm-400 hover:text-red-600"
disabled={isPending}
onClick={() => handleArchive(selectedGroup.id)}
>
<IconTrash size={14} className="mr-1" /> Archive
</Button>
)
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "export async function (deleteGroup|addGroupMember|removeGroupMember)" -A 20 src/app/lifting/actions/groups.ts
rg -n "group_type" src/app/lifting/actions/groups.ts

Repository: njrini99-code/helmv3

Length of output: 612


🏁 Script executed:

#!/bin/bash
sed -n '1,280p' src/app/lifting/actions/groups.ts

Repository: njrini99-code/helmv3

Length of output: 10456


🏁 Script executed:

#!/bin/bash
sed -n '280,520p' src/app/lifting/actions/groups.ts

Repository: njrini99-code/helmv3

Length of output: 2248


🏁 Script executed:

#!/bin/bash
nl -ba src/app/lifting/actions/groups.ts | sed -n '120,320p'

Repository: njrini99-code/helmv3

Length of output: 194


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
path = Path('src/app/lifting/actions/groups.ts')
for i, line in enumerate(path.read_text().splitlines(), 1):
    if 120 <= i <= 320:
        print(f"{i}:{line}")
PY

Repository: njrini99-code/helmv3

Length of output: 8131


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
path = Path('src/app/lifting/actions/groups.ts')
for i, line in enumerate(path.read_text().splitlines(), 1):
    if 315 <= i <= 380:
        print(f"{i}:{line}")
PY

Repository: njrini99-code/helmv3

Length of output: 1161


Block dynamic-group writes in the server actions.

src/components/lifting/groups/StrengthGroupsClient.tsx:412-507 only disables Archive/Add/Remove in the UI. src/app/lifting/actions/groups.ts:213-243, 250-308, and 315-340 still mutate helm_lifting_groups / helm_lifting_group_members without checking group_type !== 'dynamic', so any direct action call can still edit a rule-managed group. Add the same guard server-side before those writes.

🤖 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/lifting/groups/StrengthGroupsClient.tsx` around lines 412 -
435, The UI in StrengthGroupsClient only blocks Archive/Add/Remove for dynamic
groups, but the server actions still allow direct mutations. Update the
server-side guards in groups.ts, specifically the archive/add/remove action
handlers, to check selected group/group_type before any writes to
helm_lifting_groups or helm_lifting_group_members. Reuse the same dynamic-group
check used by StrengthGroupsClient so rule-managed groups are rejected even when
actions are called directly.

Comment on lines +212 to 221
describe('every coach hub caps at 3 rendered subtabs (Ruling 2)', () => {
it.each([
['stats-performance', COACH_STATS_TABS],
['development', COACH_DEVELOPMENT_TABS],
['management', COACH_MANAGEMENT_TABS],
['messages', COACH_MESSAGES_TABS],
] as const)('%s renders ≤3 tabs', (_hub, tabs) => {
expect(tabs.length).toBeLessThanOrEqual(3);
});
});

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

"Caps at 3 subtabs" invariant only checked for 4 of 8 coach hubs.

COACH_HUB_ORDER (hub-definitions.ts) has 8 hubs: dashboard, messages, team, stats-performance, development, recruiting, academics, management. This parameterized test only asserts the cap for stats-performance, development, management, messages — the four hubs the ruling touched. team, recruiting, academics, and dashboard aren't verified here, so a future regression that pushes any of those past 3 tabs won't be caught by this specific guardrail (even though the PR's stated goal is "no hub exceeding three subtabs" system-wide).

✅ Proposed fix — cover all 8 hubs
 describe('every coach hub caps at 3 rendered subtabs (Ruling 2)', () => {
   it.each([
+    ['dashboard', COACH_DASHBOARD_TABS],
+    ['team', COACH_TEAM_TABS],
     ['stats-performance', COACH_STATS_TABS],
     ['development', COACH_DEVELOPMENT_TABS],
+    ['recruiting', COACH_RECRUITING_TABS],
+    ['academics', COACH_ACADEMICS_TABS],
     ['management', COACH_MANAGEMENT_TABS],
     ['messages', COACH_MESSAGES_TABS],
   ] as const)('%s renders ≤3 tabs', (_hub, tabs) => {
     expect(tabs.length).toBeLessThanOrEqual(3);
   });
 });

(Add the corresponding imports from hub-definitions.ts.)

🤖 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/lib/baseball/__tests__/resolve-active-hub.test.ts` around lines 212 -
221, The invariant test in resolve-active-hub.test.ts only covers four coach
hubs, so expand the parameterized cases in the COACH_*_TABS check to include all
hubs defined by COACH_HUB_ORDER from hub-definitions.ts, including dashboard,
team, recruiting, and academics. Update the test data imports and assertions so
every hub’s rendered tab list is verified to stay at 3 or fewer subtabs, not
just the four currently listed.

Comment on lines +14 to +36
CREATE OR REPLACE FUNCTION public.bridge_baseball_coach_lifting_access()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $$
BEGIN
IF NEW.organization_id IS NOT NULL AND NEW.user_id IS NOT NULL THEN
INSERT INTO public.helm_lifting_org_viewers (organization_id, user_id, sport, can_edit, granted_by)
VALUES (NEW.organization_id, NEW.user_id, 'baseball', true, 'trigger:baseball-coach-lifting-bridge')
ON CONFLICT (organization_id, user_id, sport)
DO UPDATE SET can_edit = true;
END IF;
RETURN NEW;
END;
$$;

REVOKE EXECUTE ON FUNCTION public.bridge_baseball_coach_lifting_access() FROM PUBLIC, anon, authenticated;

DROP TRIGGER IF EXISTS trg_bridge_baseball_coach_lifting ON public.baseball_coaches;
CREATE TRIGGER trg_bridge_baseball_coach_lifting
AFTER INSERT OR UPDATE OF organization_id, user_id ON public.baseball_coaches
FOR EACH ROW EXECUTE FUNCTION public.bridge_baseball_coach_lifting_access();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Stale can_edit grants aren't revoked on org change or coach removal.

The trigger only handles INSERT/UPDATE OF organization_id, user_id, and on update it adds a new helm_lifting_org_viewers row for the new org — it never downgrades or removes the row for the coach's previous organization_id. Two concrete leak paths:

  1. Coach transfers from Org A → Org B: the update fires, granting can_edit=true on Org B, but the Org A row (can_edit=true) is left untouched — the coach retains lifting edit access on a team they no longer coach.
  2. Coach row is deleted from baseball_coaches entirely: no AFTER DELETE trigger exists, so the helm_lifting_org_viewers grant is permanent regardless of employment status.

Since this bridges into an RLS-gating table (helm_lifting_can_edit_org per the migration's own header comment), this is a live authorization surface, not just a data-hygiene nit. Consider either (a) adding an AFTER UPDATE OF organization_id branch that flips can_edit=false (or deletes the row) for OLD.organization_id/OLD.user_id when it differs from NEW, and (b) an AFTER DELETE trigger that revokes the grant for the deleted coach's org/user pair — being careful not to revoke access for a user who is also a legitimate lifting coach/org-viewer independent of this bridge.

🤖 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 `@supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql`
around lines 14 - 36, The bridge trigger in
public.bridge_baseball_coach_lifting_access only grants can_edit on
insert/update and never revokes stale access when a coach changes org or is
deleted. Update the trigger logic around trg_bridge_baseball_coach_lifting so it
also handles OLD vs NEW organization_id/user_id on UPDATE to remove or downgrade
the previous helm_lifting_org_viewers grant, and add an AFTER DELETE trigger on
public.baseball_coaches to revoke the corresponding grant for the deleted row.
Be careful to scope revocation to only the bridge-derived permission so existing
independent access is not removed.

Comment on lines +17 to +19
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $$

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

SET search_path doesn't match the required empty-string form.

This SECURITY DEFINER function sets SET search_path TO 'public', 'pg_temp' rather than SET search_path = ''. As per path instructions, supabase/migrations/*.sql rule 8 states: "Forbid SECURITY DEFINER functions without an explicit SET search_path = '' and an inline comment justifying the elevation." All object references in the function body are already schema-qualified (public.helm_lifting_org_viewers), which mitigates most of the practical risk, but it's a literal deviation from the stated rule, and there's no inline comment specifically justifying the SECURITY DEFINER elevation itself (the header comment explains the migration's purpose, not the elevation).

Note this also conflicts with docs/v3-rls-template.md's documented convention of SET search_path TO 'public' for existing helpers like is_golf_team_primary_coach — worth reconciling which convention is canonical so future migrations don't alternate between the two.

🤖 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 `@supabase/migrations/20260708010000_bridge_baseball_coach_lifting_edit.sql`
around lines 17 - 19, The SECURITY DEFINER function in this migration uses the
wrong search_path setting and lacks the required inline justification. Update
the function definition to use the empty search path form required by the
migration rules, and add a brief inline comment next to the SECURITY DEFINER
declaration explaining why the elevation is necessary. Use the existing function
body reference and schema-qualified object usage in this migration to keep the
change minimal.

Source: Path instructions

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

Labels

database Schema, migrations, indexes, SQL design-system

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant