Skip to content

fix(golf): per-course scorecards + library-owner gating + Holes summary - #935

Closed
njrini99-code wants to merge 2 commits into
mainfrom
feat/course-library-real-data
Closed

fix(golf): per-course scorecards + library-owner gating + Holes summary#935
njrini99-code wants to merge 2 commits into
mainfrom
feat/course-library-real-data

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Fixes #913.

Problem

Three related issues in the Cloud Course Library:

  1. All 24 library courses shared one cloned scorecard. Confirmed live against the current DB (mcp__supabase__execute_sql): 14 courses with source IS NULL + 6 with source='seed' (20 total, all with created_by_user_id IS NULL — no human creator) point at an identical 3-tee template — Championship 7,165 yds / 73.6 rating / 140 slope, Members 6,455 / 71.2 / 132, Forward 5,450 / 69.0 / 120 — down to the same hole-by-hole pars, yardages, and stroke indexes for every course, regardless of the real facility (Bethpage Black par 71 was showing par 72; Harbour Town par 71 was showing par 72; etc.). The other 4 active courses in the table (Danville Golf Club, Forsyth Country Club, Pebble Beach Golf Club, Statesville Country Club) already have distinct, real, human-entered data (created_by_user_id set) — those are genuine team/coach contributions, not shared library rows, and are intentionally left untouched.
  2. Any team coach could Remove or Edit a shared library course. canManageTeam (= userRole === 'coach') gated "Remove course"; "Edit course" wasn't gated by role at all. Neither checked ownership — the same open-contribution model meant to let a coach fix a typo on a course their team added also let any coach rename or delete "Pinehurst No. 2" for every team on the platform. RLS on golf_courses UPDATE (golf_courses_update_authenticated, from 20260614010000_course_library_update_policy.sql) had the identical gap: USING (auth.uid() IS NOT NULL) — any authenticated user, no ownership check.
  3. No way to see hole-by-hole data without entering tee edit. "Edit course" only exposes facility-level fields (name/city/state/address/website); hole pars/yardages only render inside the per-tee "Edit tee" form.

Fix

1. DATA — scripts/seed-course-library-scorecards.ts (REVIEWED-ONLY, not executed)

  • src/lib/golf/course-scorecard-generator.ts — pure, unit-tested, deterministic per-course generator. Given a course's top-line facts (par, championship yardage, rating, slope), it produces a full 18-hole routing: which holes are par 3/4/5 (real-world-typical composition per par value), individual yardages (rescaled to SUM EXACTLY to the target total, no rounding drift), and a stroke-index permutation (1=hardest..18=easiest, with a "no par-3 gets index 1 or 2" realism nudge). Seeded on the course name (mulberry32 PRNG over a string hash) so re-running is byte-identical (idempotent) and no two courses ever get the same layout, even at the same par/yardage.
  • scripts/seed-course-library-scorecards.ts — the 20-course fact table + DB-apply logic. --dry-run (default) prints the full diff and writes nothing; --apply performs writes; --course="Name" scopes to one course for review. UPDATE-only (never inserts a course or tee row — the existing Championship/Members/Forward tee ids are reused, so golf_rounds.tee_id and already-recorded rounds are untouched), matches by golf_normalize_name(name) (never a hardcoded id), and re-checks created_by_user_id IS NULL before touching a row (skips with a warning otherwise — never overwrites a real user's course). Hole rows use upsert-first/prune-surplus-second (the same stage-and-swap pattern as updateTeeImpl), never a wipe-then-reinsert.

Per-course source/confidence notes (also in the script's sourceNote field):

Course Location Par Championship yds Rating/Slope Source confidence
Bethpage Black Farmingdale, NY 71 7,468 76.6/148 High — multi US Open/PGA Championship host, widely published back-tee card
Harbour Town Golf Links Hilton Head, SC 71 7,099 74.9/137 High on par/length (RBC Heritage host, notably short for a Tour venue); rating/slope typical-range estimate
Isleworth Golf & Country Club Windermere, FL 72 7,207 75.6/143 Par high confidence (Tiger Woods' home course, private); yardage/rating/slope realistic estimate, no public tee sheet
Kiawah Island - Ocean Course Kiawah Island, SC 72 7,356 76.9/155 High — 2012/2021 PGA Championship host, published back-tee figures, one of the highest US slope ratings
Marsh Landing Country Club Ponte Vedra Beach, FL 71 6,900 73.5/132 Moderate — private club, no confirmed public tee sheet, realistic estimate
Pebble Beach Golf Links Pebble Beach, CA 72 6,828 74.9/143 High — iconic, widely published standard back-tee card
Pinehurst No. 2 Pinehurst, NC 72 7,209 75.4/137 High — Donald Ross design, multi US Open host, commonly cited resort back-tee card
Reynolds Lake Oconee - Great Waters Greensboro, GA 72 7,048 74.5/141 Moderate-high — real Jack Nicklaus design; rating/slope realistic-range estimate
Sawgrass Country Club Ponte Vedra Beach, FL 72 7,100 74.3/133 Moderate — real, historic pre-TPC Tour stop; yardage/rating/slope estimate
Sea Island - Seaside Course St. Simons Island, GA 70 7,005 73.4/137 High on par (RSM Classic host, Rees Jones/Fazio redesign); yardage/rating/slope estimate
TPC Sawgrass Ponte Vedra Beach, FL 70 6,864 72.6/128 Moderate — treated as the Dye's Valley Course (the property's non-Stadium 18; Stadium is the separate entry below)
TPC Sawgrass - Stadium Ponte Vedra Beach, FL 72 7,275 74.1/144 High on par/yardage (THE PLAYERS host, widely published); rating/slope member-card estimate
Whistling Straits Kohler, WI 72 7,142 76.7/152 High — 2004/2010/2015 PGA Championship + 2021 Ryder Cup host; standard back-tee card
World Golf Village - King & Bear St. Augustine, FL 72 7,279 75.4/140 Moderate-high — real Palmer/Nicklaus collaborative design
Golden Horseshoe gold course Williamsburg, VA 71 6,817 73.2/144 High on par (Colonial Williamsburg, Robert Trent Jones Sr.); yardage/rating/slope estimate
Club at Savannah Habor (sic, matches existing DB typo) Savannah, GA 72 7,288 75.2/130 Moderate-high — real (Westin Savannah Harbor Golf Resort, Robert Cupp design, LPGA host); estimate
Forest Creek Pinehurst, NC 72 7,154 74.9/143 High on par — real Tom Fazio design, highly rated Pinehurst-area club; estimate
Pine Lakes Jekyll Island Jekyll Island, GA 72 6,657 71.8/124 Moderate — Pine Lakes is one of Jekyll Island Golf Club's 4 courses; resort-style estimate
Poplar Grove (real) Amherst, VA 71 6,450 71.0/125 Low — no confirmed public scorecard found for this exact name+location; realistic small-market VA estimate, flagged for a human to verify/replace
The Cardinal Greensboro, NC 72 7,213 76.0/147 Moderate-high — real Pete Dye design

Left untouched (already real, human-owned data, not library rows): Danville Golf Club, Forsyth Country Club, Pebble Beach Golf Club (distinct from "Pebble Beach Golf Links" above), Statesville Country Club.

2. SAFETY — ownership gating

  • updateCourse / softDeleteCourse (src/app/golf/actions/course-library.ts) now check created_by_user_id: NULL (library row, no human creator — the same signal createCourse/contributeCourseFromRound always stamp) requires SUPER_ADMIN_USER_IDS allowlist membership (same pattern already used in src/app/golf/actions/auth.ts); a team/user-owned course keeps the existing open-contribution model unchanged.
  • CourseDetailDrawer hides "Edit course" and "Remove course" for non-admins on a library-owned course, and shows a "Library course" badge so it's clear why (not just a vanished button). isSuperAdmin is resolved server-side in courses/page.tsx (env-allowlist check, no DB round trip) and threaded through CourseLibraryClient.
  • supabase/migrations/20260717120000_course_library_owner_gate.sqlcommitted for review, NOT applied by this PR/agent. RLS previously allowed any authenticated user to UPDATE any golf_courses row (the app-layer guard above doesn't help if something talks to PostgREST directly). The migration tightens golf_courses_update_authenticated to created_by_user_id IS NOT NULL OR public.is_super_admin(), using the existing is_super_admin() SECURITY DEFINER function (20260701110000_admin_allowlist_is_super_admin.sql). supabase/tests/rls/golf_course_library.sql gets 2 new pgTAP assertions locking in the policy shape (plan 37 → 39). A human should review + apply this migration via the normal deploy process.

Caveat / explicitly out of scope: tee-level "Delete tee"/"Edit tee" (TeeRow in CourseDetailDrawer, updateTee/softDeleteTee) have the identical gap — a coach can still delete a library course's "Championship" tee set even though they can no longer delete the course itself. The ticket's reported bug was specifically "Remove course (and open edit)"; extending the same ownership check to tees is a natural, small follow-up but is left out here to keep this diff scoped to what was reported.

3. DISCOVERABILITY — read-only "Holes" section

  • New getCourseTeeHoles(courseId) read action returns hole rows for every active tee, grouped by tee id (one extra query, kept separate from getCourseDetail so the new-round/tee-picker hot paths — which don't render hole data — never pay for it).
  • CourseDetailDrawer renders a "Holes" section below "Tee sets": one compact table per tee (Hole # / Par / Yds rows), horizontally scrollable, purely presentational (no edit affordance — hole editing stays exclusively in "Edit tee", this just makes the data visible without entering it).

Gates

  • npx tsc --noEmit -p tsconfig.json — clean (0 errors)
  • npx eslint <changed files> — clean (0 errors/warnings)
  • npx vitest run (course-library actions + lib + new generator + course-picker suites) — 484 passed, 4 pre-existing skips, 0 failures, 0 new flake
  • npx tsx scripts/seed-course-library-scorecards.ts (dry-run only, no DB writes) — verified output totals match the table above exactly (e.g. Bethpage Black Championship totals exactly 7,468 yds)
  • Did not run supabase db lint / pgTAP locally (no Docker in this environment) — CI's supabase job runs both against an ephemeral local stack on this PR.

Not done / needs a human

  • Migration 20260717120000_course_library_owner_gate.sql is not applied to any environment — review then apply via the normal deploy process.
  • scripts/seed-course-library-scorecards.ts has not been run with --apply anywhere — review the fact table (especially the "Low confidence" Poplar Grove entry) then decide when/whether to run it.
  • Tee-level ownership gating (see caveat above) is a follow-up, not included here.

🤖 Generated with Claude Code

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

Fixes #913.

Problem: the 20 cloud-library golf courses with no human creator
(created_by_user_id IS NULL — bulk-seeded rows) all shared ONE cloned
3-tee scorecard (identical Championship/Members/Forward totals AND
hole-by-hole pars/yardages/stroke indexes, confirmed live via SQL
against the current DB). Any team coach could also Remove or open Edit
on those same shared rows, and the course detail sheet had no way to
see hole-by-hole data without entering tee-set edit.

Fix, three parts:

1. DATA — scripts/seed-course-library-scorecards.ts (REVIEWED-ONLY, not
   executed) + src/lib/golf/course-scorecard-generator.ts (pure,
   unit-tested). Hand-set top-line facts (par, championship yardage,
   rating, slope) per course from well-known public scorecard facts
   where documented, realistic estimates elsewhere (source/confidence
   noted per course in the script + PR body); a seeded-per-course
   generator produces a genuinely distinct 18-hole routing + stroke
   index for every course, yardages always summing exactly to the
   chosen total. UPDATE-only, ownership-guarded, idempotent.

2. SAFETY — updateCourse/softDeleteCourse (course-library.ts) now
   require public admin allowlist membership to edit/remove a
   library-owned row (created_by_user_id IS NULL); team/user-owned
   courses keep the existing open-contribution model. UI in
   CourseDetailDrawer hides Edit/Remove for non-admins on library rows
   and adds a "Library course" badge. RLS previously allowed ANY
   authenticated user to UPDATE any golf_courses row underneath the
   app guard — supabase/migrations/20260717120000_course_library_owner_gate.sql
   closes that gap (committed for review, NOT applied by this PR) and
   supabase/tests/rls/golf_course_library.sql locks in the new policy
   shape.

3. DISCOVERABILITY — new getCourseTeeHoles read action + a read-only
   "Holes" section (par row + yardage row per tee) in the course detail
   sheet, so a coach can see the scorecard without opening tee edit.

Gates: npx tsc --noEmit -p tsconfig.json (clean); npx eslint (clean);
npx vitest run (484 passed, 4 pre-existing skips, 0 new failures).

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

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown

PR title or description contains an excluded keyword.

@cursor

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

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

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

@qodo-code-review

Copy link
Copy Markdown

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

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@njrini99-code, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7acd91cf-f46c-4218-9a39-80f77e5a3919

📥 Commits

Reviewing files that changed from the base of the PR and between 6d25e44 and 075fc47.

📒 Files selected for processing (12)
  • scripts/seed-course-library-scorecards.ts
  • src/app/golf/(dashboard)/dashboard/courses/page.tsx
  • src/app/golf/actions/__tests__/course-library.test.ts
  • src/app/golf/actions/course-library.ts
  • src/components/golf/courses/CourseDetailDrawer.tsx
  • src/components/golf/courses/CourseLibraryClient.tsx
  • src/lib/admin/__tests__/coverage-contract.foundation.test.ts
  • src/lib/admin/__tests__/feature-registry.test.ts
  • src/lib/golf/__tests__/course-scorecard-generator.test.ts
  • src/lib/golf/course-scorecard-generator.ts
  • supabase/migrations/20260717120000_course_library_owner_gate.sql
  • supabase/tests/rls/golf_course_library.sql
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/course-library-real-data
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@vercel

vercel Bot commented Jul 17, 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 17, 2026 11:15pm

Request Review

@supabase

supabase Bot commented Jul 17, 2026

Copy link
Copy Markdown

Updates to Preview Branch (feat/course-library-real-data) ↗︎

Deployments Status Updated
Database Fri, 17 Jul 2026 23:16:21 UTC
Services Fri, 17 Jul 2026 23:16:21 UTC
APIs Fri, 17 Jul 2026 23:16:21 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, 17 Jul 2026 23:16:22 UTC
Migrations Fri, 17 Jul 2026 23:16:22 UTC
Seeding Fri, 17 Jul 2026 23:16:22 UTC
Edge Functions ⚠️ Fri, 17 Jul 2026 23:16:22 UTC

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


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

…bump tripwires

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

Copy link
Copy Markdown
Owner Author

🤖 Mission Control — PR summary

What it changes: Per-course scorecards + library-owner gating (#913). The 20 cloud-library courses with created_by_user_id IS NULL all shared one cloned 3-tee scorecard and were editable/removable by any team coach. Adds: a seeded, unit-tested src/lib/golf/course-scorecard-generator.ts producing a distinct 18-hole scorecard per course; a REVIEWED-ONLY seed script (scripts/seed-course-library-scorecards.ts, not executed); a DB migration (supabase/migrations/20260717120000_course_library_owner_gate.sql) + RLS test (supabase/tests/rls/golf_course_library.sql) gating edit/remove to the owner; and a Holes summary in CourseDetailDrawer.

Area: GolfHelm (⚠️ includes a schema / RLS migration).

Risk / reviewers watch:

  • DB migration + RLS change on a Golf-shared production DB — should go through db-migration-reviewer before apply.
  • Owner-gating policy: library (null-owner) rows must not be editable/removable by non-owners, while normal team courses stay writable.
  • Per-course generator determinism; seed script must stay reviewed-only until Nick's go.

CI: 4 required gates green (incl. the Supabase RLS-test job). CodeRabbit advisory + REVIEW_REQUIREDBlocked. No action taken.

@njrini99-code

Copy link
Copy Markdown
Owner Author

Superseded — landed on main inside merge train #938 (commit 6ecede6). Branch kept.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QA] Cloud course library: all 24 courses share ONE template of tee/hole data

1 participant