Skip to content

fix: feature-flow sweep — 47 confirmed broken/stranded flows fixed across golf, baseball, and Bridge#802

Merged
njrini99-code merged 4 commits into
mainfrom
fix/feature-flows
Jul 10, 2026
Merged

fix: feature-flow sweep — 47 confirmed broken/stranded flows fixed across golf, baseball, and Bridge#802
njrini99-code merged 4 commits into
mainfrom
fix/feature-flows

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Problem

Owner directive: exhaustive feature sweep for "details and flows we might've missed... but also make sure the feature works." A 23-agent flow trace (every control → action → write → revalidate → consuming surface, plus prod error-table mining) surfaced 148 candidates; 47 survived adversarial confirmation (28 P0 / 19 P1; 1 refuted of 50 checked).

Fix / Outcome

17 fixer packets (each adversarially verified, repair loops until clean) + lead hand-repairs:

  • Baseball pending joiners un-stranded — approve/reject flow built into the roster Status board (require_coach_approval defaults true on 10/10 prod teams; joiners previously vanished into pending with no UI anywhere).
  • Built-but-dead controls wired: player-notes edit/delete, lineup Share + saved-lineups panel, remove-player / add-existing-player / jersey-position edits, class-schedule UI.
  • Roster stats read-model merges current-season box-score stats over legacy CSV aggregates (box-score-tracked players stop reading "Needs Data").
  • Silent data loss closed: travel link-to-event edit (zod schema dropped event_id), recurring events now invite attendees + notify on create/edit/delete, task-reminder cron actually matches user-set reminders.
  • Messaging: unread badges can clear (participant-checked SECURITY DEFINER RPC replaces the RLS-blocked update); broadcasts stop deduping by title into old threads.
  • Qualifiers: edit flow exists; lifecycle auto-advances (incl. single-entrant num_rounds=1 fall-through) with a view-time deadline reconcile; num_rounds caps submissions.
  • Notifications: quiet-mode enforced, CoachHelm pushes use their real preference key, in-app receipts surfaced.
  • Bridge work log reads PR lifecycle from fields the GitHub search API actually returns.
  • Practice-block visibility enforced by RLS + app allow-list (staff_only/restricted no longer player-readable).

DB

Four replay-safe migrations applied to prod and verified (pg_policy / information_schema / proacl): settings-audit reconcile + capability-scoped INSERT policy, decision-log reconcile, practice-blocks visibility RLS, mark_golf_messages_read RPC (anon revoked).

Residual (tracked)

savePartialRound same-course/same-day collision narrowed but not eliminated — full fix is client-generated round ids (follow-up).

Verification

tsc ✓ · eslint ✓ · full vitest suite 19,641 tests / 1,949 files ✓ · production build ✓ · contract tripwires updated with the new truth (registry 419→421, stat-layer grandfather, observability wraps).

Git Activity Timeline note

Feature-flow sweep: 47 confirmed broken/stranded user flows fixed; 4 prod migrations; approve/reject joiners flow built.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX

…ross golf, baseball, and Bridge

An exhaustive per-cluster flow trace (every control -> action -> write ->
revalidate -> consuming surface) found 148 candidate defects; 47 survived
adversarial confirmation (28 P0 / 19 P1) and are fixed here by 17 verified
packets plus lead hand-repairs.

Highest impact:
- Baseball pending joiners un-stranded: approve/reject flow built into the
  roster Status board (require_coach_approval defaults true on every prod
  team; joiners previously vanished into 'pending' with no UI anywhere).
- Built-but-dead controls wired: player-notes edit/delete, lineup Share +
  a saved-lineups panel (load/edit/delete), remove-player + add-existing-
  player + jersey/position edits, class-schedule UI.
- Roster/leaderboard stats read-model now merges current-season box-score
  stats over the legacy CSV aggregates (players stop reading 'Needs Data').
- Silent data loss closed: travel link-to-event edit (schema dropped
  event_id), recurring events now invite attendees + notify (create/edit/
  delete, via after()), imports honesty (deprecated-table writes surfaced),
  task-reminder cron now actually matches the reminders users set.
- Messaging: unread badges can finally clear (participant-checked
  SECURITY DEFINER RPC replaces an RLS-blocked update), broadcast dedupe
  no longer swallows new announcements into old threads by title.
- Qualifiers: full lifecycle now reachable — edit details, auto
  in_progress on first round, auto completed when every entrant is done
  (incl. the single-entrant num_rounds=1 fall-through) plus a view-time
  reconcile for deadline-passed qualifiers; num_rounds caps submissions.
- Notifications: quiet-mode preference actually enforced, CoachHelm pushes
  route through their real preference key, in-app receipts surfaced.
- Bridge work log reads PR lifecycle from fields the GitHub API returns.
- Practice-block visibility enforced by RLS (staff_only/restricted no
  longer readable or rendered player-side).

DB: four replay-safe migrations (settings-audit reconcile + capability-
scoped INSERT policy, decision-log reconcile, practice-blocks visibility
RLS, mark_golf_messages_read RPC with anon revoked) applied to prod and
verified via pg_policy/information_schema/proacl.

Contracts updated with the new truth: feature-registry qualifiers manifest
(+2, counts 419->421), stat-layer manifest grandfathers the roster
aggregates bridging shim, golf observability wraps reconcileQualifierStatus.

Residual (tracked, not closed): savePartialRound can still collide on two
same-course/same-day in-progress rounds — full fix is client-generated
round ids.

Gates: tsc, eslint --max-warnings 0, FULL vitest suite (19,641 tests,
1,949 files), production build — all green.

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

vercel Bot commented Jul 10, 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 10, 2026 7:58pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

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

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

@cursor

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

@supabase

supabase Bot commented Jul 10, 2026

Copy link
Copy Markdown

Updates to Preview Branch (fix/feature-flows) ↗︎

Deployments Status Updated
Database ⚠️ Fri, 10 Jul 2026 20:01:57 UTC
Services ⚠️ Fri, 10 Jul 2026 20:01:57 UTC
APIs ⚠️ Fri, 10 Jul 2026 20:01:57 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 Fri, 10 Jul 2026 20:02:28 UTC
Migrations ⏸️ Fri, 10 Jul 2026 19:58:55 UTC
Seeding ⏸️ Fri, 10 Jul 2026 19:58:55 UTC
Edge Functions ⏸️ Fri, 10 Jul 2026 19:58:55 UTC

❌ Branch Error • Fri, 10 Jul 2026 20:02:30 UTC

unexpected status 400: {"message":"Resource has been removed"}
unexpected status 400: {"message":"Resource has been removed"}
unexpected status 400: {"message":"Resource has been removed"}
unexpected status 400: {"message":"Resource has been removed"}

⚠️ Warning — Service health check failed


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

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 87a4ca20-52de-4ef0-a360-0164539a29f3

📥 Commits

Reviewing files that changed from the base of the PR and between e1baf12 and 429d08d.

📒 Files selected for processing (3)
  • src/app/actions/messages.ts
  • src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx
  • src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx

Summary by CodeRabbit

  • New Features
    • Added baseball class schedule management with conflict checking, plus roster assignment flows (member approvals/removals) and saved lineup editing with sharing.
    • Enhanced player compare/search with recruitability gating.
    • Added golf qualifier editing and coach notes for round reviews, plus selection/recurring event notifications and better canceled event cues.
  • Bug Fixes
    • Improved message notification previews, read-receipt handling, roster stats freshness, practice visibility, reminder scheduling, and focus-area lifecycle protections.
    • Tightened authorization and gating for coach actions and practice building.
  • Tests
    • Expanded regression coverage across academics, rosters, imports, notifications, qualifiers, and key dashboard components.

Walkthrough

This PR expands baseball and golf workflows with authorization changes, roster and academic interfaces, canonical import persistence, qualifier lifecycle management, messaging notifications, recurring-event fanout, focus-area controls, review notes, and database reconciliation migrations.

Changes

Baseball workflows

Layer / File(s) Summary
Academic and roster interaction surfaces
src/app/baseball/.../academics/*, src/app/baseball/.../roster/*, src/components/baseball/..., src/components/coach/...
Adds class scheduling, conflict checks, roster membership mutations, saved-lineup editing, player notes editing, and lineup sharing.
Eligibility, authorization, and practice visibility
src/app/baseball/actions/*, src/components/baseball/.../compare/*, src/components/baseball/.../practice-planner/*, supabase/migrations/*
Moves comparison and watchlist discovery behind server actions, adjusts coach authorization, gates practice mutations by capability, and restricts player-visible practice blocks.
Imports, read models, and schema reconciliation
src/app/baseball/actions/imports.ts, src/lib/baseball/read-models/*, src/app/baseball/actions/decision-room.ts, supabase/migrations/*
Routes supported imports to canonical tables with rollback snapshots, merges season stats into roster aggregates, reconciles decision-log and audit-log schemas, and adds regression coverage.

Golf workflows

Layer / File(s) Summary
Qualifier lifecycle and editing
src/app/golf/.../qualifiers/*, src/app/golf/actions/golf.ts, src/components/fairway/pages/qualifiers/*, src/lib/coachhelm/v3/qualifying/*
Adds coach qualifier editing, configured round limits, lifecycle reconciliation, conclusion controls, committed-selection badges, and selection notifications.
Messaging, recurring events, and delivery
src/app/actions/messages.ts, src/app/golf/actions/{message-attachments,recurring-events}.ts, src/lib/notifications/*, supabase/migrations/20260710160000_mark_golf_messages_read_rpc.sql
Centralizes golf message fanout, secures read marking with an RPC, improves broadcast reuse, adds recurring-event notifications, and routes push delivery through the APNs Edge Function.
Focus areas and supporting UI
src/app/golf/actions/{development,round-reviews,recruiting,tasks,travel}.ts, src/components/fairway/pages/{coachhelm,calendar,roster,settings}/*
Adds lifecycle guards, reopening, coach notes, structured recruiting errors, reminder queue persistence, nullable travel links, cancellation styling, and save-error UI.

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

Possibly related PRs

Suggested labels: security, database


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (1 error, 2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
No Service-Role In Client Bundles ❌ Error FAIL: PR adds createAdminClient outside allowed admin paths in src/app/golf/actions/development.ts:4/292, src/app/golf/actions/recurring-events.ts:26/264, and src/lib/notifications/golf-message-fan... Move service-role Supabase access into src/lib/supabase/admin* or src/app/api//admin/; keep these actions on user-scoped clients or call an allowed helper.
Title check ⚠️ Warning The title is related, but it does not follow the required Conventional Commit scope format and omits a required scope. Change it to a Conventional Commit like fix(golf): ... using one of the required scopes, or the single best matching scope for the main change.
Docstring Coverage ⚠️ Warning Docstring coverage is 46.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Conventional Commits ❓ Inconclusive HEAD commit subject matches the conventional-commit regex, but the PR title isn't present in the checkout so the title can't be verified. Provide the PR title or PR metadata; I can confirm the regex once the title is available.
✅ Passed checks (8 passed)
Check name Status Explanation
Description check ✅ Passed The description is clearly related to the changeset and summarizes the main fixes and verification work.
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: 20260710020000:37/111, 20260710031500:92/120, 20260710150000:56/57, 20260710160000:20 are ALTER/CREATE POLICY/FUNCTION only; no changed migration adds CREATE TABLE.
Auth Check In Server Actions ✅ Passed All changed src/app//actions//*.ts exported async functions call supabase.auth.getUser() before any .from()/.rpc() query; no blocking auth-order issues found.
Sport-Prefixed Table Names ✅ Passed Changed TS/TSX files only query golf_/baseball_ prefixed tables; exact bare coaches/players/teams/rounds/events searches found none.
No Destructive Writes ✅ Passed PASS: the only same-table delete→insert in save/submit code is the rollback path in golf.ts:965-995, explicitly justified; savePartialRound uses atomic RPC at golf.ts:5081-5082.
No Edits To Historical Migrations ✅ Passed PASS: only touched migrations are 20260710020000...:1 and 20260710031500...:1, both after the 20260527120000 baseline; no <=baseline migration changed.
✨ 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/feature-flows
  • 🛠️ 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.

src/app/actions/messages.ts

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

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

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

src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx

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


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

open
onClose={() => setClassesStudent(null)}
playerId={classesStudent.player_id}
teamId={selectedTeamId ?? ''}
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Feature-flow sweep: 47 fixes for broken/stranded flows in golf, baseball, Bridge

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

Grey Divider

AI Description

• Wires up previously-unreachable actions to real UI: roster approvals, notes edit/delete, saved
 lineups, qualifier edit/conclude.
• Fixes silent correctness/data-loss bugs: travel event links, recurring-event notifications, task
 reminders, qualifier round caps.
• Closes messaging gaps: golf unread badges now clear via RPC; broadcasts no longer reuse stale
 threads.
• Tightens security/privacy: recruitability-gated Compare/Watchlist search; practice-block
 visibility enforced by RLS.
• Adds replay-safe prod migrations and broad test coverage for the repaired flows.
Diagram

graph TD
    A["Coach/Player UI"] --> B["Roster & Lineup Actions"] --> C[("baseball_team_members")]
    A --> D["Messaging Actions"] --> E["mark_golf_messages_read RPC"] --> F[("golf_messages")]
    A --> G["Qualifier Actions"] --> H["Lifecycle Auto-Advance"] --> I[("golf_qualifiers")]
    D --> J["golf-message-fanout"] --> K{{"Email / Push / In-App"}}
    B --> L["roster-aggregates-merge"] --> C
    G --> K
    subgraph Legend
      direction LR
      _db[(Database)] ~~~ _svc([Service]) ~~~ _ext{{External Notify}}
    end
Loading
High-Level Assessment

The packetized remediation approach with targeted tests plus forward-only, existence-guarded migrations is appropriate for a shared prod DB with known schema drift. The PR consistently chooses enforcement boundaries correctly (DB/RLS for visibility; SECURITY DEFINER with participant checks for unread flips) and extracts shared fan-out logic to avoid divergence between message-send paths.

Files changed (95) +8368 / -489

Enhancement (28) +4118 / -211
AcademicsClient.tsxWire class-schedule editor and class-conflict detection trigger +93/-13

Wire class-schedule editor and class-conflict detection trigger

• Adds per-student Classes modal entrypoint and a masthead action to run class conflict detection and surface results via toast.

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

ClassScheduleModal.tsxAdd class schedule CRUD modal +341/-0

Add class schedule CRUD modal

• Introduces a modal UI that calls existing academics CRUD actions so coaches can create/edit/delete player class schedules.

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

RosterClient.tsxWire roster mutations and merge season stats on client refetch +187/-12

Wire roster mutations and merge season stats on client refetch

• Adds UI handlers for remove/assign/approve/reject/search and ensures box-score season stats are merged over legacy aggregates after mutations.

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

RosterFairway.tsxAdd roster row actions, pending controls, and saved lineups surface +191/-31

Add roster row actions, pending controls, and saved lineups surface

• Integrates row menu + pending approve/decline and adds SavedLineupsPanel; fixes dev-board bucketing so players with stats aren’t labeled Needs Data.

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

RosterMemberActions.tsxNew roster action components (pending, row menu, add existing) +497/-0

New roster action components (pending, row menu, add existing)

• Adds PendingMemberActions, RosterRowMenu, and AssignPlayerModal to provide reachable UI for existing roster server actions.

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

SavedLineupsPanel.tsxNew saved lineups list/load/delete panel +207/-0

New saved lineups list/load/delete panel

• Adds a reachable surface to view saved lineups, load them into the builder, and delete them.

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

imports.tsWrite import commits to canonical season/box-score tables +641/-18

Write import commits to canonical season/box-score tables

• Adds dataShape routing so commits best-effort upsert season totals / save full box scores and recalc aggregates, while preserving legacy writes.

src/app/baseball/actions/imports.ts

practice-planner-capability.tsAdd can_manage_practice capability probe +40/-0

Add can_manage_practice capability probe

• New server action used by PracticePlannerClient to render honest read-only state for staff lacking can_manage_practice.

src/app/baseball/actions/practice-planner-capability.ts

roster.tsUnstrand pending joiners and add assign/search roster actions +281/-15

Unstrand pending joiners and add assign/search roster actions

• Adds approve/reject pending membership actions, searchAssignablePlayers, and reworks assignPlayerToTeam into insert-vs-update to preserve lifecycle fields.

src/app/baseball/actions/roster.ts

error.tsxAdd error boundary for qualifier edit route +23/-0

Add error boundary for qualifier edit route

• Provides a route-scoped error boundary for edit qualifier flow.

src/app/golf/(dashboard)/dashboard/qualifiers/[id]/edit/error.tsx

loading.tsxAdd loading skeleton for qualifier edit route +5/-0

Add loading skeleton for qualifier edit route

• Adds a lightweight skeleton UI during server load for the editor page.

src/app/golf/(dashboard)/dashboard/qualifiers/[id]/edit/loading.tsx

page.tsxAdd coach-only qualifier edit page +62/-0

Add coach-only qualifier edit page

• Loads qualifier details and round courses then renders FairwayEditQualifier; redirects players to read-only view.

src/app/golf/(dashboard)/dashboard/qualifiers/[id]/edit/page.tsx

CoachNotesSection.tsxAdd coach notes UI section for round review +124/-0

Add coach notes UI section for round review

• Introduces a coach-only annotation UI for round reviews.

src/app/golf/(dashboard)/dashboard/rounds/[id]/review/CoachNotesSection.tsx

page.tsxWire coach notes into round review page +24/-2

Wire coach notes into round review page

• Integrates CoachNotesSection into the review route so notes are reachable in UI.

src/app/golf/(dashboard)/dashboard/rounds/[id]/review/page.tsx

golf.tsQualifier edit + lifecycle fixes + num_rounds enforcement +321/-41

Qualifier edit + lifecycle fixes + num_rounds enforcement

• Adds updateGolfQualifierDetails, enforces num_rounds caps, fixes qualifier entry stat writes via admin client, adds reconcile-on-view, and tightens savePartialRound collision heuristic.

src/app/golf/actions/golf.ts

PlayerNotesSection.tsxEnable coach note edit/delete from player profile +170/-47

Enable coach note edit/delete from player profile

• Adds inline editing, confirm delete, optimistic local state updates, and server action wiring for notes.

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

PlayerProfileClient.tsxAdd remove-from-team action +60/-11

Add remove-from-team action

• Adds a coach-only “Remove from team” confirm dialog wired to removePlayerFromTeam, then routes back to roster.

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

LineupBuilder.tsxImplement Share and support loading saved lineups +69/-10

Implement Share and support loading saved lineups

• Adds initialLineup/initialName props and implements Share button via native share sheet or clipboard fallback with toast.

src/components/coach/lineup/LineupBuilder.tsx

FocusAreaCard.tsxSupport reopen + lifecycle-aware presentation +25/-3

Support reopen + lifecycle-aware presentation

• Adds reopen affordance and updates badges to reflect proposed/declined/active/completed states correctly.

src/components/fairway/pages/coachhelm/FocusAreaCard.tsx

PlayersGridView.tsxAdd coach reopen flow and split focus areas by lifecycle +96/-2

Add coach reopen flow and split focus areas by lifecycle

• Wires reactivateFocusArea for coaches and separates proposed/declined from active so actions aren’t exposed on non-actionable states.

src/components/fairway/pages/coachhelm/PlayersGridView.tsx

FairwayEditQualifier.tsxAdd full qualifier edit form (scalar + round courses) +405/-0

Add full qualifier edit form (scalar + round courses)

• New edit flow wiring updateGolfQualifierDetails and setQualifierRoundCourses into a single Fairway form.

src/components/fairway/pages/qualifiers/FairwayEditQualifier.tsx

FairwayQualifierDetail.tsxAdd coach Edit Qualifier link and improve player messaging +17/-4

Add coach Edit Qualifier link and improve player messaging

• Adds coach-only secondary action to edit; adjusts player notices to avoid implying an in-app self-entry action.

src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx

FairwayQualifierLeaderboard.tsxShow committed selection outcomes when roster finalized +59/-2

Show committed selection outcomes when roster finalized

• Fetches golf_qualifier_selections once selection_state=selected and overlays Selected/Not selected badges over merit projection.

src/components/fairway/pages/qualifiers/FairwayQualifierLeaderboard.tsx

FairwayQualifyingWorkspace.tsxAdd Conclude Qualifier button (first caller of updateQualifierStatus) +79/-0

Add Conclude Qualifier button (first caller of updateQualifierStatus)

• Adds coach-only conclude action after roster is committed to transition qualifier status to completed.

src/components/fairway/pages/qualifiers/FairwayQualifyingWorkspace.tsx

loader.tsLoad qualifier status into qualifying workspace +2/-0

Load qualifier status into qualifying workspace

• Extends qualifying loader to include golf_qualifiers.status so workspace can conclude qualifiers.

src/lib/coachhelm/v3/qualifying/loader.ts

player-notify.tsAdd player selection-outcome notifications +81/-0

Add player selection-outcome notifications

• Fans out email + push notifying each candidate of selected/not_selected after confirmSelection commits.

src/lib/coachhelm/v3/qualifying/player-notify.ts

service.tsTrigger player-notify after confirming selection +13/-0

Trigger player-notify after confirming selection

• Calls notifyPlayersOfSelectionOutcome best-effort after roster commit.

src/lib/coachhelm/v3/qualifying/service.ts

types.tsExtend QualifyingWorkspace type with status +5/-0

Extend QualifyingWorkspace type with status

• Adds status field representing qualifier play lifecycle (upcoming/in_progress/completed).

src/lib/coachhelm/v3/qualifying/types.ts

Bug fix (39) +1986 / -260
messages.tsRoute golf read-receipts through new RPC; fix broadcast dedupe +105/-99

Route golf read-receipts through new RPC; fix broadcast dedupe

• Replaces RLS-blocked golf_messages read updates with mark_golf_messages_read RPC, and changes broadcast dedupe to compare title + audience, reconciling participants on reuse.

src/app/actions/messages.ts

CompareClient.tsxEnforce recruitability via server actions in Compare +59/-36

Enforce recruitability via server actions in Compare

• Replaces direct client Supabase queries with server actions to prevent compare URLs/search from bypassing recruitability and privacy filtering.

src/app/baseball/(dashboard)/dashboard/compare/CompareClient.tsx

actions.tsAdd recruitability-gated search/fetch actions +237/-0

Add recruitability-gated search/fetch actions

• Implements server-side searchRecruitablePlayers/getComparablePlayers/canAddPlayerToCompare mirroring Discover’s recruitability rules.

src/app/baseball/(dashboard)/dashboard/compare/actions.ts

page.tsxFix lift home routing via basePath +7/-1

Fix lift home routing via basePath

• Passes basePath to PlayerLiftHomeClient so links resolve under the baseball dashboard prefix.

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

WatchlistClient.tsxGate quick-add search through recruitability server action +17/-22

Gate quick-add search through recruitability server action

• Removes direct client search query and reuses the Compare recruitability-gated search action.

src/app/baseball/(dashboard)/dashboard/watchlist/WatchlistClient.tsx

academics.tsFix class edit/delete authorization for coaches +25/-2

Fix class edit/delete authorization for coaches

• Expands assertOwnPlayerClass to allow coaches with can_view_academics to mutate classes for rostered players on their active team.

src/app/baseball/actions/academics.ts

decision-room.tsWrite decision ledger to real prod columns +38/-12

Write decision ledger to real prod columns

• Moves decision_log inserts from nonexistent subject_table/subject_id to meeting_item_id/signal_id, aligning with prod drift + migration reconcile.

src/app/baseball/actions/decision-room.ts

dev-plans.tsDev plan action corrections +18/-3

Dev plan action corrections

• Small fixes to development plan actions (behavioral/validation adjustments).

src/app/baseball/actions/dev-plans.ts

practice.tsFix visibility allow-list for player practice blocks +5/-1

Fix visibility allow-list for player practice blocks

• Changes filter to allow-list only player_visible/NULL blocks, matching new RLS policy and removing restricted leak.

src/app/baseball/actions/practice.ts

program-settings.tsLog settings-audit insert failures +20/-1

Log settings-audit insert failures

• Adds explicit error logging for audit inserts so schema drift doesn’t silently nullify auditing again.

src/app/baseball/actions/program-settings.ts

page.tsxReconcile qualifier status on view +6/-1

Reconcile qualifier status on view

• Calls reconcileQualifierStatus so in_progress qualifiers can auto-complete after deadline even without new submissions.

src/app/golf/(dashboard)/dashboard/qualifiers/[id]/page.tsx

page.tsxRecruiting page wiring fix +28/-0

Recruiting page wiring fix

• Adds/adjusts routing so the recruiting dashboard page renders correctly.

src/app/golf/(dashboard)/dashboard/recruiting/page.tsx

development.tsAdd focus-area lifecycle guards and coach-prescription consent model +92/-14

Add focus-area lifecycle guards and coach-prescription consent model

• Prevents progress/outcome writes on proposed/declined items; coach-promoted focus areas become proposed until player accepts.

src/app/golf/actions/development.ts

message-attachments.tsAdd notifications for attachment-only messages +38/-0

Add notifications for attachment-only messages

• Adds preview generation and calls notifyGolfMessageRecipients so attachment sends notify recipients via email/push/in-app.

src/app/golf/actions/message-attachments.ts

recruiting.tsRecruiting action fixes +10/-4

Recruiting action fixes

• Small correctness adjustments in recruiting server actions.

src/app/golf/actions/recruiting.ts

recurring-events.tsInvite attendees and notify on recurring series changes +426/-3

Invite attendees and notify on recurring series changes

• Adds attendeeIds, resolves attendee user_ids before delete cascades, and uses after() to best-effort fan out cancellation in-app/email notifications.

src/app/golf/actions/recurring-events.ts

round-reviews.tsFix coach-notes authorization + revalidation +29/-7

Fix coach-notes authorization + revalidation

• Validates non-empty notes, requires coach role via verifyReviewAccess, trims content, and revalidates relevant pages after update.

src/app/golf/actions/round-reviews.ts

tasks.tsMake task reminders actually dispatch +49/-2

Make task reminders actually dispatch

• Writes/clears golf_task_reminders rows (queue of record) before updating display columns, preventing false “reminder set/cleared” UI.

src/app/golf/actions/tasks.ts

travel.tsFix travel itinerary update schema for event_id +11/-2

Fix travel itinerary update schema for event_id

• Adds event_id to zod schema and normalizes undefined to null so clearing event links persists instead of silently dropping the field.

src/app/golf/actions/travel.ts

page.tsxDashboard lift page fix +1/-0

Dashboard lift page fix

• Small routing/render correction to lifting dashboard lift page.

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

ImportWizardClient.tsxPass dataShape to commitImport +5/-0

Pass dataShape to commitImport

• Wires ImportWizard’s data-shape selection through to commitImport so canonical-table routing is applied.

src/components/baseball/import-center/ImportWizardClient.tsx

PracticePlannerClient.tsxGate Practice Planner writes on can_manage_practice +50/-12

Gate Practice Planner writes on can_manage_practice

• Fetches getCanManagePractice and gates create/edit/publish/attendance/recap affordances, showing a read-only notice for unauthorized staff.

src/components/baseball/practice-planner/PracticePlannerClient.tsx

PracticePrintExport.tsxFix practice print export visibility filter +6/-1

Fix practice print export visibility filter

• Restricts export to player_visible blocks only (excluding staff_only and restricted) to match the intended player-sharing contract.

src/components/baseball/practice-planner/PracticePrintExport.tsx

PracticeRecapPanel.tsxPrevent per-task errors from wiping recap panel +14/-5

Prevent per-task errors from wiping recap panel

• Moves task-composer error state local so objective-level validation errors don’t trip the parent load-failure branch.

src/components/baseball/practice-planner/PracticeRecapPanel.tsx

FairwayEventCard.tsxCalendar card tweak +17/-1

Calendar card tweak

• Small UI/behavior fix in event card.

src/components/fairway/pages/calendar/FairwayEventCard.tsx

FairwayMonthGrid.tsxMonth grid fix +9/-2

Month grid fix

• Small calendar grid adjustment.

src/components/fairway/pages/calendar/FairwayMonthGrid.tsx

FairwayStatsCockpit.tsxStats cockpit fix +1/-1

Stats cockpit fix

• One-line correctness change.

src/components/fairway/pages/coachhelm/FairwayStatsCockpit.tsx

FairwayJoinRequests.tsxPrevent double-expanded join request surfaces +23/-4

Prevent double-expanded join request surfaces

• Defaults inline accordion expansion opposite of enableModal so accordion and modal don’t both show expanded lists simultaneously.

src/components/fairway/pages/roster/FairwayJoinRequests.tsx

FairwaySettingsCoachingIntelligence.tsxSettings panel adjustment +23/-0

Settings panel adjustment

• Adds/adjusts content to match intended coaching-intelligence settings behavior.

src/components/fairway/pages/settings/FairwaySettingsCoachingIntelligence.tsx

PremiumCalendarClient.tsxRestore desktop in-app notification bell +17/-4

Restore desktop in-app notification bell

• Mounts NotificationCenter on desktop calendar header because the global shell doesn’t render a bell, restoring an in-app receipt surface.

src/components/golf/calendar/PremiumCalendarClient.tsx

PlayerLiftHomeClient.tsxAdd basePath support +22/-6

Add basePath support

• Introduces basePath prop for correct link generation across dashboard trees.

src/components/lifting/players/PlayerLiftHomeClient.tsx

github-pr-timeline.tsFix PR lifecycle state for GitHub Search Issues API +40/-2

Fix PR lifecycle state for GitHub Search Issues API

• Normalizes Search Issues API items (merged_at under pull_request) to the unified RawPullRequest shape.

src/lib/admin/github-pr-timeline.ts

roster-aggregates-merge.tsAdd shared helper to merge season stats over legacy aggregates +80/-0

Add shared helper to merge season stats over legacy aggregates

• Introduces a pure merge helper used by server read model and client refetch so box-score teams stop showing “Needs Data”.

src/lib/baseball/read-models/roster-aggregates-merge.ts

roster.tsMerge box-score season stats into roster read model +24/-2

Merge box-score season stats into roster read model

• Fetches baseball_player_season_stats and merges into aggregates, and flags aggregatesError when either source fails.

src/lib/baseball/read-models/roster.ts

push.tsFix push gating keys and honor quiet_mode via gatedDelivery +35/-10

Fix push gating keys and honor quiet_mode via gatedDelivery

• Maps coachhelm_insight to push_coachhelm (not push_events) and centralizes push gating through gatedDelivery to respect quiet mode.

src/lib/notifications/push.ts

20260710020000_baseball_settings_audit_log_column_reconcile.sqlReconcile baseball_settings_audit_log schema drift + policy scope +119/-0

Reconcile baseball_settings_audit_log schema drift + policy scope

• Adds missing audit columns, relaxes legacy NOT NULL, adds event_type CHECK + index, and aligns INSERT policy with can_manage_settings capability.

supabase/migrations/20260710020000_baseball_settings_audit_log_column_reconcile.sql

20260710031500_baseball_decision_log_kind_reconcile.sqlReconcile baseball_decision_log columns + widen decision_kind CHECK +138/-0

Reconcile baseball_decision_log columns + widen decision_kind CHECK

• Adds missing columns for Decision Room usage, adds covering indexes, and widens decision_kind CHECK to include Decision Room values.

supabase/migrations/20260710031500_baseball_decision_log_kind_reconcile.sql

20260710150000_baseball_practice_blocks_visibility_rls.sqlEnforce practice block visibility via RLS +89/-0

Enforce practice block visibility via RLS

• Replaces baseball_practice_blocks_select policy so non-staff can only read player_visible (or NULL) blocks for published practices.

supabase/migrations/20260710150000_baseball_practice_blocks_visibility_rls.sql

20260710160000_mark_golf_messages_read_rpc.sqlAdd participant-checked SECURITY DEFINER RPC for unread clearing +53/-0

Add participant-checked SECURITY DEFINER RPC for unread clearing

• Creates mark_golf_messages_read() to flip read=true for non-sender rows with participant verification; revokes anon/public execute and grants authenticated/service_role.

supabase/migrations/20260710160000_mark_golf_messages_read_rpc.sql

Refactor (1) +109 / -0
golf-message-fanout.tsExtract shared golf message notification fan-out helper +109/-0

Extract shared golf message notification fan-out helper

• Adds notifyGolfMessageRecipients shared by text and attachment send paths; performs best-effort email/push/in-app inserts.

src/lib/notifications/golf-message-fanout.ts

Tests (23) +2090 / -15
search-recruitability.test.tsAdd tests for recruitability-gated search +200/-0

Add tests for recruitability-gated search

• Adds coverage ensuring private/college/off-territory/own-roster exclusions are enforced for Compare search.

src/app/baseball/(dashboard)/dashboard/compare/tests/search-recruitability.test.ts

academics-coach-gating.test.tsTest coach gating for class CRUD +125/-0

Test coach gating for class CRUD

• Ensures coaches with the right capability can edit/delete player classes on their team.

src/app/baseball/actions/tests/academics-coach-gating.test.ts

assign-player-membership-fields.test.tsTest assignPlayerToTeam insert/update semantics +136/-0

Test assignPlayerToTeam insert/update semantics

• Validates joined_at/status are only set on insert, never reset during edits.

src/app/baseball/actions/tests/assign-player-membership-fields.test.ts

decision-log-column-reconcile.test.tsTest decision-log column mapping reconciliation +113/-0

Test decision-log column mapping reconciliation

• Adds tests validating decision-room ledger writes target reconciled columns.

src/app/baseball/actions/tests/decision-log-column-reconcile.test.ts

dev-plans-coach-gating.test.tsAdd dev plan gating tests +143/-0

Add dev plan gating tests

• Covers permission checks for dev plan flows.

src/app/baseball/actions/tests/dev-plans-coach-gating.test.ts

imports-event-grain-guard.test.tsAdd imports grain-guard tests +124/-0

Add imports grain-guard tests

• Tests import validation that prevents incorrect event/stat grain routing.

src/app/baseball/actions/tests/imports-event-grain-guard.test.ts

imports-registry.test.tsAdd imports registry test +34/-0

Add imports registry test

• Ensures registry changes are reflected in tests.

src/app/baseball/actions/tests/imports-registry.test.ts

CoachNotesSection.test.tsxAdd CoachNotesSection tests +94/-0

Add CoachNotesSection tests

• Adds component tests for coach notes section behavior.

src/app/golf/(dashboard)/dashboard/rounds/[id]/review/tests/CoachNotesSection.test.tsx

golf-save-partial-round.test.tsTest scoped partial-round recovery heuristic +176/-0

Test scoped partial-round recovery heuristic

• Covers tightened in_progress round matching (course/date/qualifier context) to reduce collision/data loss.

src/app/golf/actions/tests/golf-save-partial-round.test.ts

recurring-events.test.tsTest recurring-events invite/notify behavior +12/-0

Test recurring-events invite/notify behavior

• Adds coverage for new attendee invite and cancellation notify fan-out paths.

src/app/golf/actions/tests/recurring-events.test.ts

PracticeRecapPanel.test.tsxAdd recap panel regression test +98/-0

Add recap panel regression test

• Ensures task composer errors don’t clear the recap UI.

src/components/baseball/practice-planner/tests/PracticeRecapPanel.test.tsx

FairwayJoinRequests.test.tsxTest join request default expansion behavior +95/-0

Test join request default expansion behavior

• Adds tests for defaultExpanded behavior when modal is enabled.

src/components/fairway/pages/roster/FairwayJoinRequests.test.tsx

PlayerLiftHomeClient.test.tsxAdd basePath tests +129/-0

Add basePath tests

• Adds tests verifying link behavior with basePath.

src/components/lifting/players/tests/PlayerLiftHomeClient.test.tsx

coverage-contract.foundation.test.tsUpdate coverage-contract expectations +7/-1

Update coverage-contract expectations

• Updates coverage-contract foundation test for new/changed action registrations.

src/lib/admin/tests/coverage-contract.foundation.test.ts

feature-registry.test.tsUpdate feature-registry test counts +6/-2

Update feature-registry test counts

• Adjusts expected feature registry counts for new entries.

src/lib/admin/tests/feature-registry.test.ts

github-pr-timeline.test.tsAdd tests for GitHub search API normalization +87/-0

Add tests for GitHub search API normalization

• Verifies merged_at is derived correctly from Search Issues API shape.

src/lib/admin/tests/github-pr-timeline.test.ts

roster-aggregates-merge.test.tsAdd unit tests for roster stats merge helper +98/-0

Add unit tests for roster stats merge helper

• Covers mergeSeasonStatsIntoAggregates behavior and edge cases.

src/lib/baseball/read-models/tests/roster-aggregates-merge.test.ts

confirm-selection-chat.integration.test.tsIntegration test update +1/-0

Integration test update

• Small adjustment for confirm-selection behavior changes.

src/test/coachhelm/v3/confirm-selection-chat.integration.test.ts

player-notify.test.tsAdd tests for selection outcome notifications +130/-0

Add tests for selection outcome notifications

• Covers notifyPlayersOfSelectionOutcome behavior and outcomes mapping.

src/test/coachhelm/v3/player-notify.test.ts

travel-brief.test.tsTravel brief test update +1/-0

Travel brief test update

• Minor update aligning expectations with current travel brief behavior.

src/test/coachhelm/v3/travel-brief.test.ts

settings-coaching-intelligence.test.tsxUpdate coaching-intelligence settings tests +43/-7

Update coaching-intelligence settings tests

• Adjusts tests for settings behavior changes.

src/test/fairway/settings-coaching-intelligence.test.tsx

development.test.tsAdd tests for focus area lifecycle guards +142/-4

Add tests for focus area lifecycle guards

• Ensures actionable statuses are enforced and coach-prescribed items behave as proposed until accepted.

src/test/golf/actions/development.test.ts

push.test.tsAdd push gating tests +96/-1

Add push gating tests

• Validates gatedDelivery mapping and quiet_mode behavior for pushes.

src/test/lib/notifications/push.test.ts

Documentation (1) +13 / -0
stat-layer-manifest.tsDocument bridging shim in stat layer manifest +13/-0

Document bridging shim in stat layer manifest

• Adds roster-aggregates-merge entries as grandfathered consumers pending future migration behind stats-center.

src/lib/baseball/stat-layer-manifest.ts

Other (3) +52 / -3
feature-registry.tsRegister new actions/features +2/-0

Register new actions/features

• Adds new actions to the feature registry (count 419→421).

src/lib/admin/feature-registry.ts

nav-registry.tsUpdate nav registry +11/-0

Update nav registry

• Adds/adjusts navigation registry entries for newly surfaced baseball routes/features.

src/lib/baseball/nav-registry.ts

database.tsUpdate generated DB types for reconciled schema and new RPC +39/-3

Update generated DB types for reconciled schema and new RPC

• Reflects baseball_settings_audit_log column additions and exposes mark_golf_messages_read function type.

src/lib/types/database.ts

@coderabbitai coderabbitai Bot added database Schema, migrations, indexes, SQL security Auth, secrets, RLS, PII, webhooks labels Jul 10, 2026
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 98 rules

Grey Divider


Action required

1. any used in block filter 📘 Rule violation ⚙ Maintainability
Description
The updated practice visibility filter keeps an explicit any annotation, violating the no-any
TypeScript requirement. This weakens type safety around visibility enforcement and can hide future
logic/type regressions.
Code

src/app/baseball/actions/practice.ts[763]

+    const visibleBlocks = (p.blocks ?? []).filter((b: any) => b.visibility == null || b.visibility === 'player_visible');
Relevance

⭐⭐ Medium

Team sometimes removes explicit any (PR296), but many TS convention nit suggestions are rejected
elsewhere.

PR-#296
PR-#332

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519320 forbids explicit any. The modified lines include (b: any) and as any
assertions/casts on Supabase calls introduced/used in this PR.

Rule 1519320: Disallow use of the any type in TypeScript
src/app/baseball/actions/practice.ts[759-766]
src/app/actions/messages.ts[401-413]
src/app/golf/actions/recurring-events.ts[682-688]

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

## Issue description
Changed code introduces/retains explicit `any` in TypeScript in modified lines.

## Issue Context
Compliance requires eliminating `any` annotations/assertions in changed TS/TSX. These instances are in production action code paths.

## Fix Focus Areas
- src/app/baseball/actions/practice.ts[763-766]
- src/app/actions/messages.ts[401-413]
- src/app/golf/actions/recurring-events.ts[682-688]

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


2. use server file outside actions 📘 Rule violation ⌂ Architecture
Description
A new server action module ('use server') was added under src/app/baseball/actions/ instead of
src/app/actions/. This violates the required server-action placement convention and makes action
discovery/enforcement inconsistent.
Code

src/app/baseball/actions/practice-planner-capability.ts[1]

+'use server';
Relevance

⭐⭐ Medium

No historical evidence found enforcing server actions only under src/app/actions; similar placement
convention not observed.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519234 requires server action modules to be located under src/app/actions/. The
new file contains a top-level 'use server' directive while living in src/app/baseball/actions/.

Rule 1519234: Place Next.js server actions in src/app/actions directory
src/app/baseball/actions/practice-planner-capability.ts[1-5]

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

## Issue description
Server action files must live under `src/app/actions/`, but this PR adds a `'use server'` module under `src/app/baseball/actions/`.

## Issue Context
The module is clearly a server action file (top-level `'use server'` directive) and exports an action wrapper.

## Fix Focus Areas
- src/app/baseball/actions/practice-planner-capability.ts[1-40]

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


3. Message fanout recipient lookup fails 🐞 Bug ≡ Correctness
Description
notifyGolfMessageRecipients excludes the sender from recipientUserIds, then attempts to SELECT
recipient rows from public.users using the request-scoped Supabase client, but users RLS only
permits selecting your own row—so recipientProfiles will be empty and email/push/in-app
notifications won’t be sent to recipients.
Code

src/lib/notifications/golf-message-fanout.ts[R35-66]

+    const { data: otherParticipants } = await supabase
+      .from('golf_conversation_participants')
+      .select('user_id')
+      .eq('conversation_id', conversationId)
+      .neq('user_id', senderId);
+
+    if (!otherParticipants || otherParticipants.length === 0) return;
+
+    const recipientUserIds = otherParticipants.map(p => p.user_id);
+
+    // Batch every lookup into ONE round-trip of parallel queries: sender name
+    // (coach → player → email fallback) and all recipient emails via a single
+    // .in() instead of sequential per-recipient fetches.
+    const [
+      { data: senderCoach },
+      { data: senderPlayer },
+      { data: senderUser },
+      { data: recipientProfiles },
+    ] = await Promise.all([
+      supabase.from('golf_coaches').select('full_name').eq('user_id', senderId).maybeSingle(),
+      supabase.from('golf_players').select('first_name, last_name').eq('user_id', senderId).maybeSingle(),
+      supabase.from('users').select('email').eq('id', senderId).maybeSingle(),
+      supabase.from('users').select('id, email').in('id', recipientUserIds),
+    ]);
+
+    const senderName = senderCoach?.full_name
+      || (senderPlayer ? `${senderPlayer.first_name || ''} ${senderPlayer.last_name || ''}`.trim() : '')
+      || senderUser?.email
+      || 'Someone';
+
+    if (!recipientProfiles) return;
+
Relevance

⭐⭐ Medium

No direct precedent on users-table RLS blocking recipient lookups in notification fanout; only
adjacent PII/RLS changes.

PR-#640

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The fanout code explicitly excludes the sender (neq('user_id', senderId)) and then relies on
supabase.from('users')...in(recipientUserIds); however, RLS on public.users only allows
selecting the caller’s own row, and createClient() uses the anon key so RLS is enforced—therefore
the query cannot return any recipient rows and the function returns before sending notifications.

src/lib/notifications/golf-message-fanout.ts[34-66]
supabase/migrations/20260527000000_prod_public_baseline.sql[20153-20164]
src/lib/supabase/server.ts[9-24]

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

### Issue description
`notifyGolfMessageRecipients()` builds `recipientUserIds` as “all other participants” and then calls `supabase.from('users').select('id, email').in('id', recipientUserIds)`. With current RLS (`users_select_own`), the sender cannot read any of those rows (they are *explicitly not* the sender), so the function returns early and no notifications are sent.

### Issue Context
- The server `createClient()` uses the anon key + user cookies, so RLS applies.
- `users_select_own` restricts SELECT to `auth.uid() = id`.
- `recipientUserIds` explicitly excludes `senderId`, guaranteeing the RLS-visible set is empty.

### Fix Focus Areas
- src/lib/notifications/golf-message-fanout.ts[35-105]
- supabase/migrations/20260527000000_prod_public_baseline.sql[20153-20164]
- src/lib/supabase/server.ts[9-24]

### Implementation notes
- Use `createAdminClient()` (or a purpose-built RPC) to resolve recipient emails (and optionally ids) for notification fanout.
- For push + in-app notifications, you don’t need `users` rows at all—use `recipientUserIds` directly.
- Keep best-effort semantics (do not fail the message send on notification failures), but log per-channel failures with enough context (conversationId, recipient count).

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


View more (1)
4. Broadcast audience leaks on reuse 🐞 Bug ⛨ Security
Description
createGolfTeamBroadcastImpl reuses an existing same-titled team chat even when the newly
selectedPlayerIds imply a different audience, and it only adds missing participants without removing
stale ones—so future broadcasts can be delivered to unintended players from the prior audience.
Code

src/app/actions/messages.ts[R615-651]

+      // A broadcast with this title exists, but for a DIFFERENT audience
+      // than the one just selected. Reuse the oldest such thread (rather
+      // than fragmenting into yet another same-titled conversation) but
+      // reconcile membership first: add anyone newly selected who isn't
+      // already a participant, so the message the coach is about to send
+      // actually reaches every recipient they just chose. Never remove
+      // existing participants here — golf messaging never does destructive
+      // participant writes.
+      const reuseTarget = existingConvs[0];
+      if (reuseTarget) {
+        const { data: existingParticipants } = await supabase
+          .from('golf_conversation_participants')
+          .select('user_id')
+          .eq('conversation_id', reuseTarget.id);
+
+        const existingIds = new Set((existingParticipants ?? []).map((p) => p.user_id));
+        const missingIds = desiredParticipantIds.filter((id) => !existingIds.has(id));
+
+        if (missingIds.length > 0) {
+          const { error: syncError } = await supabase
+            .from('golf_conversation_participants')
+            .insert(
+              missingIds.map((userId) => ({
+                conversation_id: reuseTarget.id,
+                user_id: userId,
+                joined_at: new Date().toISOString(),
+              })),
+            );
+
+          if (syncError) {
+            await logServerError(`[Broadcast] Failed to sync participants on reuse: ${syncError instanceof Error ? syncError.message : String(syncError)}`, { action: 'messages.createGolfTeamBroadcast' });
+            throw new Error(`Failed to update broadcast recipients: ${syncError.message}`);
+          }
+        }
+
+        return { conversationId: reuseTarget.id, reused: true };
+      }
Relevance

⭐⭐ Medium

No prior review evidence on broadcast reuse needing participant removals; messaging work exists but
not this pattern.

PR-#282
PR-#329

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The function supports selectedPlayerIds to scope recipients, but the reuse path for a different
audience explicitly refuses to remove existing participants and only inserts missing participants
into the reused conversation, meaning prior recipients remain and will receive subsequent messages.

src/app/actions/messages.ts[503-651]

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

### Issue description
When a broadcast title already exists, the code reuses the oldest conversation even if the participant set differs, and only inserts `missingIds` while explicitly never removing existing participants. This can cause messages meant for a newly selected subset (e.g., JV) to also be delivered to prior recipients (e.g., Varsity) who remain participants.

### Issue Context
- `selectedPlayerIds` is explicitly supported to target a specific subset.
- Conversation participants define who can see/receive messages in that thread.
- The current logic makes same-title threads accumulate participants over time (union of audiences).

### Fix Focus Areas
- src/app/actions/messages.ts[503-651]

### Implementation notes
Choose one:
1) **Strict audience semantics (recommended):** if no exact participant-set match exists, create a NEW conversation (no reuse).
2) If reuse is required, reconcile to the new audience by removing stale participants (requires confirming product + ensuring non-destructive constraints don’t conflict), and ensure the coach is always included.

Also consider surfacing `reused` semantics to the UI so coaches understand whether they’re posting into an old thread.

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



Remediation recommended

5. baseball_practice missing registry mapping 📘 Rule violation ⚙ Maintainability
Description
The new/modified action metadata references the feature identifier baseball_practice, but there is
no corresponding entry visible in memory/registry.yml. This likely violates the requirement to
keep feature registry mappings in sync with feature identifiers used in code.
Code

src/app/baseball/actions/practice-planner-capability.ts[35]

+  { feature: 'baseball_practice', featureArea: 'baseball-practice' },
Relevance

⭐⭐⭐ High

Feature registry sync has precedent; PR296 updated memory/registry.yml after similar mapping
suggestion.

PR-#296

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519254 requires feature identifiers referenced in modified code to be present in
memory/registry.yml or explicitly marked as unmapped. The changed action metadata uses `feature:
'baseball_practice', while the registry section for baseball only shows baseball_core` and no
baseball_practice entry nearby.

Rule 1519254: Update feature registry mappings when touching unmapped features
src/app/baseball/actions/practice-planner-capability.ts[33-36]
memory/registry.yml[110-147]

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

## Issue description
A feature identifier referenced in changed code (`baseball_practice`) does not appear to have a mapping (or explicit gap marker) in `memory/registry.yml`.

## Issue Context
The action metadata is used for feature-aware observability/governance; the registry should include a mapping or an explicit awareness-gap marker per convention.

## Fix Focus Areas
- src/app/baseball/actions/practice-planner-capability.ts[33-36]
- memory/registry.yml[110-147]

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


6. Pending join writes not conditional 🐞 Bug ☼ Reliability
Description
approvePendingMember and rejectPendingMember check membership.status in a prior read but then
UPDATE/DELETE by id only; without a conditional predicate (e.g., eq('status','pending')) and
affected-row verification, concurrent processing can act on stale state and incorrectly
update/delete an already-processed membership.
Code

src/app/baseball/actions/roster.ts[R379-449]

+    const { error: updateError } = await supabase
+      .from('baseball_team_members')
+      .update({
+        status: 'active',
+        approved_by: coach?.id ?? null,
+        approved_at: new Date().toISOString(),
+      })
+      .eq('id', memberId);
+
+    if (updateError) {
+      throw updateError;
+    }
+
+    const playerRow = (membership as unknown as {
+      baseball_players: { first_name: string | null; last_name: string | null } | null;
+    }).baseball_players;
+
+    // Best-effort timeline side-effect — never rolls back the approval.
+    await appendRosterTimelineEvent({
+      teamId: membership.team_id,
+      playerId: membership.player_id,
+      title: `${playerDisplayName(playerRow)}'s join request was approved`,
+      visibility: 'team',
+      source: 'manual',
+      createdBy: ctx.user.id,
+    });
+
+    revalidateRoster();
+    return { success: true, memberId };
+  },
+);
+
+/**
+ * Decline a pending join request. This is a scoped single-membership DELETE
+ * (never the player's account/stats) — the player can rejoin later via the
+ * team's invite link if the decline was in error.
+ */
+export const rejectPendingMember = withBaseballAction(
+  'rejectPendingMember',
+  { featureArea: 'baseball-roster' },
+  async (ctx, args: { memberId: string }): Promise<PendingMemberActionResult> => {
+    const { memberId } = args;
+    if (!memberId) {
+      return { success: false, error: 'A membership is required.' };
+    }
+
+    const supabase = await createClient();
+    const { data: membership, error: memberError } = await loadPendingMembership(supabase, memberId);
+
+    if (memberError || !membership) {
+      return { success: false, error: 'That join request could not be found.' };
+    }
+
+    try {
+      await requireBaseballCapability(membership.team_id, 'can_manage_roster');
+    } catch {
+      return { success: false, error: 'You do not have permission to manage this roster.' };
+    }
+
+    if (membership.status !== 'pending') {
+      return { success: false, error: 'This request has already been processed.' };
+    }
+
+    const playerRow = (membership as unknown as {
+      baseball_players: { first_name: string | null; last_name: string | null } | null;
+    }).baseball_players;
+
+    const { error: deleteError } = await supabase
+      .from('baseball_team_members')
+      .delete()
+      .eq('id', memberId);
Relevance

⭐⭐⭐ High

Write integrity/affected-row enforcement previously accepted (requireWriteSuccess) to avoid
stale/false-success writes.

PR-#20

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both actions gate on membership.status !== 'pending' but the subsequent UPDATE/DELETE statements
do not include a status predicate, so the write is not tied to the state that was validated in the
read.

src/app/baseball/actions/roster.ts[347-467]

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

### Issue description
`approvePendingMember` and `rejectPendingMember` read the row (including `status`) and then write by `id` only. Under concurrency/double-submit, both requests can pass the `status === 'pending'` check and then both mutate the row because the mutation isn’t constrained to `status='pending'`.

### Issue Context
These are admin/coach actions that are realistic to double-click or run concurrently by multiple staff.

### Fix Focus Areas
- src/app/baseball/actions/roster.ts[369-390]
- src/app/baseball/actions/roster.ts[438-453]

### Implementation notes
- For approval: `...update({...}).eq('id', memberId).eq('status','pending').select('id').maybeSingle()` and error if no row updated.
- For rejection: `...delete().eq('id', memberId).eq('status','pending').select('id')` and error if no row deleted.
- Optionally add an idempotent response if the row is already processed, but ensure you don’t delete/overwrite non-pending rows.

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



Informational

7. Database imported via subpath 📘 Rule violation ⚙ Maintainability
Description
Changed code imports shared types from @/lib/types/database instead of the @/lib/types barrel,
which violates the required canonical type import path. This increases drift/duplication risk and
makes type reuse inconsistent across modules.
Code

src/app/golf/actions/golf.ts[32]

+import type { Database, Json } from '@/lib/types/database';
Relevance

⭐ Low

Canonical type-barrel import suggestions were explicitly rejected (e.g., @/lib/types/* to
@/lib/types).

PR-#290
PR-#776

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519311 requires type imports to come from @/lib/types. The modified/new files
import Database/Json from the non-canonical subpath @/lib/types/database.

Rule 1519311: Type imports must use '@/lib/types' and disallow deprecated paths
src/app/golf/actions/golf.ts[32-32]
src/lib/coachhelm/v3/qualifying/player-notify.ts[18-20]
src/test/coachhelm/v3/player-notify.test.ts[14-16]

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

## Issue description
Type-only imports must use `@/lib/types` (canonical barrel) rather than `@/lib/types/database`.

## Issue Context
The PR adds/changes imports of `Database`/`Json` from `@/lib/types/database` in multiple files.

## Fix Focus Areas
- src/app/golf/actions/golf.ts[32-32]
- src/lib/coachhelm/v3/qualifying/player-notify.ts[18-20]
- src/test/coachhelm/v3/player-notify.test.ts[14-16]
- src/lib/types/index.ts[1-6]

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


Grey Divider

Qodo Logo

// (20260710150000_baseball_practice_blocks_visibility_rls.sql).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const visibleBlocks = (p.blocks ?? []).filter((b: any) => b.visibility !== 'staff_only');
const visibleBlocks = (p.blocks ?? []).filter((b: any) => b.visibility == null || b.visibility === 'player_visible');

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. any used in block filter 📘 Rule violation ⚙ Maintainability

The updated practice visibility filter keeps an explicit any annotation, violating the no-any
TypeScript requirement. This weakens type safety around visibility enforcement and can hide future
logic/type regressions.
Agent Prompt
## Issue description
Changed code introduces/retains explicit `any` in TypeScript in modified lines.

## Issue Context
Compliance requires eliminating `any` annotations/assertions in changed TS/TSX. These instances are in production action code paths.

## Fix Focus Areas
- src/app/baseball/actions/practice.ts[763-766]
- src/app/actions/messages.ts[401-413]
- src/app/golf/actions/recurring-events.ts[682-688]

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

@@ -0,0 +1,40 @@
'use server';

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. use server file outside actions 📘 Rule violation ⌂ Architecture

A new server action module ('use server') was added under src/app/baseball/actions/ instead of
src/app/actions/. This violates the required server-action placement convention and makes action
discovery/enforcement inconsistent.
Agent Prompt
## Issue description
Server action files must live under `src/app/actions/`, but this PR adds a `'use server'` module under `src/app/baseball/actions/`.

## Issue Context
The module is clearly a server action file (top-level `'use server'` directive) and exports an action wrapper.

## Fix Focus Areas
- src/app/baseball/actions/practice-planner-capability.ts[1-40]

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

*/
export const getCanManagePractice = withBaseballAction(
'getCanManagePractice',
{ feature: 'baseball_practice', featureArea: 'baseball-practice' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. baseball_practice missing registry mapping 📘 Rule violation ⚙ Maintainability

The new/modified action metadata references the feature identifier baseball_practice, but there is
no corresponding entry visible in memory/registry.yml. This likely violates the requirement to
keep feature registry mappings in sync with feature identifiers used in code.
Agent Prompt
## Issue description
A feature identifier referenced in changed code (`baseball_practice`) does not appear to have a mapping (or explicit gap marker) in `memory/registry.yml`.

## Issue Context
The action metadata is used for feature-aware observability/governance; the registry should include a mapping or an explicit awareness-gap marker per convention.

## Fix Focus Areas
- src/app/baseball/actions/practice-planner-capability.ts[33-36]
- memory/registry.yml[110-147]

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

Comment on lines +35 to +66
const { data: otherParticipants } = await supabase
.from('golf_conversation_participants')
.select('user_id')
.eq('conversation_id', conversationId)
.neq('user_id', senderId);

if (!otherParticipants || otherParticipants.length === 0) return;

const recipientUserIds = otherParticipants.map(p => p.user_id);

// Batch every lookup into ONE round-trip of parallel queries: sender name
// (coach → player → email fallback) and all recipient emails via a single
// .in() instead of sequential per-recipient fetches.
const [
{ data: senderCoach },
{ data: senderPlayer },
{ data: senderUser },
{ data: recipientProfiles },
] = await Promise.all([
supabase.from('golf_coaches').select('full_name').eq('user_id', senderId).maybeSingle(),
supabase.from('golf_players').select('first_name, last_name').eq('user_id', senderId).maybeSingle(),
supabase.from('users').select('email').eq('id', senderId).maybeSingle(),
supabase.from('users').select('id, email').in('id', recipientUserIds),
]);

const senderName = senderCoach?.full_name
|| (senderPlayer ? `${senderPlayer.first_name || ''} ${senderPlayer.last_name || ''}`.trim() : '')
|| senderUser?.email
|| 'Someone';

if (!recipientProfiles) 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.

Action required

5. Message fanout recipient lookup fails 🐞 Bug ≡ Correctness

notifyGolfMessageRecipients excludes the sender from recipientUserIds, then attempts to SELECT
recipient rows from public.users using the request-scoped Supabase client, but users RLS only
permits selecting your own row—so recipientProfiles will be empty and email/push/in-app
notifications won’t be sent to recipients.
Agent Prompt
### Issue description
`notifyGolfMessageRecipients()` builds `recipientUserIds` as “all other participants” and then calls `supabase.from('users').select('id, email').in('id', recipientUserIds)`. With current RLS (`users_select_own`), the sender cannot read any of those rows (they are *explicitly not* the sender), so the function returns early and no notifications are sent.

### Issue Context
- The server `createClient()` uses the anon key + user cookies, so RLS applies.
- `users_select_own` restricts SELECT to `auth.uid() = id`.
- `recipientUserIds` explicitly excludes `senderId`, guaranteeing the RLS-visible set is empty.

### Fix Focus Areas
- src/lib/notifications/golf-message-fanout.ts[35-105]
- supabase/migrations/20260527000000_prod_public_baseline.sql[20153-20164]
- src/lib/supabase/server.ts[9-24]

### Implementation notes
- Use `createAdminClient()` (or a purpose-built RPC) to resolve recipient emails (and optionally ids) for notification fanout.
- For push + in-app notifications, you don’t need `users` rows at all—use `recipientUserIds` directly.
- Keep best-effort semantics (do not fail the message send on notification failures), but log per-channel failures with enough context (conversationId, recipient count).

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

Comment on lines +615 to +651
// A broadcast with this title exists, but for a DIFFERENT audience
// than the one just selected. Reuse the oldest such thread (rather
// than fragmenting into yet another same-titled conversation) but
// reconcile membership first: add anyone newly selected who isn't
// already a participant, so the message the coach is about to send
// actually reaches every recipient they just chose. Never remove
// existing participants here — golf messaging never does destructive
// participant writes.
const reuseTarget = existingConvs[0];
if (reuseTarget) {
const { data: existingParticipants } = await supabase
.from('golf_conversation_participants')
.select('user_id')
.eq('conversation_id', reuseTarget.id);

const existingIds = new Set((existingParticipants ?? []).map((p) => p.user_id));
const missingIds = desiredParticipantIds.filter((id) => !existingIds.has(id));

if (missingIds.length > 0) {
const { error: syncError } = await supabase
.from('golf_conversation_participants')
.insert(
missingIds.map((userId) => ({
conversation_id: reuseTarget.id,
user_id: userId,
joined_at: new Date().toISOString(),
})),
);

if (syncError) {
await logServerError(`[Broadcast] Failed to sync participants on reuse: ${syncError instanceof Error ? syncError.message : String(syncError)}`, { action: 'messages.createGolfTeamBroadcast' });
throw new Error(`Failed to update broadcast recipients: ${syncError.message}`);
}
}

return { conversationId: reuseTarget.id, reused: true };
}

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. Broadcast audience leaks on reuse 🐞 Bug ⛨ Security

createGolfTeamBroadcastImpl reuses an existing same-titled team chat even when the newly
selectedPlayerIds imply a different audience, and it only adds missing participants without removing
stale ones—so future broadcasts can be delivered to unintended players from the prior audience.
Agent Prompt
### Issue description
When a broadcast title already exists, the code reuses the oldest conversation even if the participant set differs, and only inserts `missingIds` while explicitly never removing existing participants. This can cause messages meant for a newly selected subset (e.g., JV) to also be delivered to prior recipients (e.g., Varsity) who remain participants.

### Issue Context
- `selectedPlayerIds` is explicitly supported to target a specific subset.
- Conversation participants define who can see/receive messages in that thread.
- The current logic makes same-title threads accumulate participants over time (union of audiences).

### Fix Focus Areas
- src/app/actions/messages.ts[503-651]

### Implementation notes
Choose one:
1) **Strict audience semantics (recommended):** if no exact participant-set match exists, create a NEW conversation (no reuse).
2) If reuse is required, reconcile to the new audience by removing stale participants (requires confirming product + ensuring non-destructive constraints don’t conflict), and ensure the coach is always included.

Also consider surfacing `reused` semantics to the UI so coaches understand whether they’re posting into an old thread.

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

Comment on lines +379 to +449
const { error: updateError } = await supabase
.from('baseball_team_members')
.update({
status: 'active',
approved_by: coach?.id ?? null,
approved_at: new Date().toISOString(),
})
.eq('id', memberId);

if (updateError) {
throw updateError;
}

const playerRow = (membership as unknown as {
baseball_players: { first_name: string | null; last_name: string | null } | null;
}).baseball_players;

// Best-effort timeline side-effect — never rolls back the approval.
await appendRosterTimelineEvent({
teamId: membership.team_id,
playerId: membership.player_id,
title: `${playerDisplayName(playerRow)}'s join request was approved`,
visibility: 'team',
source: 'manual',
createdBy: ctx.user.id,
});

revalidateRoster();
return { success: true, memberId };
},
);

/**
* Decline a pending join request. This is a scoped single-membership DELETE
* (never the player's account/stats) — the player can rejoin later via the
* team's invite link if the decline was in error.
*/
export const rejectPendingMember = withBaseballAction(
'rejectPendingMember',
{ featureArea: 'baseball-roster' },
async (ctx, args: { memberId: string }): Promise<PendingMemberActionResult> => {
const { memberId } = args;
if (!memberId) {
return { success: false, error: 'A membership is required.' };
}

const supabase = await createClient();
const { data: membership, error: memberError } = await loadPendingMembership(supabase, memberId);

if (memberError || !membership) {
return { success: false, error: 'That join request could not be found.' };
}

try {
await requireBaseballCapability(membership.team_id, 'can_manage_roster');
} catch {
return { success: false, error: 'You do not have permission to manage this roster.' };
}

if (membership.status !== 'pending') {
return { success: false, error: 'This request has already been processed.' };
}

const playerRow = (membership as unknown as {
baseball_players: { first_name: string | null; last_name: string | null } | null;
}).baseball_players;

const { error: deleteError } = await supabase
.from('baseball_team_members')
.delete()
.eq('id', memberId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

7. Pending join writes not conditional 🐞 Bug ☼ Reliability

approvePendingMember and rejectPendingMember check membership.status in a prior read but then
UPDATE/DELETE by id only; without a conditional predicate (e.g., eq('status','pending')) and
affected-row verification, concurrent processing can act on stale state and incorrectly
update/delete an already-processed membership.
Agent Prompt
### Issue description
`approvePendingMember` and `rejectPendingMember` read the row (including `status`) and then write by `id` only. Under concurrency/double-submit, both requests can pass the `status === 'pending'` check and then both mutate the row because the mutation isn’t constrained to `status='pending'`.

### Issue Context
These are admin/coach actions that are realistic to double-click or run concurrently by multiple staff.

### Fix Focus Areas
- src/app/baseball/actions/roster.ts[369-390]
- src/app/baseball/actions/roster.ts[438-453]

### Implementation notes
- For approval: `...update({...}).eq('id', memberId).eq('status','pending').select('id').maybeSingle()` and error if no row updated.
- For rejection: `...delete().eq('id', memberId).eq('status','pending').select('id')` and error if no row deleted.
- Optionally add an idempotent response if the row is already processed, but ensure you don’t delete/overwrite non-pending rows.

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

@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: 10

Caution

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

⚠️ Outside diff range comments (2)
src/lib/baseball/read-models/roster.ts (1)

130-164: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Parallelize the two independent aggregate queries.

baseball_player_aggregates and baseball_player_season_stats only depend on teamId and are fetched sequentially. Run them via Promise.all to cut one round trip off this read path, mirroring the Promise.all pattern already used elsewhere in this PR (e.g. lift/page.tsx's fetchPlayerSessions).

⚡ Suggested parallelization
-  // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  const { data: aggregatesData, error: aggregatesError } = await (supabase as any)
-    .from('baseball_player_aggregates')
-    .select('*')
-    .eq('team_id', teamId) as {
-    data: BaseballPlayerAggregates[] | null;
-    error: unknown;
-  };
-
-  let aggregates: Record<string, BaseballPlayerAggregates> = {};
-  if (!aggregatesError && aggregatesData) {
-    for (const agg of aggregatesData) {
-      aggregates[agg.player_id] = agg;
-    }
-  }
-
-  const currentSeasonYear = new Date().getFullYear();
-  const { data: seasonStatsData, error: seasonStatsError } = await supabase
-    .from('baseball_player_season_stats')
-    .select('player_id, avg, obp, slg, ops, g, last_updated')
-    .eq('team_id', teamId)
-    .eq('season_year', currentSeasonYear);
+  const currentSeasonYear = new Date().getFullYear();
+  const [
+    // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
+    { data: aggregatesData, error: aggregatesError },
+    { data: seasonStatsData, error: seasonStatsError },
+  ] = await Promise.all([
+    (supabase as any)
+      .from('baseball_player_aggregates')
+      .select('*')
+      .eq('team_id', teamId) as Promise<{
+      data: BaseballPlayerAggregates[] | null;
+      error: unknown;
+    }>,
+    supabase
+      .from('baseball_player_season_stats')
+      .select('player_id, avg, obp, slg, ops, g, last_updated')
+      .eq('team_id', teamId)
+      .eq('season_year', currentSeasonYear),
+  ]);
+
+  let aggregates: Record<string, BaseballPlayerAggregates> = {};
+  if (!aggregatesError && aggregatesData) {
+    for (const agg of aggregatesData) {
+      aggregates[agg.player_id] = agg;
+    }
+  }
🤖 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/read-models/roster.ts` around lines 130 - 164, Parallelize
the independent queries in the roster read path by starting the
baseball_player_aggregates and baseball_player_season_stats requests together
with Promise.all, then destructure their results and preserve the existing error
handling and merge behavior in mergeSeasonStatsIntoAggregates.
src/components/fairway/pages/calendar/FairwayMonthGrid.tsx (1)

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

Cancelled cue is tooltip-only — not exposed to assistive tech.

title renders "${e.title} (cancelled)" but that string is never surfaced as an accessible name; screen readers announce only the visible child text (e.title), so the cancelled status is invisible non-visually.

♿️ Proposed fix
                       onClick={onEventClick ? () => onEventClick(e) : undefined}
                       title={isCancelled ? `${e.title} (cancelled)` : e.title}
+                      aria-label={isCancelled ? `${e.title} (cancelled)` : undefined}
                       className={cn(

As per path instructions, "Components handling user input must have visible focus rings, aria-labels, and keyboard handlers."

🤖 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/fairway/pages/calendar/FairwayMonthGrid.tsx` around lines 216
- 236, Expose the cancelled status to assistive technology for each event Button
rendered in the calendar grid. Update the Button’s accessible label, using the
same title-with-cancelled-suffix logic currently used by title, while preserving
the visible event text and existing click/keyboard behavior; ensure the Button
retains a visible focus ring consistent with the component’s accessibility
requirements.

Source: Path instructions

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

Inline comments:
In `@src/app/actions/messages.ts`:
- Around line 401-414: Fix the golf message-read failure logging in the messages
action: since messagesError is typed as an object with a message field, the
downstream instanceof Error check produces “[object Object]”. Update the
non-fatal log to use messagesError.message directly, while preserving null
handling and the existing baseball path.

In `@src/app/golf/actions/message-attachments.ts`:
- Around line 135-144: Isolate notifyGolfMessageRecipients from the outer send
try/catch so notification failures cannot turn an already-persisted message into
a failed result. Catch and log fan-out errors after the message insert succeeds,
while still returning the successful send response and preserving the existing
message ID.

In `@src/app/golf/actions/tasks.ts`:
- Around line 465-488: The reminder replacement in setTaskReminder is not atomic
because delete, insert, and task update occur in separate operations. Refactor
setTaskReminder to perform the pending golf_task_reminders deletion, replacement
insertion, and related golf_tasks update through one transactional RPC or
database transaction, preserving error handling and returning failure if any
step rolls back.

In
`@src/components/baseball/practice-planner/__tests__/PracticeRecapPanel.test.tsx`:
- Around line 94-96: Update the Cancel button assertion in PracticeRecapPanel
tests to require exactly two matching buttons, replacing the current
greater-than-or-equal-to-one check so it verifies both the composer and panel
row remain inline.

In `@src/components/fairway/pages/qualifiers/FairwayQualifyingWorkspace.tsx`:
- Line 160: Update ConcludeQualifier’s status prop to use the existing
QualifierStatus-style union type from the qualifying types module instead of
string, and ensure the workspace.status value and its comparison with
'completed' use that narrowed type.

In `@src/lib/baseball/read-models/__tests__/roster-aggregates-merge.test.ts`:
- Around line 61-76: Add a test alongside the existing
mergeSeasonStatsIntoAggregates case that uses a legacy aggregate with
total_sessions lower than the season-stats g value, then assert the merged
player’s total_sessions equals the box-score g value. Keep the setup focused on
the same player and preserve assertions that validate the MAX behavior.

In `@src/lib/coachhelm/v3/qualifying/player-notify.ts`:
- Around line 40-54: Collapse the sequential player and user queries in the
notification logic into one Supabase query by selecting the player fields and
embedding the related user’s id and email through the relationship. Update the
result transformation to build the required player-to-user and user-to-email
mappings from the nested response, preserving the existing early return behavior
when no rows or related users are present.

In
`@supabase/migrations/20260710020000_baseball_settings_audit_log_column_reconcile.sql`:
- Around line 1-10: Reword the header comment in the migration so it no longer
contains the exact CREATE TABLE phrase targeted by semgrep, while preserving the
explanation that an existing older-schema table prevented the intended
definition from being applied. Do not change the ALTER TABLE or
policy-reconciliation SQL.
- Around line 37-43: Add a dedicated index for the foreign-key column
actor_coach_id after the ALTER TABLE statement, using a clear name consistent
with the existing indexes. Keep the foreign key definition unchanged and ensure
the index is created idempotently with CREATE INDEX IF NOT EXISTS.

In `@supabase/migrations/20260710031500_baseball_decision_log_kind_reconcile.sql`:
- Around line 6-9: Reword the root-cause comment in the migration to remove the
literal “CREATE TABLE IF NOT EXISTS” DDL pattern, while preserving the
explanation that the earlier migration’s conditional table creation was skipped
because an unrelated legacy table already existed. Do not change the migration’s
ALTER TABLE, constraint, or index statements.

---

Outside diff comments:
In `@src/components/fairway/pages/calendar/FairwayMonthGrid.tsx`:
- Around line 216-236: Expose the cancelled status to assistive technology for
each event Button rendered in the calendar grid. Update the Button’s accessible
label, using the same title-with-cancelled-suffix logic currently used by title,
while preserving the visible event text and existing click/keyboard behavior;
ensure the Button retains a visible focus ring consistent with the component’s
accessibility requirements.

In `@src/lib/baseball/read-models/roster.ts`:
- Around line 130-164: Parallelize the independent queries in the roster read
path by starting the baseball_player_aggregates and baseball_player_season_stats
requests together with Promise.all, then destructure their results and preserve
the existing error handling and merge behavior in
mergeSeasonStatsIntoAggregates.
🪄 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: c41a0080-8edb-4a6f-a230-e7d410c80497

📥 Commits

Reviewing files that changed from the base of the PR and between 1035e6b and 1234fc7.

⛔ Files ignored due to path filters (1)
  • src/lib/types/database.ts is excluded by !src/lib/types/database.ts
📒 Files selected for processing (94)
  • src/app/actions/messages.ts
  • src/app/baseball/(dashboard)/dashboard/academics/AcademicsClient.tsx
  • src/app/baseball/(dashboard)/dashboard/academics/ClassScheduleModal.tsx
  • src/app/baseball/(dashboard)/dashboard/compare/CompareClient.tsx
  • src/app/baseball/(dashboard)/dashboard/compare/__tests__/search-recruitability.test.ts
  • src/app/baseball/(dashboard)/dashboard/compare/actions.ts
  • src/app/baseball/(dashboard)/dashboard/lift/page.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/RosterClient.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/RosterMemberActions.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/SavedLineupsPanel.tsx
  • src/app/baseball/(dashboard)/dashboard/watchlist/WatchlistClient.tsx
  • src/app/baseball/actions/__tests__/academics-coach-gating.test.ts
  • src/app/baseball/actions/__tests__/assign-player-membership-fields.test.ts
  • src/app/baseball/actions/__tests__/decision-log-column-reconcile.test.ts
  • src/app/baseball/actions/__tests__/dev-plans-coach-gating.test.ts
  • src/app/baseball/actions/__tests__/imports-event-grain-guard.test.ts
  • src/app/baseball/actions/__tests__/imports-registry.test.ts
  • src/app/baseball/actions/academics.ts
  • src/app/baseball/actions/decision-room.ts
  • src/app/baseball/actions/dev-plans.ts
  • src/app/baseball/actions/imports.ts
  • src/app/baseball/actions/practice-planner-capability.ts
  • src/app/baseball/actions/practice.ts
  • src/app/baseball/actions/program-settings.ts
  • src/app/baseball/actions/roster.ts
  • src/app/golf/(dashboard)/dashboard/qualifiers/[id]/edit/error.tsx
  • src/app/golf/(dashboard)/dashboard/qualifiers/[id]/edit/loading.tsx
  • src/app/golf/(dashboard)/dashboard/qualifiers/[id]/edit/page.tsx
  • src/app/golf/(dashboard)/dashboard/qualifiers/[id]/page.tsx
  • src/app/golf/(dashboard)/dashboard/recruiting/page.tsx
  • src/app/golf/(dashboard)/dashboard/rounds/[id]/review/CoachNotesSection.tsx
  • src/app/golf/(dashboard)/dashboard/rounds/[id]/review/__tests__/CoachNotesSection.test.tsx
  • src/app/golf/(dashboard)/dashboard/rounds/[id]/review/page.tsx
  • src/app/golf/actions/__tests__/golf-save-partial-round.test.ts
  • src/app/golf/actions/__tests__/recurring-events.test.ts
  • src/app/golf/actions/development.ts
  • src/app/golf/actions/golf.ts
  • src/app/golf/actions/message-attachments.ts
  • src/app/golf/actions/recruiting.ts
  • src/app/golf/actions/recurring-events.ts
  • src/app/golf/actions/round-reviews.ts
  • src/app/golf/actions/tasks.ts
  • src/app/golf/actions/travel.ts
  • src/app/lifting/(dashboard)/dashboard/lift/page.tsx
  • src/components/baseball/import-center/ImportWizardClient.tsx
  • src/components/baseball/player-profile/PlayerNotesSection.tsx
  • src/components/baseball/player-profile/PlayerProfileClient.tsx
  • src/components/baseball/practice-planner/PracticePlannerClient.tsx
  • src/components/baseball/practice-planner/PracticePrintExport.tsx
  • src/components/baseball/practice-planner/PracticeRecapPanel.tsx
  • src/components/baseball/practice-planner/__tests__/PracticeRecapPanel.test.tsx
  • src/components/coach/lineup/LineupBuilder.tsx
  • src/components/fairway/pages/calendar/FairwayEventCard.tsx
  • src/components/fairway/pages/calendar/FairwayMonthGrid.tsx
  • src/components/fairway/pages/coachhelm/FairwayStatsCockpit.tsx
  • src/components/fairway/pages/coachhelm/FocusAreaCard.tsx
  • src/components/fairway/pages/coachhelm/PlayersGridView.tsx
  • src/components/fairway/pages/qualifiers/FairwayEditQualifier.tsx
  • src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx
  • src/components/fairway/pages/qualifiers/FairwayQualifierLeaderboard.tsx
  • src/components/fairway/pages/qualifiers/FairwayQualifyingWorkspace.tsx
  • src/components/fairway/pages/roster/FairwayJoinRequests.test.tsx
  • src/components/fairway/pages/roster/FairwayJoinRequests.tsx
  • src/components/fairway/pages/settings/FairwaySettingsCoachingIntelligence.tsx
  • src/components/golf/calendar/PremiumCalendarClient.tsx
  • src/components/lifting/players/PlayerLiftHomeClient.tsx
  • src/components/lifting/players/__tests__/PlayerLiftHomeClient.test.tsx
  • src/lib/admin/__tests__/coverage-contract.foundation.test.ts
  • src/lib/admin/__tests__/feature-registry.test.ts
  • src/lib/admin/__tests__/github-pr-timeline.test.ts
  • src/lib/admin/feature-registry.ts
  • src/lib/admin/github-pr-timeline.ts
  • src/lib/baseball/nav-registry.ts
  • src/lib/baseball/read-models/__tests__/roster-aggregates-merge.test.ts
  • src/lib/baseball/read-models/roster-aggregates-merge.ts
  • src/lib/baseball/read-models/roster.ts
  • src/lib/baseball/stat-layer-manifest.ts
  • src/lib/coachhelm/v3/qualifying/loader.ts
  • src/lib/coachhelm/v3/qualifying/player-notify.ts
  • src/lib/coachhelm/v3/qualifying/service.ts
  • src/lib/coachhelm/v3/qualifying/types.ts
  • src/lib/notifications/golf-message-fanout.ts
  • src/lib/notifications/push.ts
  • src/test/coachhelm/v3/confirm-selection-chat.integration.test.ts
  • src/test/coachhelm/v3/player-notify.test.ts
  • src/test/coachhelm/v3/travel-brief.test.ts
  • src/test/fairway/settings-coaching-intelligence.test.tsx
  • src/test/golf/actions/development.test.ts
  • src/test/lib/notifications/push.test.ts
  • supabase/migrations/20260710020000_baseball_settings_audit_log_column_reconcile.sql
  • supabase/migrations/20260710031500_baseball_decision_log_kind_reconcile.sql
  • supabase/migrations/20260710150000_baseball_practice_blocks_visibility_rls.sql
  • supabase/migrations/20260710160000_mark_golf_messages_read_rpc.sql

Comment on lines +401 to +414
let messagesError: { message: string } | null = null;
if (sport === 'golf') {
const { error } = await (supabase as any).rpc('mark_golf_messages_read', {
p_conversation_id: conversationId,
});
messagesError = error;
} else {
const { error } = await supabase
.from('baseball_messages' as any)
.update({ read: true })
.eq('conversation_id', conversationId)
.neq('sender_id', user.id);
messagesError = error;
}

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 | 🟡 Minor | ⚡ Quick win

Error message is lost in the log on the golf-RPC failure path.

messagesError is typed { message: string } | null, so the downstream log at Line 417 (messagesError instanceof Error ? messagesError.message : String(messagesError)) always hits the String() branch and emits [object Object]. That defeats the "logged as non-fatal" observability this golf change explicitly relies on. Log messagesError.message directly.

🔭 Fix (line 417)
-    await logServerError(`[Messages] Failed to mark messages as read: ${messagesError instanceof Error ? messagesError.message : String(messagesError)}`, { action: 'messages.markMessagesAsRead' });
+    await logServerError(`[Messages] Failed to mark messages as read: ${messagesError.message}`, { action: 'messages.markMessagesAsRead' });
🤖 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/actions/messages.ts` around lines 401 - 414, Fix the golf
message-read failure logging in the messages action: since messagesError is
typed as an object with a message field, the downstream instanceof Error check
produces “[object Object]”. Update the non-fatal log to use
messagesError.message directly, while preserving null handling and the existing
baseball path.

Comment on lines +135 to +144
// Fan out email/push/in-app notifications to the other participants —
// mirrors sendGolfMessageImpl's text-only fan-out (P1: this attachments
// path previously fired none of these, so a photo/document send was
// invisible to the recipient until they manually opened Messages).
await notifyGolfMessageRecipients(
conversationId,
user.id,
buildAttachmentPreview(content, attachments),
);

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 | 🟠 Major | ⚡ Quick win

Notification failure will falsely report the whole send as failed.

notifyGolfMessageRecipients (Line 139-143) runs inside the same try that already committed the message row (Line 87-97). If it throws, control falls to the outer catch (Line 146), which returns { success: false, error: ... } — even though the message was already persisted. A client that retries on success: false will re-insert a duplicate message.

🔧 Proposed fix: isolate fan-out failure from the send result
-    await notifyGolfMessageRecipients(
-      conversationId,
-      user.id,
-      buildAttachmentPreview(content, attachments),
-    );
+    try {
+      await notifyGolfMessageRecipients(
+        conversationId,
+        user.id,
+        buildAttachmentPreview(content, attachments),
+      );
+    } catch (notifyErr) {
+      await logServerError(
+        `[Attachments] Notification fan-out failed: ${notifyErr instanceof Error ? notifyErr.message : String(notifyErr)}`,
+        { action: 'message_attachments.sendGolfMessageWithAttachments' },
+      );
+      // Message already persisted — don't fail the whole send over a notify error.
+    }
📝 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
// Fan out email/push/in-app notifications to the other participants —
// mirrors sendGolfMessageImpl's text-only fan-out (P1: this attachments
// path previously fired none of these, so a photo/document send was
// invisible to the recipient until they manually opened Messages).
await notifyGolfMessageRecipients(
conversationId,
user.id,
buildAttachmentPreview(content, attachments),
);
// Fan out email/push/in-app notifications to the other participants —
// mirrors sendGolfMessageImpl's text-only fan-out (P1: this attachments
// path previously fired none of these, so a photo/document send was
// invisible to the recipient until they manually opened Messages).
try {
await notifyGolfMessageRecipients(
conversationId,
user.id,
buildAttachmentPreview(content, attachments),
);
} catch (notifyErr) {
await logServerError(
`[Attachments] Notification fan-out failed: ${notifyErr instanceof Error ? notifyErr.message : String(notifyErr)}`,
{ action: 'message_attachments.sendGolfMessageWithAttachments' },
);
// Message already persisted — don't fail the whole send over a notify error.
}
🤖 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/golf/actions/message-attachments.ts` around lines 135 - 144, Isolate
notifyGolfMessageRecipients from the outer send try/catch so notification
failures cannot turn an already-persisted message into a failed result. Catch
and log fan-out errors after the message insert succeeds, while still returning
the successful send response and preserving the existing message ID.

Comment thread src/app/golf/actions/tasks.ts Outdated
Comment on lines +465 to +488
const { error: clearQueueError } = await supabase
.from('golf_task_reminders')
.delete()
.eq('task_id', taskId)
.eq('sent', false);

if (clearQueueError) {
await logServerError(`[setTaskReminder Error]: ${clearQueueError instanceof Error ? clearQueueError.message : String(clearQueueError)}`, { action: 'tasks.setTaskReminder' });
return { success: false, error: clearQueueError.message };
}

const { error: queueError } = await supabase
.from('golf_task_reminders')
.insert({
task_id: taskId,
scheduled_for: reminderAt,
reminder_type: 'in_app',
sent: false,
});

if (queueError) {
await logServerError(`[setTaskReminder Error]: ${queueError instanceof Error ? queueError.message : String(queueError)}`, { action: 'tasks.setTaskReminder' });
return { success: false, error: queueError.message };
}

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does the review gate flag delete->insert on the same table here?
fd -e yml -e yaml . .coderabbit .greptile 2>/dev/null | xargs rg -n -i 'delete.*insert|destructive' 2>/dev/null
rg -n "mark_golf_messages_read|task_reminders" -g '*.sql' supabase/migrations

Repository: njrini99-code/helmv3

Length of output: 1992


Wrap the reminder replacement in one transaction src/app/golf/actions/tasks.ts:465-499 deletes the pending golf_task_reminders row and inserts a replacement in separate round-trips. If the insert fails after the delete succeeds, the reminder is lost, and the later golf_tasks update can still leave the UI showing a reminder that will never fire. Move the delete/insert/update into one atomic RPC or transaction.

🧰 Tools
🪛 GitHub Actions: Review Gate / 10_semgrep (custom rules).txt

[error] 465-474: coderabbit.semgrep.helmv3-destructive-write-pattern: DELETE followed by INSERT/clear path on the same table in a save/sync/submit path (risk of permanent data loss).

🪛 GitHub Actions: Review Gate / semgrep (custom rules)

[error] 465-474: Semgrep (coderabbit.semgrep.helmv3-destructive-write-pattern): Blocking. DELETE followed by INSERT on the same table in a save/sync/submit path (risk of permanent data loss on transient failure).

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

In `@src/app/golf/actions/tasks.ts` around lines 465 - 488, The reminder
replacement in setTaskReminder is not atomic because delete, insert, and task
update occur in separate operations. Refactor setTaskReminder to perform the
pending golf_task_reminders deletion, replacement insertion, and related
golf_tasks update through one transactional RPC or database transaction,
preserving error handling and returning failure if any step rolls back.

Comment on lines +94 to +96
expect(screen.getByText('First-step reads')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save recap/i })).toBeInTheDocument();
expect(screen.getAllByRole('button', { name: 'Cancel' }).length).toBeGreaterThanOrEqual(1);

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 | 🟡 Minor | ⚡ Quick win

Assertion doesn't verify the documented invariant.

The comment states two "Cancel" buttons must coexist (composer's own + panel's row) as proof the composer stayed inline, but toBeGreaterThanOrEqual(1) passes even if the composer collapsed to a single Cancel. Assert the exact count to make the regression guard match its intent.

💚 Tighten the guard
-    expect(screen.getAllByRole('button', { name: 'Cancel' }).length).toBeGreaterThanOrEqual(1);
+    expect(screen.getAllByRole('button', { name: 'Cancel' })).toHaveLength(2);
📝 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
expect(screen.getByText('First-step reads')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save recap/i })).toBeInTheDocument();
expect(screen.getAllByRole('button', { name: 'Cancel' }).length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('First-step reads')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save recap/i })).toBeInTheDocument();
expect(screen.getAllByRole('button', { name: 'Cancel' })).toHaveLength(2);
🤖 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/practice-planner/__tests__/PracticeRecapPanel.test.tsx`
around lines 94 - 96, Update the Cancel button assertion in PracticeRecapPanel
tests to require exactly two matching buttons, replacing the current
greater-than-or-equal-to-one check so it verifies both the composer and panel
row remain inline.

* golf.ts) — this button is the manual override for a coach who doesn't want
* to wait for stragglers, or who never lets that auto-transition fire.
* ────────────────────────────────────────────────────────────────────────── */
function ConcludeQualifier({ qualifierId, status }: { qualifierId: string; status: string }) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Widen-then-narrow: status: string throws away the real union type.

ConcludeQualifier's prop is typed as bare string at Line 160, but workspace.status (destructured at Line 84) almost certainly comes from a typed QualifierStatus-style union in @/lib/coachhelm/v3/qualifying/types. Comparing a widened string against the literal 'completed' at Line 164 means a typo like 'compelted' would compile silently instead of erroring.

♻️ Proposed fix
+import type { QualifierStatus } from '`@/lib/coachhelm/v3/qualifying/types`';
-function ConcludeQualifier({ qualifierId, status }: { qualifierId: string; status: string }) {
+function ConcludeQualifier({ qualifierId, status }: { qualifierId: string; status: QualifierStatus }) {
📝 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
function ConcludeQualifier({ qualifierId, status }: { qualifierId: string; status: string }) {
import type { QualifierStatus } from '`@/lib/coachhelm/v3/qualifying/types`';
function ConcludeQualifier({ qualifierId, status }: { qualifierId: string; status: QualifierStatus }) {
🤖 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/fairway/pages/qualifiers/FairwayQualifyingWorkspace.tsx` at
line 160, Update ConcludeQualifier’s status prop to use the existing
QualifierStatus-style union type from the qualifying types module instead of
string, and ensure the workspace.status value and its comparison with
'completed' use that narrowed type.

