Skip to content

Consolidated admin-dashboard fix train (supersedes #736–#739, includes #734) - #740

Merged
njrini99-code merged 16 commits into
mainfrom
fix/admin-consolidated-20260703
Jul 3, 2026
Merged

Consolidated admin-dashboard fix train (supersedes #736–#739, includes #734)#740
njrini99-code merged 16 commits into
mainfrom
fix/admin-consolidated-20260703

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

One-train consolidation of the 2026-07-03 admin-dashboard fixes

Prod admin dashboard (/admin, /admin/golf) has been erroring since the Bridge v2 + DB-hardening deploys. This PR consolidates every open fix onto one branch so main gets exactly one Vercel deploy (supersedes #736, #737, #738, #739; includes #734 via the train2 merge).

What was actually broken (root-caused)

  1. get_admin_rounds_rollup 42501 Forbidden on /admin/golf — every get_admin_*_rollup RPC gates on users.role='admin'; baseball onboarding's service-role users upsert clobbered the super-admin's row down to coach. The role was restored live at 2026-07-03 04:18Z; this train makes the demotion impossible to reintroduce (ensureUserRowPreservingAdmin, from P0: stop baseball onboarding demoting admins + honest Bridge panel fallback #736).
  2. /golf/dashboard RSC crash (digest 3173807121, "Functions cannot be passed to Client Components") — icon component types crossed the RSC boundary via EmptyState (from P1: fix /golf/dashboard RSC function-serialization crash (digest 3173807121) #737).
  3. PR Telemetry: prod-only incident feed, Next control-flow filter, demo-sessions force-dynamic, retention mirror #739's branch was build-brokenshouldPersistAdminTables was a sync export from a 'use server' file. Completed here: moved to src/lib/telemetry-gate.ts, all 4 importers repointed, writer tests use the ADMIN_EVENTS_FORCE_CAPTURE=1 hatch, new gate unit tests.

Contents

Verification

  • tsc --noEmit: 0 errors
  • lint-ratchet: green, baseline re-locked
  • Unit suite: 4287 passed, 0 failed (39 skipped)
  • 3-lens adversarial review (build/RSC, DB/auth, telemetry) over the full consolidated diff: see PR comment
  • Prod DB: admin role restored + verified via simulated authenticated rollup calls; migrations in this train were already check-first applied to prod

🤖 Generated with Claude Code

https://claude.ai/code/session_01KsHxAKSPhqgfJKTFfpjRiC

njrini99-code and others added 16 commits July 3, 2026 03:38
Auto-generated by .github/workflows/docs-regen.yml.
Sources: src/lib/types/database.ts, src/app/**/page.tsx,
src/app/**/actions/**/*.ts, src/hooks/**/*.ts.
…ting branches trigger billed preview builds

Old dependabot branches carry a vercel.json from before git.deploymentEnabled
landed, and Vercel reads the config from the branch being built — so every
auto-rebase push fired a preview build (see the failed tailwindcss-4.3.1
build). Rebases now happen only on demand (@dependabot rebase).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
…allback with retry [vercel skip]

P0 from the 2026-07-03 Mission Control sweep. Root cause (H1 confirmed):
every get_admin_*_rollup SECURITY-DEFINER function gates on
users.role='admin', but baseball onboarding's users upsert
({ onConflict: 'id' }, service-role client, no ignoreDuplicates) clobbered
the allowlisted super-admin's row down to role='coach' — so
requireSuperAdmin() passed and every Bridge panel then died with 42501.
The role was restored live (2026-07-03 04:18Z, verified: simulated
authenticated calls to get_admin_dashboard_rollup/get_admin_rounds_rollup
return data); this commit makes the demotion impossible to reintroduce:

- ensureUserRowPreservingAdmin(): both baseball onboarding users-row writes
  now insert-if-missing / allow player<->coach conversion / NEVER overwrite
  an existing 'admin'. (Golf onboarding already used ignoreDuplicates and
  could not demote.)
- PanelBoundary/PanelStale: the error fallback claimed "showing last known
  data" while rendering none. Copy now tells the truth ("temporarily
  unavailable") and ships a real retry (router.refresh() + boundary remount).
- panel-boundary tests: assert the honest copy, the retry path, and that the
  misleading claim never comes back.

H2 (session not reaching the RPC) ruled out: fetchAdminRollupA uses the
request-scoped server client and the RPC succeeds with only a sub claim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
… event_type [vercel skip]

P2 from the 2026-07-03 Mission Control sweep (Postgres logs still firing
400s at 04:03 UTC). Same committed-not-applied class as wave 1
(20260702095900): 20260624000090_baseball_settings_os.sql used CREATE TABLE
IF NOT EXISTS, which silently no-op'd for baseball_program_settings and
baseball_integration_configs (both pre-existed under older schemas), so the
whole intended settings-OS column set never landed.

- baseball_program_settings: full settings-OS parity (36 columns incl.
  brand_accent + appearance_theme — the dominant >=85/4-min 400) + 7 guarded
  CHECKs
- baseball_integration_configs: adapter-contract columns (provider_key,
  display_name, integration_level, status, config, created_by...) + the
  UNIQUE (team_id, provider_key) that upsertIntegration's onConflict needs +
  guarded DROP NOT NULL on legacy integration_key (db-migration-review
  required fix — the insert path never writes it)
- baseball_catching_events: event_type + CHECK (the one code-referenced
  column wave 1 missed)

Reviewed by db-migration-reviewer (golf-safety PASS, fresh-DB idempotent —
guard names match the auto-generated inline-CHECK names, so a fresh DB
no-ops cleanly). APPLIED TO PROD 2026-07-03 via MCP before this commit;
verified: all columns present, integration_key nullable, the previously
400ing brand_accent/appearance_theme read runs clean. All three tables were
0 rows — backfill-free.

The two other runtime-log 42703s (baseball_coaches_2.first_name,
baseball_stat_visual_views.owner_user_id) are already code-side fixed on
main (practice.ts selects full_name; stat-visual-views.ts dropped
owner_user_id) — no schema change needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
…(digest 3173807121) [vercel skip]

P1 from the 2026-07-03 Mission Control sweep. Vercel prod runtime error on
/golf/dashboard + /golf/dashboard/coachhelm: "Functions cannot be passed
directly to Client Components" with payload {$$typeof, render: function,
displayName} — a lucide forwardRef component crossing the server->client
boundary.

Root cause: EmptyState is 'use client' (framer-motion) and takes
icon?: LucideIcon (a component TYPE). Four server components passed icons
that way — most damaging the golf (dashboard) group's not-found.tsx, so
EVERY notFound() under the dashboard (deleted round/player/qualifier ids)
crashed to the generic RSC error instead of the branded 404.

Fix: EmptyState's icon prop now also accepts a rendered ELEMENT
(serializable across the boundary); the chip sizes element icons via
[&>svg] classes so they match the component-type path. The four server
call sites (dashboard not-found, travel, stats/team x2) pass
icon={<Icon strokeWidth={1.75} />}. Client callers passing component types
are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
…ention mirror [vercel skip]

The 2026-07-03 incident export (93 incidents/24h) was mostly noise burying
signal: CI dev servers (/home/runner/...), local dev + next start
(/Users/...), and Vercel preview/prod BUILDS (/vercel/path0/...) all hold
prod Supabase creds and were writing their errors into the prod
admin_events/error_logs feed the Bridge reads.

- shouldPersistAdminTables(): admin-table writes only when
  VERCEL_ENV==='production' and not during phase-production-build;
  ADMIN_EVENTS_FORCE_CAPTURE=1 escape hatch. Applied at every writer:
  captureServerTrace (logServerError/Exception/Event), logAdminEvent
  (admin-logger), /api/log-error, /api/admin/log-event. Off-prod keeps
  console + Sentry visibility.
- isNextControlFlowError(): DYNAMIC_SERVER_USAGE / NEXT_* digests are
  framework signals, not incidents — skip recording (130 phantom "Dynamic
  server usage" events, one burst per preview build).
- /golf/admin/demo-sessions: force-dynamic — admin data is per-request
  (cookies), so prerender attempts stop throwing inside getDemoSessions.
- 20260703043000_admin_events_retention_pg_cron.sql: in-repo mirror of the
  retention job applied live 2026-07-03 (nightly purge >180d; admin_events
  was 91k rows/287MB with no policy). Verified live: cron.job row active.

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

Semgrep (helmv3-server-action-missing-auth-check) flagged the pre-existing
fallback this PR's file touch exposed: when signUp errors already-exists,
the action resumed onboarding for ANY caller who knew the email (service-role
users lookup + getUserById, no caller verification). Now the caller must
either already be signed in as that email (the checkAuth() race case) or
sign in with the submitted password (the abandoned-onboarding retry, which
also sets their session). Otherwise: the generic already-registered error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
…e — sync export from 'use server' file breaks the production build

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KsHxAKSPhqgfJKTFfpjRiC
… unit tests; re-lock lint ratchet (2275→2158)

The prod-only persistence gate correctly blocks admin_events/error_logs
writes in the test environment; the writer column-mapping tests now use
the documented ADMIN_EVENTS_FORCE_CAPTURE=1 hatch so they keep
exercising the real write path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KsHxAKSPhqgfJKTFfpjRiC
… console fallback on gated logAdminEvent

Adversarial review of the consolidated train caught both: regen-docs.mjs's
first-match indexOf locked onto the empty graphql_public schema that the
#733 types-regen added above public, silently rewriting the glossary
inventory to '0 tables, no enums' — and the self-healing docs workflow
would have seen 'in sync' forever. logAdminEvent was the only gated writer
with zero off-prod visibility.

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

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
helmv3 Ready Ready Preview, Comment Jul 3, 2026 5:27am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown

PR title or description contains an excluded keyword.

@njrini99-code

Copy link
Copy Markdown
Owner Author

Adversarial review (13 Sonnet agents, 3 lenses × refuter panels) over the full consolidated diff:

Confirmed & fixed in 486145a:

  • regen-docs.mjs first-match indexOf locked onto the empty graphql_public schema → glossary inventory silently corrupted to '0 tables, no enums'. Anchored search to public schema, regenerated (291 tables / 7 views / 139 functions / 6 enums restored).
  • logAdminEvent was the only prod-gated telemetry writer with zero off-prod console fallback — added.

Refuted (no change): TOCTOU read-then-write on the admin-preserve guard — no code path writes role='admin', promotion is manual-SQL-only; window is theoretical.

@njrini99-code
njrini99-code merged commit 13c62c8 into main Jul 3, 2026
31 of 35 checks passed
@njrini99-code
njrini99-code deleted the fix/admin-consolidated-20260703 branch July 3, 2026 05:21
@coderabbitai

coderabbitai Bot commented Jul 3, 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: ea1cbb36-221f-49df-9776-7dd29b2bd8ee

📥 Commits

Reviewing files that changed from the base of the PR and between 76959f9 and 486145a.

⛔ Files ignored due to path filters (1)
  • src/lib/types/database.ts is excluded by !src/lib/types/database.ts
📒 Files selected for processing (24)
  • .github/dependabot.yml
  • .lint-baseline.json
  • memory/glossary.md
  • scripts/regen-docs.mjs
  • src/app/admin/_components/PanelBoundary.tsx
  • src/app/admin/_components/PanelStates.tsx
  • src/app/admin/_components/__tests__/panel-boundary.test.tsx
  • src/app/api/admin/log-event/route.ts
  • src/app/api/log-error/route.ts
  • src/app/baseball/actions/onboarding.ts
  • src/app/golf/(dashboard)/dashboard/not-found.tsx
  • src/app/golf/(dashboard)/dashboard/stats/team/page.tsx
  • src/app/golf/(dashboard)/dashboard/travel/page.tsx
  • src/app/golf/admin/demo-sessions/page.tsx
  • src/components/fairway/feedback/EmptyState.tsx
  • src/lib/__tests__/admin-logger-bridge.test.ts
  • src/lib/__tests__/server-error-logger-bridge.test.ts
  • src/lib/__tests__/telemetry-gate.test.ts
  • src/lib/admin-logger.ts
  • src/lib/server-error-logger.ts
  • src/lib/telemetry-gate.ts
  • src/test/api/log-error.test.ts
  • supabase/migrations/20260703043000_admin_events_retention_pg_cron.sql
  • supabase/migrations/20260703050000_baseball_651_settings_os_reconcile.sql

Summary by CodeRabbit

  • New Features

    • Added retry support for failed admin panels, with clearer “temporarily unavailable” states and a visible retry action.
    • Improved empty states with more flexible icon rendering and cleaner visuals.
  • Bug Fixes

    • Fixed onboarding flows to better protect existing admin accounts from role changes.
    • Improved error handling so recoverable panel failures can refresh and recover cleanly.
    • Prevented certain non-production environments from storing telemetry and error logs.
  • Chores

    • Updated telemetry retention so older admin event data is removed automatically.

Walkthrough

This PR adds runtime telemetry gating (shouldPersistAdminTables) to skip DB writes off-prod, adds retry/remount UX to admin PanelBoundary, introduces an admin-preserving user-row helper for onboarding flows, extends EmptyState to accept React element icons, fixes a docs-generation schema-offset bug, forces dynamic rendering for a demo-sessions page, disables dependabot auto-rebase, updates lint baseline, and adds a pg_cron retention purge plus baseball settings reconciliation migration.

Changes

Telemetry Persistence Gating

Layer / File(s) Summary
Telemetry gate contract and tests
src/lib/telemetry-gate.ts, src/lib/__tests__/telemetry-gate.test.ts
Adds shouldPersistAdminTables() branching on ADMIN_EVENTS_FORCE_CAPTURE, NEXT_PHASE, and VERCEL_ENV, with tests covering all four cases.
Wire gate into loggers and routes
src/lib/admin-logger.ts, src/lib/server-error-logger.ts, src/app/api/admin/log-event/route.ts, src/app/api/log-error/route.ts
Gates persistence in logAdminEvent/captureServerTrace/both API handlers behind the gate, and adds Next.js control-flow error suppression in server-error-logger.ts.
Update logger/route tests
src/lib/__tests__/admin-logger-bridge.test.ts, src/lib/__tests__/server-error-logger-bridge.test.ts, src/test/api/log-error.test.ts
Stubs ADMIN_EVENTS_FORCE_CAPTURE=1 in setup and unstubs env in teardown to exercise the real write path.
Admin events retention cron migration
supabase/migrations/20260703043000_admin_events_retention_pg_cron.sql
Schedules a daily pg_cron job purging admin_events/admin_analytics_events rows older than 180 days.

Admin Panel Retry UX

Layer / File(s) Summary
PanelStale props/copy update
src/app/admin/_components/PanelStates.tsx
Adds optional action prop and changes stale copy to "temporarily unavailable".
PanelBoundary retry/remount wiring
src/app/admin/_components/PanelBoundary.tsx
Adds PanelRetryButton, reset() method, onRetryReset callback, and attempt-keyed remount.
Panel tests
src/app/admin/_components/__tests__/panel-boundary.test.tsx
Adds tests for retry-triggered refresh/remount and updated stale-copy assertions.

Admin-Preserving Onboarding Role Writes

Layer / File(s) Summary
ensureUserRowPreservingAdmin and flow updates
src/app/baseball/actions/onboarding.ts
Adds a helper that never demotes existing admins, applies it to coach onboarding and baseball signup, and reworks "email already exists" handling to require ownership verification via session or password sign-in.

EmptyState Icon Flexibility

Layer / File(s) Summary
EmptyState React-element icon support
src/components/fairway/feedback/EmptyState.tsx
Adds isValidElement handling so icon can be a rendered element, not just a Lucide component.
Golf pages custom stroke-width icons
src/app/golf/(dashboard)/dashboard/not-found.tsx, .../stats/team/page.tsx, .../travel/page.tsx
Passes icons as elements with strokeWidth={1.75}.

Docs Generation, Dynamic Rendering, Config, and Migrations

Layer / File(s) Summary
regen-docs public schema offset fix
scripts/regen-docs.mjs, memory/glossary.md
Fixes block-start lookups to search within the public schema; glossary reflects new function count and entry.
Force dynamic rendering
src/app/golf/admin/demo-sessions/page.tsx
Adds export const dynamic = 'force-dynamic'.
Dependabot and lint baseline config
.github/dependabot.yml, .lint-baseline.json
Disables auto-rebase for all ecosystems; updates helm/* lint rule counts.
Baseball settings-OS reconciliation migration
supabase/migrations/20260703050000_baseball_651_settings_os_reconcile.sql
Adds idempotent columns/constraints to baseball_program_settings, baseball_integration_configs, and baseball_catching_events.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Route as API Route (log-event/log-error)
  participant Gate as shouldPersistAdminTables
  participant Logger as admin-logger / server-error-logger
  participant DB as admin_events / error_logs

  Route->>Gate: shouldPersistAdminTables()
  alt gate returns false
    Gate-->>Route: false
    Route-->>Route: return {success:true, persisted:false}
  else gate returns true
    Gate-->>Route: true
    Route->>Logger: proceed with validation and logging
    Logger->>Gate: shouldPersistAdminTables()
    Gate-->>Logger: true
    Logger->>DB: insert row
    DB-->>Logger: ack
    Logger-->>Route: success
  end
Loading
sequenceDiagram
  participant User
  participant PanelStale
  participant PanelRetryButton
  participant Router as next/navigation router
  participant PanelErrorBoundary
  participant PanelBoundary

  PanelErrorBoundary->>PanelStale: render error state with action
  User->>PanelRetryButton: click "Try again"
  PanelRetryButton->>Router: router.refresh() (in transition)
  Router-->>PanelRetryButton: refresh complete
  PanelRetryButton->>PanelErrorBoundary: call reset()
  PanelErrorBoundary->>PanelErrorBoundary: clear error state, call onRetryReset()
  PanelErrorBoundary->>PanelBoundary: onRetryReset()
  PanelBoundary->>PanelBoundary: increment attempt, key={attempt}
  PanelBoundary->>PanelErrorBoundary: remount subtree
Loading

Possibly related PRs

  • njrini99-code/helmv3#631: Both PRs modify .lint-baseline.json, updating the same helm/* rule baseline counts (e.g., helm/no-raw-button, helm/no-raw-input).

Suggested labels: security, database, ci

✨ 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/admin-consolidated-20260703
  • 🛠️ 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.0)

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


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

@coderabbitai coderabbitai Bot added ci CI, Vercel, test, build, deploy database Schema, migrations, indexes, SQL security Auth, secrets, RLS, PII, webhooks labels Jul 3, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Consolidated admin-dashboard fix train: role guard, RSC fix, telemetry gate, schema drift

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

Grey Divider

AI Description

• **P0 – Admin role demotion prevention**: Replaces bare upsert in baseball onboarding with
 ensureUserRowPreservingAdmin, which reads the existing row first and skips the update if the user
 is already admin, preventing service-role writes from clobbering the super-admin's role.
• **P1 – RSC boundary crash fix**: EmptyState now accepts React.ReactElement in addition to
 LucideIcon component types; all Server Component call-sites updated to pass rendered elements (``)
 instead of component references, eliminating the "Functions cannot be passed to Client Components"
 digest crash.
• **P2 – Schema drift reconcile**: Migration adds ~40 missing columns to
 baseball_program_settings, baseball_integration_configs, and baseball_catching_events
 (settings-OS parity, adapter-contract columns, event_type), fixing live 400s; TypeScript types
 regenerated to match.
• **Telemetry gate**: Extracts shouldPersistAdminTables() into its own module
 (src/lib/telemetry-gate.ts) so it can be a sync export; gates all four telemetry writers
 (admin-logger, server-error-logger, log-event route, log-error route) to prod-only, with
 ADMIN_EVENTS_FORCE_CAPTURE=1 escape hatch.
• **Next.js control-flow filter**: isNextControlFlowError() prevents
 redirect()/notFound()/DYNAMIC_SERVER_USAGE signals from being recorded as incidents;
 demo-sessions page marked force-dynamic to stop ISR prerender bursts.
• **Retention pg_cron**: Adds nightly cron job to purge admin_events and admin_analytics_events
 older than 180 days (table had grown to ~91k rows / 287MB with no retention policy).
• **Dependabot rebase disabled**: Sets rebase-strategy: disabled across all 7 package ecosystems
 to stop auto-rebase pushes from triggering billed Vercel preview builds on pre-gating branches.
• **Panel retry UX**: PanelBoundary gains a real retry button that calls router.refresh() and
 remounts the boundary subtree; PanelStale copy corrected from misleading "showing last known data"
 to "temporarily unavailable".
• **Lint ratchet re-locked**: Baseline counts reduced (2275→2158 effective violations) after new
 code was written to standard.
Diagram

graph TD
    subgraph Auth_Guard["Auth Guard (P0)"]
        OB["onboarding.ts"] --> EUPA["ensureUserRowPreservingAdmin()"]
        EUPA --> UsersDB[("public.users")]
    end

    subgraph RSC_Fix["RSC Boundary Fix (P1)"]
        RSC_Pages["Server Pages\n(not-found, team, travel)"] -->|"icon=<Icon />"| ES["EmptyState.tsx\n'use client'"]
    end

    subgraph Telemetry_Gate["Telemetry Gate"]
        TG["telemetry-gate.ts\nshouldPersistAdminTables()"] --> AL["admin-logger.ts"]
        TG --> SEL["server-error-logger.ts"]
        TG --> LERoute["api/log-event route"]
        TG --> LERRoute["api/log-error route"]
        SEL --> NCF["isNextControlFlowError()"]
    end

    subgraph Schema_Drift["Schema Drift (P2)"]
        MIG["Migration 20260703050000"] --> BPS[("baseball_program_settings")]
        MIG --> BIC[("baseball_integration_configs")]
        MIG --> BCE[("baseball_catching_events")]
        MIG --> DBTypes["database.ts types"]
    end

    subgraph Retention["Retention"]
        CRON["Migration 20260703043000"] --> AE[("admin_events")]
        CRON --> AAE[("admin_analytics_events")]
    end

    subgraph Panel_UX["Panel UX"]
        PB["PanelBoundary.tsx"] --> PRB["PanelRetryButton\nrouter.refresh()"]
        PB --> PS["PanelStates.tsx"]
    end

    subgraph Legend
        direction LR
        _db[("Database")] ~~~ _svc["Module/Component"] ~~~ _mig["Migration"]
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Upsert with conditional DO UPDATE
  • ➕ Single round-trip instead of read+write
  • ➕ Race-free: atomically skips admin demotion at the DB level
  • ➖ Requires raw SQL or RPC rather than the Supabase client upsert helper
  • ➖ Slightly harder to read for future maintainers
2. DB trigger on users table
  • ➕ Enforcement is universal — no application code can bypass it
  • ➕ Protects against future service-role writes from other code paths
  • ➖ Requires a new migration
  • ➖ Adds implicit DB behavior that is harder to discover during code review

Recommendation: The PR's approach of a consolidated fix train is correct given the goal of a single Vercel deploy. The main area worth considering is the ensureUserRowPreservingAdmin read-then-write pattern: it introduces a TOCTOU race (two concurrent onboarding calls for the same user could both read 'no row' and then both insert). For the current traffic volume this is acceptable, but a DB-level trigger or a DO UPDATE SET role = EXCLUDED.role WHERE users.role != &#x27;admin&#x27; upsert would be race-free. The current approach is simpler and the race window is tiny in practice.

Files changed (25) +760 / -74

Enhancement (3) +85 / -6
telemetry-gate.tsNew module: prod-only telemetry gate with force-capture hatch +19/-0

New module: prod-only telemetry gate with force-capture hatch

• Exports 'shouldPersistAdminTables()' — returns 'true' only when 'VERCEL_ENV=production' and not during the build phase, or when 'ADMIN_EVENTS_FORCE_CAPTURE=1'. Lives in its own file because 'server-error-logger.ts' is ''use server'' and cannot export sync functions.

src/lib/telemetry-gate.ts

PanelBoundary.tsxAdd real retry button with router.refresh() and boundary remount +45/-6

Add real retry button with router.refresh() and boundary remount

• Introduces 'PanelRetryButton' (calls 'router.refresh()' + resets boundary state in a transition) and wires it into 'PanelErrorBoundary'. 'PanelBoundary' now tracks an 'attempt' counter as the boundary's 'key' so retry gets a clean subtree mount instead of re-rendering a poisoned tree.

src/app/admin/_components/PanelBoundary.tsx

20260703043000_admin_events_retention_pg_cron.sqlAdd nightly pg_cron purge for admin_events older than 180 days +21/-0

Add nightly pg_cron purge for admin_events older than 180 days

• Schedules a daily cron job ('purge-admin-event-telemetry') at 04:10 UTC to delete rows older than 180 days from 'admin_events' and 'admin_analytics_events'. Idempotent via 'cron.schedule' upsert-by-name semantics.

supabase/migrations/20260703043000_admin_events_retention_pg_cron.sql

Bug fix (13) +376 / -51
onboarding.tsPrevent baseball onboarding from demoting admin users +73/-36

Prevent baseball onboarding from demoting admin users

• Introduces 'ensureUserRowPreservingAdmin' which reads the existing 'users' row before writing: if the user is already 'admin', the update is skipped entirely; player↔coach conversions are still allowed. Replaces two bare 'upsert' calls that ran as service-role and could overwrite the super-admin's role. Also tightens the 'already registered' fallback to require proof of account ownership (session match or password sign-in) before resuming onboarding, closing an account-enumeration vector.

src/app/baseball/actions/onboarding.ts

EmptyState.tsxAccept rendered ReactElement icons to fix RSC boundary crash +22/-7

Accept rendered ReactElement icons to fix RSC boundary crash

• Widens the 'icon' prop type from 'LucideIcon | null' to 'LucideIcon | React.ReactElement | null'. Uses 'isValidElement' to distinguish pre-rendered elements from component types, rendering them directly in the icon chip. Adds CSS size constraints ('[&>svg]:h-*') so element-form icons are sized identically to the component-form path.

src/components/fairway/feedback/EmptyState.tsx

not-found.tsxPass rendered <Compass> element to EmptyState to fix RSC crash +1/-1

Pass rendered <Compass> element to EmptyState to fix RSC crash

• Changes 'icon={Compass}' to 'icon={<Compass strokeWidth={1.75} />}' so the Server Component no longer passes a function reference across the RSC boundary.

src/app/golf/(dashboard)/dashboard/not-found.tsx

page.tsxPass rendered <Users> elements to EmptyState (RSC fix) +2/-2

Pass rendered <Users> elements to EmptyState (RSC fix)

• Updates two 'EmptyState' usages from 'icon={Users}' to 'icon={<Users strokeWidth={1.75} />}' to comply with the RSC boundary constraint.

src/app/golf/(dashboard)/dashboard/stats/team/page.tsx

page.tsxPass rendered <Plane> element to EmptyState (RSC fix) +1/-1

Pass rendered <Plane> element to EmptyState (RSC fix)

• Updates 'icon={Plane}' to 'icon={<Plane strokeWidth={1.75} />}' in the travel page's empty state.

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

admin-logger.tsGate admin event writes to production only +7/-0

Gate admin event writes to production only

• Adds 'shouldPersistAdminTables()' check at the top of 'logAdminEvent'; off-prod runtimes log a console message and return 'null' instead of writing to the prod incident feed.

src/lib/admin-logger.ts

server-error-logger.tsFilter Next.js control-flow errors and gate prod writes +25/-0

Filter Next.js control-flow errors and gate prod writes

• Adds 'isNextControlFlowError()' to skip 'redirect()'/'notFound()'/'DYNAMIC_SERVER_USAGE' signals before they reach Sentry or the DB. Adds 'shouldPersistAdminTables()' guard so off-prod runtimes only log to console.

src/lib/server-error-logger.ts

route.tsGate /api/admin/log-event to production deployments only +7/-0

Gate /api/admin/log-event to production deployments only

• Returns '{ success: true, persisted: false }' immediately when 'shouldPersistAdminTables()' is false, preventing preview/CI events from entering the prod incident feed.

src/app/api/admin/log-event/route.ts

route.tsGate /api/log-error to production deployments only +8/-0

Gate /api/log-error to production deployments only

• Same prod-gate pattern as the log-event route: short-circuits with a no-op success response on non-production runtimes.

src/app/api/log-error/route.ts

page.tsxForce-dynamic on demo-sessions page to stop ISR prerender bursts +6/-0

Force-dynamic on demo-sessions page to stop ISR prerender bursts

• Adds 'export const dynamic = 'force-dynamic'' to prevent Next.js build/ISR from attempting to prerender a page that requires cookies and live session rows, which was generating 130 phantom 'DYNAMIC_SERVER_USAGE' admin events per preview build.

src/app/golf/admin/demo-sessions/page.tsx

PanelStates.tsxCorrect misleading 'showing last known data' copy and add action slot +15/-2

Correct misleading 'showing last known data' copy and add action slot

• Changes the stale panel message from 'showing last known data' (never true — nothing cached is rendered) to 'temporarily unavailable'. Adds an optional 'action' prop so 'PanelBoundary' can inject the retry button.

src/app/admin/_components/PanelStates.tsx

20260703050000_baseball_651_settings_os_reconcile.sqlSchema drift reconcile: add ~40 missing columns to 3 baseball tables +196/-0

Schema drift reconcile: add ~40 missing columns to 3 baseball tables

• Adds the full settings-OS column set to 'baseball_program_settings' (player access, module enablement, guardian/scout access, AI settings, appearance, retention), adapter-contract columns to 'baseball_integration_configs' (provider_key, display_name, status, config, etc.), and 'event_type' to 'baseball_catching_events'. Relaxes the legacy 'integration_key NOT NULL' constraint and adds idempotent CHECK/UNIQUE constraints. All additive-only per repo convention.

supabase/migrations/20260703050000_baseball_651_settings_os_reconcile.sql

regen-docs.mjsFix regen-docs parsing to anchor searches inside public schema +13/-2

Fix regen-docs parsing to anchor searches inside public schema

• Adds 'publicSchemaOffset()' to skip the 'graphql_public' schema block (which has identical structure to 'public') before searching for Tables/Views/Functions/Enums, preventing the parser from locking onto the empty graphql schema and reporting zero database objects.

scripts/regen-docs.mjs

Tests (5) +102 / -8
telemetry-gate.test.tsNew unit tests for shouldPersistAdminTables gate logic +31/-0

New unit tests for shouldPersistAdminTables gate logic

• Covers all four gate branches: force-capture hatch, build-phase suppression, live production, and preview/CI/local dev suppression.

src/lib/tests/telemetry-gate.test.ts

panel-boundary.test.tsxAdd retry and honest-copy tests for PanelBoundary/PanelStale +50/-3

Add retry and honest-copy tests for PanelBoundary/PanelStale

• Mocks 'next/navigation', adds tests verifying the 'last known data' copy is gone, that retry calls 'router.refresh()' and remounts to show recovered content, and that 'PanelStale' renders 'temporarily unavailable'.

src/app/admin/_components/tests/panel-boundary.test.tsx

admin-logger-bridge.test.tsFix admin-logger tests broken by prod gate +8/-2

Fix admin-logger tests broken by prod gate

• Stubs 'ADMIN_EVENTS_FORCE_CAPTURE=1' in 'beforeEach' so column-mapping tests exercise the real write path despite the new prod gate; cleans up with 'vi.unstubAllEnvs()' in 'afterEach'.

src/lib/tests/admin-logger-bridge.test.ts

server-error-logger-bridge.test.tsFix server-error-logger tests broken by prod gate +8/-2

Fix server-error-logger tests broken by prod gate

• Same force-capture hatch pattern as admin-logger tests.

src/lib/tests/server-error-logger-bridge.test.ts

log-error.test.tsFix log-error route tests broken by prod gate +5/-1

Fix log-error route tests broken by prod gate

• Adds 'ADMIN_EVENTS_FORCE_CAPTURE=1' stub and 'afterEach' cleanup so the route's persistence path is exercised in tests.

src/test/api/log-error.test.ts

Documentation (1) +3 / -2
glossary.mdUpdate function count to 139 and add helm_lifting_mark_athlete_onboarded +3/-2

Update function count to 139 and add helm_lifting_mark_athlete_onboarded

• Auto-regenerated inventory: bumps function count from 138 to 139 and adds the new 'helm_lifting_mark_athlete_onboarded' entry to the alphabetical list.

memory/glossary.md

Other (3) +194 / -7
database.tsRegenerate TypeScript DB types to match schema drift reconcile +169/-3

Regenerate TypeScript DB types to match schema drift reconcile

• Adds all new columns from the settings-OS migration to 'baseball_program_settings', 'baseball_integration_configs', and 'baseball_catching_events' Row/Insert/Update interfaces, plus new FK relationships.

src/lib/types/database.ts

dependabot.ymlDisable dependabot auto-rebase across all 7 package ecosystems +21/-0

Disable dependabot auto-rebase across all 7 package ecosystems

• Adds 'rebase-strategy: disabled' to every 'updates' entry (npm root, GitHub Actions, 4 nested npm projects, Swift) to prevent auto-rebase pushes on pre-gating branches from triggering billed Vercel preview builds.

.github/dependabot.yml

.lint-baseline.jsonRe-lock lint ratchet baseline after violation reduction +4/-4

Re-lock lint ratchet baseline after violation reduction

• Reduces counts for 'helm/no-arbitrary-bg-white' (1299→1242), 'helm/no-arbitrary-radius' (37→27), 'helm/no-raw-button' (102→100), and 'helm/no-raw-input' (466→418).

.lint-baseline.json

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 93 rules

Grey Divider


Remediation recommended

1. User email drift 🐞 Bug ≡ Correctness
Description
ensureUserRowPreservingAdmin() returns early when the existing role matches, so it never updates
public.users.email for existing rows even if the Auth email has changed. This can break downstream
code paths that resolve users by matching public.users.email (e.g., admin briefing links).
Code

src/app/baseball/actions/onboarding.ts[R86-89]

+  if (existing.role === 'admin' || existing.role === role) return { error: null };
+
+  const { error } = await admin.from('users').update({ role }).eq('id', userId);
+  return { error };
Relevance

⭐⭐ Medium

No historical evidence found about syncing public.users.email on auth email changes in onboarding
actions.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The helper’s early-return skips updating any fields for existing rows, while the app allows changing
Auth email and other parts of the code resolve users by matching public.users.email, so drift can
cause lookups to fail.

src/app/baseball/actions/onboarding.ts[67-90]
src/app/golf/(dashboard)/dashboard/settings/page.tsx[808-833]
src/lib/admin/data/briefing.ts[311-337]
supabase/migrations/20260527000000_prod_public_baseline.sql[3790-3821]

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

### Issue description
`ensureUserRowPreservingAdmin()` currently returns early when `existing.role === 'admin' || existing.role === role`, which prevents updating `public.users.email` for existing users. Since the app supports email changes via `supabase.auth.updateUser({ email })`, `public.users.email` can drift and email-based lookups can fail.

### Issue Context
- `public.users.email` is used for resolving user links in admin tooling.
- The helper was introduced to prevent service-role upserts from demoting admins; we should preserve that safety while still syncing email.

### Fix Focus Areas
- src/app/baseball/actions/onboarding.ts[67-90]

### Suggested fix
- Select both `role` and `email` when reading the existing row.
- If the row exists:
 - If `existing.email !== email`, update `email`.
 - Only update `role` when `existing.role !== 'admin'` and `existing.role !== role`.
 - If neither field needs changes, return `{ error: null }`.
- Keep the “never demote admin” guarantee intact.

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



Informational

2. onboarding.ts outside actions dir 📘 Rule violation ⌂ Architecture
Description
src/app/baseball/actions/onboarding.ts is a module-scoped 'use server' server-actions file, but
it is located outside src/app/actions/, which violates the server action placement requirement.
Keeping server action modules outside the designated directory increases the risk of inconsistent
action organization and accidental co-location with route code.
Code

src/app/baseball/actions/onboarding.ts[R60-90]

+/**
+ * Ensure the public.users row exists with the requested self-service role,
+ * WITHOUT demoting an existing 'admin'. A plain upsert here runs as
+ * service_role (bypasses the self-escalation trigger) and clobbered
+ * admin@helmsportslabs.com down to 'coach' on 2026-07-03, locking the only
+ * admin out of /golf/admin. player<->coach conversion stays allowed.
+ */
+async function ensureUserRowPreservingAdmin(
+  admin: ReturnType<typeof createAdminClient>,
+  userId: string,
+  email: string,
+  role: 'coach' | 'player',
+): Promise<{ error: unknown }> {
+  const { data: existing, error: readError } = await admin
+    .from('users')
+    .select('role')
+    .eq('id', userId)
+    .maybeSingle();
+  if (readError) return { error: readError };
+
+  if (!existing) {
+    const { error } = await admin
+      .from('users')
+      .upsert({ id: userId, email, role }, { onConflict: 'id', ignoreDuplicates: true });
+    return { error };
+  }
+  if (existing.role === 'admin' || existing.role === role) return { error: null };
+
+  const { error } = await admin.from('users').update({ role }).eq('id', userId);
+  return { error };
+}
Relevance

⭐ Low

Repo commonly keeps 'use server' actions under sport folders (e.g., src/app/baseball/actions);
pattern merged unchanged.

PR-#574
PR-#736
PR-#740

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519234 requires server action implementation files (identified by a module-level
'use server' directive) to be located under src/app/actions/. The file
src/app/baseball/actions/onboarding.ts begins with 'use server' while residing in
src/app/baseball/actions/, and this PR adds additional server-side action logic in that same file.

Rule 1519234: Place Next.js server actions in src/app/actions directory
src/app/baseball/actions/onboarding.ts[1-5]
src/app/baseball/actions/onboarding.ts[60-90]

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 server-actions module (module-level `'use server'`) lives at `src/app/baseball/actions/onboarding.ts`, but compliance requires server action implementation files to live under `src/app/actions/`.

## Issue Context
This PR adds/updates logic in `src/app/baseball/actions/onboarding.ts`, which is a `'use server'` module and therefore a server-actions implementation file.

## Fix Focus Areas
- src/app/baseball/actions/onboarding.ts[1-10]
- src/app/baseball/actions/onboarding.ts[60-90]

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


3. Implicit pg_cron schema 🐞 Bug ☼ Reliability
Description
The retention migration creates pg_cron without specifying a schema, making installation depend on
search_path and diverging from the repo’s convention of pinning extension schemas explicitly. This
can reduce portability and can fail or install objects into an unintended schema in some
environments.
Code

supabase/migrations/20260703043000_admin_events_retention_pg_cron.sql[10]

+CREATE EXTENSION IF NOT EXISTS pg_cron;
Relevance

⭐ Low

This exact migration (CREATE EXTENSION pg_cron without schema) was merged previously; convention not
enforced here.

PR-#739
PR-#740
PR-#117

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new migration omits an explicit schema for pg_cron, while the baseline migration pins
extension schemas explicitly, showing an established convention and highlighting the new
non-determinism.

supabase/migrations/20260703043000_admin_events_retention_pg_cron.sql[1-21]
supabase/migrations/20260527000000_prod_public_baseline.sql[16-25]

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

### Issue description
The migration uses `CREATE EXTENSION IF NOT EXISTS pg_cron;` without `WITH SCHEMA ...`, so the installation location depends on `search_path`.

### Issue Context
Other migrations in this repo explicitly pin extension schemas (e.g., `hypopg` in `extensions`, others in `public`), which avoids non-determinism.

### Fix Focus Areas
- supabase/migrations/20260703043000_admin_events_retention_pg_cron.sql[10-20]

### Suggested fix
- Update to an explicit schema consistent with your Supabase convention, e.g.:
 - `CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA extensions;`
 - (or `WITH SCHEMA public;` if that’s the established target for this repo)
- If you need maximal portability, consider wrapping extension creation in a DO block that gives a clearer error message when `pg_cron` is unavailable.

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


Grey Divider

Qodo Logo

Comment on lines +86 to +89
if (existing.role === 'admin' || existing.role === role) return { error: null };

const { error } = await admin.from('users').update({ role }).eq('id', userId);
return { 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.

Remediation recommended

2. User email drift 🐞 Bug ≡ Correctness

ensureUserRowPreservingAdmin() returns early when the existing role matches, so it never updates
public.users.email for existing rows even if the Auth email has changed. This can break downstream
code paths that resolve users by matching public.users.email (e.g., admin briefing links).
Agent Prompt
### Issue description
`ensureUserRowPreservingAdmin()` currently returns early when `existing.role === 'admin' || existing.role === role`, which prevents updating `public.users.email` for existing users. Since the app supports email changes via `supabase.auth.updateUser({ email })`, `public.users.email` can drift and email-based lookups can fail.

### Issue Context
- `public.users.email` is used for resolving user links in admin tooling.
- The helper was introduced to prevent service-role upserts from demoting admins; we should preserve that safety while still syncing email.

### Fix Focus Areas
- src/app/baseball/actions/onboarding.ts[67-90]

### Suggested fix
- Select both `role` and `email` when reading the existing row.
- If the row exists:
  - If `existing.email !== email`, update `email`.
  - Only update `role` when `existing.role !== 'admin'` and `existing.role !== role`.
  - If neither field needs changes, return `{ error: null }`.
- Keep the “never demote admin” guarantee intact.

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

This was referenced Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI, Vercel, test, build, deploy database Schema, migrations, indexes, SQL security Auth, secrets, RLS, PII, webhooks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant