Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
211fee2
docs(baseball): coherence ruling for overnight consolidation — one sh…
Jul 8, 2026
4ed9617
refactor(baseball): render BaseballFairwayShell unconditionally, dele…
Jul 8, 2026
cf0d024
refactor(baseball): delete legacy BaseballShellLayout/dashboard-shell…
Jul 8, 2026
660f51f
refactor(baseball): remove remaining isRedesignEnabled forks in calen…
Jul 8, 2026
44b036d
refactor(baseball): navigation IA restructure — cap every hub at ≤3 s…
Jul 8, 2026
01adf35
feat(baseball): add Operations + Scouting hub-landing pages (Ruling 2)
Jul 8, 2026
31ea5e8
refactor(baseball): remove duplicate <Header> mount from Settings pag…
Jul 8, 2026
672f970
test(baseball): lock the ≤3-subtabs-per-hub anti-regression contract …
Jul 8, 2026
255ea47
fix(baseball): team-local 'today' for readiness + command center; rea…
Jul 8, 2026
356e44c
fix(baseball): stats honesty — tri-state eligibility, drop phantom EX…
Jul 8, 2026
7df21b6
fix(baseball): breadcrumbs never render raw UUIDs; detail pages suppl…
Jul 8, 2026
6ba82a4
fix(baseball): calendar/event integrity + E2E self-cleanup
Jul 8, 2026
bb386d5
refactor(baseball): one Lift Lab — baseball routes now use the canoni…
Jul 8, 2026
0e3a0b5
chore(db): baseball-coach lifting-edit bridge + player-stats drift co…
Jul 8, 2026
ecb435b
chore(baseball): delete dead code + duplicate surfaces; fix dev-plan …
Jul 8, 2026
d357fc6
style(baseball): Living Annual polish — duplicate headers gone, no am…
Jul 8, 2026
0eb85ca
fix(baseball): team-local week bounds, snapshot stat fallbacks, cross…
Jul 8, 2026
cd68952
test(baseball): align contracts with tonight's consolidation + regen …
Jul 8, 2026
063382f
fix(e2e): move service-role teardown client into scripts/ helper (ast…
Jul 8, 2026
baaf9c9
fix(security): gate ungated SECURITY DEFINER RPCs (PII leak + privile…
Jul 8, 2026
f25f9b7
fix(baseball): repair CSV stat-upload — drop phantom upload_batch_id …
Jul 8, 2026
15ceeb6
docs(audit): 2026-07-08 production DB forensic audit + remediation re…
Jul 8, 2026
49f81c7
Merge remote-tracking branch 'origin/fix/baseballhelm-coherence-2026-…
Jul 8, 2026
3b3d59c
Merge remote-tracking branch 'origin/fix/db-security-hardening-2026-0…
Jul 8, 2026
da3d420
fix(baseball): statsync verification remediation — box-score↔profile …
Jul 9, 2026
7b899b6
fix(ci): heal migration-chain drift + defuse date time-bomb in visibi…
Jul 9, 2026
1297b57
fix(review): address CodeRabbit substantive findings on #794 (53 fixe…
Jul 9, 2026
6af6433
fix(review): CodeRabbit round 2 — 21 fixes (UTC windows, error surfac…
Jul 9, 2026
761cacb
fix(review): CodeRabbit round 3 — 11 fixes (honest errors, open-statu…
Jul 9, 2026
e8d213b
docs(matrix): remove 22 closed owner-issue links per maintenance rule
Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions docs/audits/DB_FORENSIC_AUDIT_2026-07-08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Production Database Forensic Audit — 2026-07-08

Shared GolfHelm + BaseballHelm Supabase project. 5 read-only auditors + Supabase
advisors. Overall grade: **C+** — structurally messy, operationally small, **zero
data corruption**. Security/hygiene debt, not integrity failure.

## Dimension grades

| Dimension | Grade | Verdict |
|---|---|---|
| RLS coverage | B− | 100% of 260 tables have RLS. One cross-tenant write hole (course library). |
| SECURITY DEFINER fns | C→**fixed** | Anon clean; 2 fns leaked all user emails to any authed user. **Gated 2026-07-08.** |
| Data quality | C | Zero orphans/corruption; E2E still writes into prod (`organizations` 50% junk). |
| Schema truth | C+ | 8/8 recent migrations verified applied; 27 prod migrations have no repo file; drift bugs. |
| Performance | B+ | 591 MB, 9/60 conns, cached. 694 perf advisories ~all cosmetic at this scale. |

## Fixed in this PR (applied to prod + repo migrations)

- **[P0] PII leak** — `get_users_with_auth()` / `get_platform_health_stats()` were
SECURITY DEFINER + authenticated-EXECUTE with no gate; any logged-in user could
dump every user's email + auth metadata. Now `is_admin()/is_super_admin()`-gated
(migration `20260708020000`).
- **[P1] Ungated RPC cluster** — `update_user_last_seen` (could overwrite any user's
timestamp) now self-or-admin; dead RPCs (`get_pending_task_reminders`,
`mark_task_reminder_sent`), CRM analytics (`get_crm_click_destinations`,
`get_crm_template_performance`), and `refresh_crm_coach_engagement` (cron uses
service_role) had authenticated EXECUTE revoked (migration `20260708021000`).
- **[P0] CSV stat upload broken** — `uploadStatsCSV` wrote `upload_batch_id` (a
column that never existed) on every insert → every upload failed. Removed the
legacy write; added the 7 real stat columns the type declared but the table
lacked (`caught_stealing`, `sacrifice_bunts`, `runs_allowed`, `pitches_thrown`,
`strikes_thrown`, `launch_angle`, `spin_rate`) additively so the DB matches the
type; type now names the 6 real `source_*`/`import_run_id` columns it was
missing (migration `20260708022000`).

## Deferred — needs your decision (NOT changed)

- **[P1] Course-library cross-tenant writes.** `golf_course_tee_holes` (ALL,
`USING true`), `golf_course_tees` (UPDATE, `USING true`), `golf_courses` (UPDATE,
`auth.uid() IS NOT NULL`) let any authenticated user edit/delete any school's
course data. **This may be intentional** — the library is a soft-delete,
crowd-sourced "grows-from-saves" wiki. I did not tighten it because it's a live
golf product and the open-edit model is plausibly by design. **Decision needed:**
is cross-school course editing intended (wiki model → add a server-side audit
trigger so edits are always logged) or not (→ scope writes to owner/admin)?
- Security-definer `*_public` views expose a platform-wide coach directory to any
authed user (no PII); fine at 10 teams, add `is_public` opt-in before scaling.
- 27 orphan migrations (recorded in prod, no repo file); `schema_migrations`
version/filename mismatches + 6 double-recorded — a future replay-from-scratch
hazard, not a live bug.
- `admin_events` + `error_logs` = 80% of DB size, ~1,550 rows/day, no retention.
- E2E suite writes into the shared prod DB (an "E2E Test University" org was created
6 days before this audit). See the separate-E2E-project recommendation.

## Healthy (verified, don't worry)

RLS on every table. Zero FK orphans, zero childless conversations, zero
impossible/negative stats, zero future-dated rows. 160-game E2E purge held.
Graveyard tables dormant. The scary advisor counts (199 unindexed FKs, 259 unused
indexes) waste 9.6 MB total and sit on tiny tables — cosmetic at this scale.
88 changes: 88 additions & 0 deletions docs/baseball/COHERENCE_RULING_2026-07-08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# BaseballHelm Coherence Ruling — 2026-07-08 (overnight consolidation)

**Authority:** Commander decision doc for the one-night coherence mission. Supersedes conflicting
guidance in CANONICAL_SPEC §2.1 (stale 10-item nav). Builds on COACH_NAV_8TAB_PROPOSAL.md
(owner-approved 2026-07-01) and the 2026-06-30 shell postmortem's unexecuted recommendations.
Baseline: `origin/main` e63de6044 — tsc/lint/ratchet/unit all green.

## Ruling 1 — ONE shell: BaseballFairwayShell, unconditional

`NEXT_PUBLIC_REDESIGN=true` is what prod serves. The legacy fork is pure regression risk
(`.env.example` defaults it off). Therefore:

- `(dashboard)/layout.tsx` and `(player-dashboard)/player/layout.tsx` render
`BaseballFairwayShell` **unconditionally** — delete the `isRedesignEnabled()` forks
(baseball layouts only; golf untouched).
- Delete `src/app/baseball/(coach-dashboard)/` entirely (zero page.tsx, confirmed dead).
- Remove the **baseball** nav paths from `src/components/layout/sidebar.tsx`
(5 legacy arrays: collegeTeamNav/hsCoachTeamNav/jucoTeamNav/showcaseOrgNav/playerTeamNav +
`buildCondensedBaseballNavigation`); the golf branch stays byte-identical.
- Delete `BaseballShellLayout`/`BaseballDashboardShell` baseball render path once unreferenced.
- Route groups `(dashboard)` + `(player-dashboard)` both stay (URLs are load-bearing:
PWA start_url, bookmarks). Folding them is deferred — not tonight's risk.

## Ruling 2 — Navigation IA: ≤8 primary, ≤3 subtabs per primary (hard caps)

Top level keeps the owner-approved hub set (College sees 7, HS 6, JUCO 8 — all ≤8):
**Dashboard · Team · Messages · Stats & Performance · Development · Recruiting ·
Academics (JUCO) · Management**. The fix is inside the hubs — every destination stays
reachable ≤2 clicks (subtab landing pages surface deeper routes as cards/CTAs; command
palette stays flat with everything). Deep routes keep their URLs; `resolve-active-hub`
maps them to the owning subtab for highlight + breadcrumbs.

| Hub | Subtabs (≤3) | Folded in (reachable from landing) |
|---|---|---|
| Dashboard | Overview · Signals | — |
| Team | Roster · Calendar · Operations | Operations = new landing: Documents, Travel, Practice Planner, Practice Effectiveness |
| Messages | Messages · Announcements | announcements moves here from Team (it's comms) |
| Stats & Performance | Stats Center · Games · Postgame | Season = view inside Stats Center; Upload + Import Center = CTAs inside Stats Center |
| Development | Dev Plans · Training · Videos | Training = existing /dashboard/performance landing → Programs, Live Weight Room, Builder, Groups |
| Recruiting | Pipeline · Discover · Scouting | Scouting = new landing: Watchlist, Compare, Saved Comparisons, Scout Packets, Camps |
| Academics | (single page) | JUCO only |
| Management | Decision Room · Settings · Organization | Settings = existing card-grid landing (KEEP grid, DELETE the 9-tab splice from COACH_MANAGEMENT_TABS); Organization = org/teams/events (Showcase types) |

Player nav (Fairway): Today · Schedule · My Stats · Development · Team · Messages ·
My Profile (+ Recruiting when activated) — already ≤8; enforce ≤3 subtabs per hub the same way.
Players hitting `/baseball/dashboard/practice` redirect to `/baseball/player/practice` (canonical
player practice surface).

Nav-manifest test extended: every coach/both registry entry maps to exactly one hub, and no
hub resolves >3 subtabs for any coach type. That test is the anti-regression lock.

## Ruling 3 — ONE Lift Lab

Canonical = `src/components/lifting/*` + `helm_lifting_*`. Repoint the 6 baseball
performance routes at the canonical components; delete `src/components/baseball/performance/*`
(23-file legacy tree, GolfHelm-palette, writes legacy `baseball_lift_*`). Completes the
in-flight unification train.

## Ruling 4 — Data honesty & correctness cluster

- "Today" is **team-local** everywhere (`resolveTeamTimezone` + `todayIsoInTz`), never server-UTC:
readiness page, command-center read-model, player-today read-model.
- Calendar: null `end_time` renders start + 1h default (never zero-duration); events query gets
lookback bound + limit; badge labels pluralize.
- Academics eligibility is tri-state: `null` = gray "Not on file"; red "Ineligible" only for real `false`.
- Roster: drop EXIT V column (column doesn't exist in schema, no write path — honest UI);
backfill career_obp/slg/ops for existing rows (prod data op).
- Breadcrumbs: UUID-shaped segments never title-cased; dynamic routes supply real names
(players/[id], stats/games/[id], dev-plans/[id]).
- `createBaseballEvent` game-insert errors checked, not swallowed.
- E2E: spec cleans up its own rows (service-role delete in teardown); prod junk rows
(`E2E Created Opponent%`) deleted as a data op; isolated E2E project documented as follow-up
needing owner (new Supabase project).
- Seed script gets realistic event times (practice 15:30–17:30, games 13:00–16:00, meetings 12:00–13:00 team-local);
demo team's polluted event rows corrected in place.

## Ruling 5 — Dead code deleted, not layered over

`(coach-dashboard)`; legacy sidebar baseball arrays; legacy shell baseball path; legacy Lift Lab
tree; `players/[id]/profile` duplicate page (canonical = `players/[id]` PlayerProfileClient);
knip-confirmed orphans (MatchScoreBadge, match-calculator, dashboard-types); 5 orphaned
`baseballhelm-*.{mjs,workflow.js}` scripts (superseded by Helm Bridge).

## Out of scope tonight (documented, not forgotten)

Route-group merge of `(player-dashboard)` into `(dashboard)`; dedicated E2E Supabase project;
PlayerPassportCard→Fairway preview swap if polish wave runs out of clock; full 3-lane
Living-Annual masthead vision (ui-migration-map L56) — the 8-hub IA is the stepping stone.
Loading
Loading