Comment on lines +61 to +76
it('prefers season-stats figures over a legacy aggregates row for the same player', () => {
const legacy = { p1: legacyAggregate({ career_avg: 0.1, career_ops: 0.3, total_sessions: 40 }) };

const merged = mergeSeasonStatsIntoAggregates(
legacy,
[{ player_id: 'p1', avg: 0.35, obp: 0.42, slg: 0.55, ops: 0.97, g: 20, last_updated: '2026-06-15T00:00:00.000Z' }],
'team1',
);

// Season-stats rate figures win — they're the box-score-canonical source.
expect(merged.p1?.career_avg).toBe(0.35);
expect(merged.p1?.career_ops).toBe(0.97);
// total_sessions takes the MAX of legacy vs. box-score game count, so a
// team with a longer legacy history never regresses to a smaller number.
expect(merged.p1?.total_sessions).toBe(40);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Add the missing MAX-direction test case.

Only the "legacy total_sessions > box-score g" direction is asserted (40 vs 20 → 40). Add a case where box-score g exceeds legacy total_sessions to lock the other half of the MAX contract described in the surrounding comments.

🤖 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/read-models/__tests__/roster-aggregates-merge.test.ts`
around lines 61 - 76, Add a test alongside the existing
mergeSeasonStatsIntoAggregates case that uses a legacy aggregate with
total_sessions lower than the season-stats g value, then assert the merged
player’s total_sessions equals the box-score g value. Keep the setup focused on
the same player and preserve assertions that validate the MAX behavior.

Comment on lines +40 to +54
const { data: playerRows } = await supabase
.from('golf_players')
.select('id, user_id')
.in('id', playerIds);
if (!playerRows?.length) return;

const userIdByPlayer = new Map(playerRows.map((p) => [p.id, p.user_id]));

const { data: userRows } = await supabase
.from('users')
.select('id, email')
.in('id', playerRows.map((p) => p.user_id));
if (!userRows?.length) return;

const emailByUser = new Map(userRows.map((u) => [u.id, u.email]));

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

Two round-trips that could be one join.

golf_players (Lines 40-43) then users (Lines 48-51) are fetched sequentially even though the second query's .in() list is entirely derived from the first result. Supabase supports embedded resource selection — collapsing this into a single query with a nested select removes one network round-trip on a path that's already inside a best-effort Promise.allSettled fan-out.

♻️ Proposed fix
-  const { data: playerRows } = await supabase
-    .from('golf_players')
-    .select('id, user_id')
-    .in('id', playerIds);
-  if (!playerRows?.length) return;
-
-  const userIdByPlayer = new Map(playerRows.map((p) => [p.id, p.user_id]));
-
-  const { data: userRows } = await supabase
-    .from('users')
-    .select('id, email')
-    .in('id', playerRows.map((p) => p.user_id));
-  if (!userRows?.length) return;
-
-  const emailByUser = new Map(userRows.map((u) => [u.id, u.email]));
+  const { data: playerRows } = await supabase
+    .from('golf_players')
+    .select('id, user_id, users(id, email)')
+    .in('id', playerIds);
+  if (!playerRows?.length) return;
+
+  const userIdByPlayer = new Map(playerRows.map((p) => [p.id, p.user_id]));
+  const emailByUser = new Map(
+    playerRows
+      .filter((p) => p.users)
+      .map((p) => [p.users!.id, p.users!.email]),
+  );
📝 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
const { data: playerRows } = await supabase
.from('golf_players')
.select('id, user_id')
.in('id', playerIds);
if (!playerRows?.length) return;
const userIdByPlayer = new Map(playerRows.map((p) => [p.id, p.user_id]));
const { data: userRows } = await supabase
.from('users')
.select('id, email')
.in('id', playerRows.map((p) => p.user_id));
if (!userRows?.length) return;
const emailByUser = new Map(userRows.map((u) => [u.id, u.email]));
const { data: playerRows } = await supabase
.from('golf_players')
.select('id, user_id, users(id, email)')
.in('id', playerIds);
if (!playerRows?.length) return;
const userIdByPlayer = new Map(playerRows.map((p) => [p.id, p.user_id]));
const emailByUser = new Map(
playerRows
.filter((p) => p.users)
.map((p) => [p.users!.id, p.users!.email]),
);
🤖 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/coachhelm/v3/qualifying/player-notify.ts` around lines 40 - 54,
Collapse the sequential player and user queries in the notification logic into
one Supabase query by selecting the player fields and embedding the related
user’s id and email through the relationship. Update the result transformation
to build the required player-to-user and user-to-email mappings from the nested
response, preserving the existing early return behavior when no rows or related
users are present.

Comment on lines +37 to +43
ALTER TABLE public.baseball_settings_audit_log
ADD COLUMN IF NOT EXISTS actor_user_id uuid,
ADD COLUMN IF NOT EXISTS actor_coach_id uuid REFERENCES public.baseball_coaches(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS event_type text,
ADD COLUMN IF NOT EXISTS summary text,
ADD COLUMN IF NOT EXISTS before_value jsonb,
ADD COLUMN IF NOT EXISTS after_value jsonb;

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 | 🟠 Major | ⚡ Quick win

Missing index on new FK column actor_coach_id.

actor_coach_id is added as a foreign key to baseball_coaches(id) but no covering index is created (only (team_id, event_type) gets one). Per migration path instructions, FK columns should be indexed to avoid slow joins and ON DELETE SET NULL scans on baseball_coaches deletes.

🔧 Proposed fix
 CREATE INDEX IF NOT EXISTS baseball_settings_audit_log_event_type_idx
   ON public.baseball_settings_audit_log (team_id, event_type);
+
+CREATE INDEX IF NOT EXISTS baseball_settings_audit_log_actor_coach_id_idx
+  ON public.baseball_settings_audit_log (actor_coach_id);

As per path instructions, "Add indexes on FK columns and on every column used in RLS predicates."

📝 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
ALTER TABLE public.baseball_settings_audit_log
ADD COLUMN IF NOT EXISTS actor_user_id uuid,
ADD COLUMN IF NOT EXISTS actor_coach_id uuid REFERENCES public.baseball_coaches(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS event_type text,
ADD COLUMN IF NOT EXISTS summary text,
ADD COLUMN IF NOT EXISTS before_value jsonb,
ADD COLUMN IF NOT EXISTS after_value jsonb;
ALTER TABLE public.baseball_settings_audit_log
ADD COLUMN IF NOT EXISTS actor_user_id uuid,
ADD COLUMN IF NOT EXISTS actor_coach_id uuid REFERENCES public.baseball_coaches(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS event_type text,
ADD COLUMN IF NOT EXISTS summary text,
ADD COLUMN IF NOT EXISTS before_value jsonb,
ADD COLUMN IF NOT EXISTS after_value jsonb;
CREATE INDEX IF NOT EXISTS baseball_settings_audit_log_actor_coach_id_idx
ON public.baseball_settings_audit_log (actor_coach_id);
🤖 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/20260710020000_baseball_settings_audit_log_column_reconcile.sql`
around lines 37 - 43, Add a dedicated index for the foreign-key column
actor_coach_id after the ALTER TABLE statement, using a clear name consistent
with the existing indexes. Keep the foreign key definition unchanged and ensure
the index is created idempotently with CREATE INDEX IF NOT EXISTS.

Source: Path instructions

…ve task-reminder re-arm, annotate the guarded scorecard swap

- push.ts: same functions.invoke-through-admin-client change as #798
  (3-way merged under this branch's preference-routing rewrite) so the
  raw service-role header is gone on both branches and they merge clean.
- messages.ts: same nosemgrep annotation as #798 (realtime-subscribed UI,
  intentional no-revalidate).
- tasks.ts setTaskReminder: update-or-insert re-arm of the unsent queue
  row instead of delete-then-insert — a transient failure can no longer
  leave a task with no armed reminder.
- golf.ts fallback swap: nosemgrep annotations — the flagged deletes are
  the snapshot-guarded swap (null-checked snapshot captured before any
  delete; every failure path restores) and its rollback helper itself.

semgrep exit 0 (unpiped), tsc, eslint, 1,408 targeted tests green.

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

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/app/golf/actions/tasks.ts (1)

458-491: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add a uniqueness guard for armed task reminders src/app/golf/actions/tasks.ts:467-491 still does a read-then-insert/update on golf_task_reminders, and the schema only has non-unique indexes (supabase/migrations/20260527000000_prod_public_baseline.sql:14245-14253) — no partial unique constraint on (task_id) WHERE sent = false. Two concurrent setTaskReminder calls can create duplicate unsent rows, and src/app/golf/actions/task-reminders.ts:251-258 will pick both up for dispatch. Add a partial unique index, or switch to an atomic upsert path keyed to the unsent reminder.

🤖 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/golf/actions/tasks.ts` around lines 458 - 491, Add a database-level
uniqueness guard for unsent reminders, preferably a partial unique index on
golf_task_reminders(task_id) WHERE sent = false, and update setTaskReminder to
use an atomic upsert targeting that constraint instead of the
read-then-insert/update flow around existingQueueRow. Ensure concurrent calls
leave exactly one armed reminder and preserve the existing error handling via
queueError.

Source: Linters/SAST tools

🤖 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.

Outside diff comments:
In `@src/app/golf/actions/tasks.ts`:
- Around line 458-491: Add a database-level uniqueness guard for unsent
reminders, preferably a partial unique index on golf_task_reminders(task_id)
WHERE sent = false, and update setTaskReminder to use an atomic upsert targeting
that constraint instead of the read-then-insert/update flow around
existingQueueRow. Ensure concurrent calls leave exactly one armed reminder and
preserve the existing error handling via queueError.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 40644373-24fd-4dd8-813d-f659d13a3145

📥 Commits

Reviewing files that changed from the base of the PR and between 1234fc7 and 99ee496.

📒 Files selected for processing (5)
  • src/app/actions/messages.ts
  • src/app/golf/actions/golf.ts
  • src/app/golf/actions/tasks.ts
  • src/lib/notifications/push.ts
  • src/test/lib/notifications/push.test.ts

Fable Integrator and others added 2 commits July 10, 2026 15:46
…thout-rls anchor pattern

Both files only ALTER an existing table; the rule matched the literal
'CREATE TABLE IF NOT EXISTS public.<name>' quoted in their history notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX
…+ roster wall (mobile split × row menu)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX
@njrini99-code njrini99-code merged commit 3b1c5fa into main Jul 10, 2026
34 of 36 checks passed
@njrini99-code njrini99-code deleted the fix/feature-flows branch July 10, 2026 19:59
@sentry

sentry Bot commented Jul 11, 2026

Copy link
Copy Markdown

Issues attributed to commits in this pull request

This pull request was merged and Sentry observed the following issues:

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants