Skip to content

Error sweep: APNs token pruning, honest severities, unauth redirects, baseball RLS-denial parity#798

Merged
njrini99-code merged 4 commits into
mainfrom
fix/error-sweep
Jul 10, 2026
Merged

Error sweep: APNs token pruning, honest severities, unauth redirects, baseball RLS-denial parity#798
njrini99-code merged 4 commits into
mainfrom
fix/error-sweep

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Morning error-sweep directive: all three production error sources (Vercel runtime, in-app admin_events, Sentry) triaged — the entire surface reduced to three root causes, all fixed and adversarially verified (with a repair round; re-verifiers confirmed each defect closed).

The three root causes

  1. Dead APNs tokens never pruned — 410 Unregistered/BadDeviceToken now deletes the device_tokens row in the shared send path. Ends retry spam running since June 11. Fixes JAVASCRIPT-NEXTJS-BQ.
  2. Empty-state logged as ERROR — "no completed rounds in 90 days" and background session-expiry now carry typed codes (engine_no_recent_rounds / engine_session_expired) classified info/warning + skipSentry across both consumers; unrecognized codes still alert. Fixes JAVASCRIPT-NEXTJS-BP.
  3. BaseballUnauthorizedError leaking as raw 500 — shared redirectOnUnauthorized helper (golf idiom, returnTo preserved) across all 11 affected pages; independent sweep of 201 wrapped getters × 106 pages confirms zero remaining instances. Fixes JAVASCRIPT-NEXTJS-BN.

Baseball tracking parity

  • RLS-denial capture wired centrally into withBaseballAction (60/60 action files covered) + explicit capture at PG-error swallow sites; lifting rethrow corrected (revert-experiment proved the pin test load-bearing).
  • 3 route-group error boundaries; dead admin_client_errors realtime subscription removed.
  • Audit finding worth knowing: baseball is ahead of golf on action instrumentation — golf's CRM cluster (9 files, ~55 actions) has zero error instrumentation; S-effort follow-up sketched in the audit.

Gates: tsc 0 · eslint 0 (max-warnings 0) · unit 478 files / 4,860 passed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX

…auth redirects, baseball RLS-denial parity

From the three-source error triage (Vercel runtime + admin_events + Sentry),
whose entire surface reduced to three root causes:

- APNs 410 Unregistered / BadDeviceToken responses now prune the
  device_tokens row in the shared send path (every push caller benefits) —
  ends the retry spam running since Jun 11 (Fixes JAVASCRIPT-NEXTJS-BQ)
- Expected empty-state soft failures (engine_no_recent_rounds,
  engine_session_expired) carry typed codes and log at info/warning with
  skipSentry across BOTH consumers (observeActionSoftFailure and
  post-round-trigger); unrecognized codes still alert at error severity
  (Fixes JAVASCRIPT-NEXTJS-BP)
- BaseballUnauthorizedError no longer leaks as a raw 500: shared
  redirectOnUnauthorized helper (golf layout idiom, returnTo preserved)
  applied across all 11 affected pages; independent sweep confirms zero
  remaining instances of the bug class (Fixes JAVASCRIPT-NEXTJS-BN)

Baseball error-tracking parity with golf:
- RLS-denial capture wired centrally into withBaseballAction (all 60
  action files covered for free) + explicit sites where actions swallow
  PG errors into result envelopes; lifting logSetResult rethrow corrected
  so the wrapper fallback cannot double-capture
- 3 route-group error boundaries added; dead admin_client_errors realtime
  subscription removed
- Audit verdict recorded: baseball is AHEAD of golf on action
  instrumentation (60/60 wrapped vs golf CRM cluster 9 files/~55 actions
  uninstrumented — follow-up)

Every fix adversarially verified (repairs re-verified; one load-bearing
test proven by revert-experiment).

Gates: tsc 0, eslint 0, unit 478 files / 4860 passed.

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

This pull request has been ignored for the connected project qmnssrrolpinvwjjnufo because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@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 2:15pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The pull request adds targeted unauthorized redirects and route error boundaries, RLS-denial capture across Supabase actions, stable engine-failure telemetry classification, removal of an inactive realtime subscription, push-token pruning events, and regression tests.

Changes

Unauthorized routing and dashboard errors

Layer / File(s) Summary
Redirect helper and page wiring
src/lib/baseball/redirect-on-unauthorized.ts, src/app/baseball/.../page.tsx
Unauthorized errors redirect to login with encoded returnTo values; other errors continue propagating.
Regression coverage
src/lib/baseball/__tests__/*, src/app/baseball/.../__tests__/*
Tests cover unauthorized redirects, non-unauthorized rethrows, and successful rendering.
Route error boundaries
src/app/baseball/.../error.tsx
Operations, Lift Builder, and scouting routes render configured RouteErrorBoundary components.

RLS-denial observability

Layer / File(s) Summary
Action-level capture
src/app/actions/messages.ts, src/app/baseball/actions/{announcements,games,lifting-v11,watchlist}.ts
Supabase failures capture table, operation, action, sport, feature, and user metadata.
Wrapper fallback and tests
src/lib/baseball/with-baseball-action.ts, src/lib/baseball/__tests__/*, src/app/baseball/actions/__tests__/*
Unexpected RLS-shaped failures are captured centrally, with tests for precise capture and duplicate suppression.

Engine failure observability

Layer / File(s) Summary
Stable result codes and classification
src/app/golf/actions/insights.ts, src/lib/coachhelm/v2/{trigger-insights-bridge,post-round-trigger}.ts, src/lib/admin/observe-action-result.ts
Engine outcomes expose stable codes and map to informational, warning, or error telemetry.
Validation and realtime cleanup
src/lib/admin/__tests__/observe-action-result.test.ts, src/test/coachhelm/v2/post-round-trigger.test.ts, src/hooks/useAdminRealtime.ts
Classification behavior is tested and the inactive admin_client_errors subscription is removed.

Push-token pruning telemetry

Layer / File(s) Summary
Deactivation logging and tests
src/lib/notifications/push.ts, src/test/lib/notifications/push.test.ts
Successful dead-token deactivation emits an info event; failed updates and unrelated failures do not.

Repository change-file filtering

Layer / File(s) Summary
Existing-path filtering
.github/scripts/changed-files.sh
Diff-listed paths are emitted only when they exist in the current checkout.

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

Possibly related PRs

Suggested labels: security


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (1 error, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
No Service-Role In Client Bundles ❌ Error FAIL: src/test/lib/notifications/push.test.ts:97 stubs SUPABASE_SERVICE_ROLE_KEY outside exempt admin/api paths. Remove the service-role env stub from this changed test, or move the service-role-dependent setup behind an exempt admin helper/path.
Title check ⚠️ Warning The title is descriptive, but it does not follow the required Conventional Commits format or include a required scope. Rewrite the title as Conventional Commits, e.g. fix(baseball): ..., and include one of the required scopes.
Conventional Commits ❓ Inconclusive The rule at .coderabbit.yaml:14-18 requires both PR title and squash subject, but this checkout only exposes commit subjects; the actual PR title isn't available to verify. Provide the PR title or squash-merge subject (or PR metadata) so I can confirm whether both match the regex.
✅ Passed checks (9 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the PR’s main fixes and related baseball/RLS instrumentation work.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
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: PR diff against origin/main includes no files under supabase/migrations, so there are no changed migrations to audit for RLS policy coverage.
Auth Check In Server Actions ✅ Passed PASS: src/app/actions/messages.ts:79-106 authenticates before .from(); the other changed action exports are wrappers or have no DB calls.
Sport-Prefixed Table Names ✅ Passed PASS: changed queries use only prefixed tables (golf_* in src/app/actions/messages.ts:494-495,627-649 and src/lib/coachhelm/v2/post-round-trigger.ts:106; baseball_* elsewhere).
No Destructive Writes ✅ Passed No changed save/submit/sync path does delete→insert on the same table; games.ts:1763-1808 and messages.ts:300-362 only roll back inserts, and push.ts:176-244 uses updates.
No Edits To Historical Migrations ✅ Passed No files under supabase/migrations/ were changed in origin/main...HEAD; the diff contains zero migration paths.
✨ 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/error-sweep
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

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

@coderabbitai coderabbitai Bot added the security Auth, secrets, RLS, PII, webhooks label Jul 10, 2026
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR addresses three production error sources: dead APNs tokens accumulating retries, routine engine outcomes (no recent rounds, background session expiry) logged as errors, and BaseballUnauthorizedError propagating as raw 500s from Server Component pages. All three fixes are architecturally sound — the severity-classification system uses stable code fields rather than message regexes, the redirectOnUnauthorized helper correctly encapsulates the redirect pattern, and the RLS-denial capture is centralized at the withBaseballAction wrapper so all 60+ action call sites get telemetry for free.

  • APNs pruning (push.ts): soft-deletes dead tokens (active: false) on 410/BadDeviceToken; prune log is gated on the UPDATE succeeding, preventing false-confirmed prunes.
  • Severity classification (observe-action-result.ts, post-round-trigger.ts, insights.ts): engine_no_recent_roundslogServerEvent at info/skipSentry; engine_session_expiredlogServerError at warning/skipSentry; both consumers import the same classification helpers so they can't diverge.
  • Unauthorized redirect (redirect-on-unauthorized.ts): new shared guard wraps action calls and redirects to /baseball/login?returnTo=… on BaseballUnauthorizedError; applied across 9 settings/dashboard pages; scout-packets and scout-packet/preview use inline try/catch instead of the helper.

Confidence Score: 4/5

Safe to merge. All three error sources are correctly addressed, the RLS double-fire regression is pinned by a dedicated test, and the redirect helper is well-covered. Two pages use inline guard logic instead of the new helper, and several announcement/game action callbacks omit user attribution from RLS capture calls — both are minor gaps with no user-facing impact.

The core changes are architecturally clean and well-tested (4,860 passing, regression test for double-fire, severity-classification tests for both consumers). The two non-blocking findings — inconsistent use of the new helper in scout-packets/preview pages, and missing userId in four maybeCaptureRlsDenial calls inside withBaseballAction callbacks — reduce future debuggability but don't affect correctness or user data.

src/app/baseball/actions/announcements.ts and src/app/baseball/actions/games.ts (missing userId in RLS capture calls); src/app/baseball/(dashboard)/dashboard/scout-packets/page.tsx and the scout-packet preview page (inline guard pattern vs. the new redirectOnUnauthorized helper).

Important Files Changed

Filename Overview
src/lib/notifications/push.ts Adds soft-delete (active: false) for dead APNs tokens on 410/BadDeviceToken; prune event logged at info only when UPDATE succeeds. Correct fire-and-forget pattern; logServerEvent import wired.
src/lib/baseball/redirect-on-unauthorized.ts New shared guard: runs action, redirects to /baseball/login?returnTo=… on BaseballUnauthorizedError, rethrows all other errors unchanged. Used consistently by 9 settings/dashboard pages in this PR.
src/lib/admin/observe-action-result.ts Adds EXPECTED_SOFT_FAILURE_CODES (engine_session_expired → warning/skipSentry) and EXPECTED_EMPTY_STATE_CODES (engine_no_recent_rounds → info/skipSentry); routes info-tier outcomes to logServerEvent instead of logServerError. Type-safe, backward-compatible, well-tested.
src/lib/baseball/with-baseball-action.ts Wires maybeCaptureRlsDenial into the shared catch block (table=featureArea fallback) so all 60 withBaseballAction call sites get RLS-denial telemetry for free; checked against raw error before normalization to preserve .code.
src/app/baseball/actions/announcements.ts Adds maybeCaptureRlsDenial at four swallow sites; withBaseballAction callbacks that have ctx.user.id available omit userId from each call, reducing admin-event attribution.
src/app/baseball/actions/lifting-v11.ts Fixes double-fire: captures precise RLS denial at helm_lifting_set_results then rethrows BaseballActionError (not raw error), preventing the wrapper's generic catch-all from firing a second bogus denial with table=featureArea.
src/app/baseball/(dashboard)/dashboard/scout-packets/page.tsx Fixes BaseballUnauthorizedError leak via inline try/catch rather than the redirectOnUnauthorized helper used by the 9 other pages fixed in this PR; functionally correct but diverges from the established pattern.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[withBaseballAction catch block] --> B{instanceof BaseballUnauthorizedError?}
    B -- yes --> C[rethrow to Server Component page]
    C --> D{redirectOnUnauthorized or inline try-catch}
    D -- is unauthorized --> E[redirect /baseball/login?returnTo=]
    D -- is other error --> F[rethrow to error.tsx]
    B -- no --> G[maybeCaptureRlsDenial table=featureArea verb=rpc]
    G --> H{isRlsDenial code=42501?}
    H -- yes --> I[logServerEvent RLS denial]
    H -- no --> J[no-op]
    G --> K[logServerException normalized error]
    K --> L[rethrow sanitized Error]
    subgraph Action body override
    M[Action body detects RLS] --> N[maybeCaptureRlsDenial precise table]
    N --> O[throw new BaseballActionError]
    O --> B
    end
    subgraph Severity classification
    P[triggerPlayerInsightsAfterRound result] --> Q{code?}
    Q -- engine_no_recent_rounds --> R[logServerEvent info + skipSentry]
    Q -- engine_session_expired --> S[logServerError warning + skipSentry]
    Q -- unrecognized --> T[logServerError error no skipSentry]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[withBaseballAction catch block] --> B{instanceof BaseballUnauthorizedError?}
    B -- yes --> C[rethrow to Server Component page]
    C --> D{redirectOnUnauthorized or inline try-catch}
    D -- is unauthorized --> E[redirect /baseball/login?returnTo=]
    D -- is other error --> F[rethrow to error.tsx]
    B -- no --> G[maybeCaptureRlsDenial table=featureArea verb=rpc]
    G --> H{isRlsDenial code=42501?}
    H -- yes --> I[logServerEvent RLS denial]
    H -- no --> J[no-op]
    G --> K[logServerException normalized error]
    K --> L[rethrow sanitized Error]
    subgraph Action body override
    M[Action body detects RLS] --> N[maybeCaptureRlsDenial precise table]
    N --> O[throw new BaseballActionError]
    O --> B
    end
    subgraph Severity classification
    P[triggerPlayerInsightsAfterRound result] --> Q{code?}
    Q -- engine_no_recent_rounds --> R[logServerEvent info + skipSentry]
    Q -- engine_session_expired --> S[logServerError warning + skipSentry]
    Q -- unrecognized --> T[logServerError error no skipSentry]
    end
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/app/baseball/actions/announcements.ts:155-163
These three `withBaseballAction` callbacks (`createAnnouncementAction`, `acknowledgeAnnouncementImpl`, `deleteAnnouncementAction`) all receive `ctx` and have `ctx.user.id` in scope, but omit `userId` from `maybeCaptureRlsDenial`. The same oversight appears in `saveFullBoxScoreAction` in `games.ts`. Without user attribution the RLS denial events in the admin feed can't be correlated to a specific coach or session, making it harder to distinguish a user-specific policy misconfiguration from a schema-wide grant issue. Compare `watchlist.ts` `addToWatchlist` and `lifting-v11.ts` `logSetResult`, which both pass `userId: ctx.user.id`.

```suggestion
    if (annError || !announcement) {
      maybeCaptureRlsDenial(annError, {
        table: 'baseball_announcements',
        verb: 'insert',
        action: 'createAnnouncement',
        feature: 'baseball_announcements',
        sport: 'baseball',
        userId: ctx.user.id,
      });
      return { success: false, error: 'Failed to create announcement' };
```

### Issue 2 of 2
src/app/baseball/(dashboard)/dashboard/scout-packets/page.tsx:47-55
`scout-packets/page.tsx` and `scout-packet/preview/page.tsx` (the two pages not refactored in this PR) use inline try/catch while the other 9 pages fixed here all use the `redirectOnUnauthorized` helper. The inline code is functionally correct today, but if the login redirect target or the unauthorized predicate ever changes, these two pages are silent drift points. `redirectOnUnauthorized` was specifically designed to avoid this — using it here would be a one-liner swap.

```suggestion
  const roster = await redirectOnUnauthorized(
    () => getScoutPacketRoster(),
    (error) => error instanceof BaseballUnauthorizedError,
    '/baseball/dashboard/scout-packets',
  );
```

Reviews (1): Last reviewed commit: "Error sweep: prune dead APNs tokens, hon..." | Re-trigger Greptile

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Error sweep: APNs token pruning, soft-failure severities, unauth redirects, RLS parity

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Prune dead device_tokens rows on APNs 410 Unregistered/BadDeviceToken in the shared push send
 path, ending retry spam.
• Classify expected empty-state soft failures (engine_no_recent_rounds, engine_session_expired)
 as info/warning with skipSentry across both consumers instead of raw errors.
• Add shared redirectOnUnauthorized helper and apply it to 9 baseball pages so
 BaseballUnauthorizedError redirects to login instead of raw-throwing a 500.
• Wire RLS-denial capture centrally into withBaseballAction plus explicit capture at PG-error
 swallow sites for baseball/golf parity; fix a rethrow bug causing double-fired denial events.
• Add 3 new route-group error boundaries and remove a dead admin_client_errors realtime
 subscription.
Diagram

graph TD
    A["Baseball Page (Server Component)"] -->|"calls getter"| B["withBaseballAction wrapper"]
    B -->|"BaseballUnauthorizedError"| C["redirectOnUnauthorized()"] --> D["/baseball/login?returnTo=..."]
    B -->|"RLS-shaped PG error"| E["maybeCaptureRlsDenial()"] --> F[(admin_events)]
    G["Push send path"] -->|"APNs 410 Unregistered"| H[(device_tokens)]
    I["triggerPlayerInsightsAfterRound"] -->|"typed code"| J["observeActionSoftFailure / postRoundTrigger"] --> F
    subgraph Legend
      direction LR
      _svc([Service]) ~~~ _db[(Database)]
    end
Loading
High-Level Assessment

Centralizing fixes in shared primitives (redirectOnUnauthorized, wrapper-level maybeCaptureRlsDenial, code-based severity classification) is the right call — it eliminates an entire bug class in one place rather than patching each of the 9+ affected pages independently, and mirrors the existing golf idiom rather than inventing a new pattern. No meaningfully better alternative architecture was evident; per-page ad-hoc try/catch would have been more error-prone and inconsistent, which is precisely the defect being fixed.

Files changed (44) +2282 / -101

Enhancement (12) +362 / -32
observe-action-result.tsAdd code-based expected/empty-state classification and info severity tier +71/-25

Add code-based expected/empty-state classification and info severity tier

• Introduces EXPECTED_SOFT_FAILURE_CODES and EXPECTED_EMPTY_STATE_CODES sets, isExpectedEmptyStateCode(), and routes info-severity failures through logServerEvent instead of logServerError.

src/lib/admin/observe-action-result.ts

trigger-insights-bridge.tsPropagate typed 'code' field through the trigger-insights result type +22/-2

Propagate typed 'code' field through the trigger-insights result type

• Extends the TriggerPlayerInsightsFn and triggerPlayerInsightsAfterRound return types to include the new engine_no_recent_rounds/engine_session_expired code field.

src/lib/coachhelm/v2/trigger-insights-bridge.ts

insights.tsAttach stable 'code' to empty-state and session-expired soft failures +30/-2

Attach stable 'code' to empty-state and session-expired soft failures

• triggerPlayerInsightsAfterRoundImpl now returns a typed code alongside the existing error message for the two routine empty-state outcomes.

src/app/golf/actions/insights.ts

redirect-on-unauthorized.tsNew shared redirectOnUnauthorized helper +41/-0

New shared redirectOnUnauthorized helper

• Dependency-free helper that runs an action and redirects to /baseball/login with returnTo preserved when a caller-supplied predicate recognizes the thrown error as unauthorized, else rethrows.

src/lib/baseball/redirect-on-unauthorized.ts

with-baseball-action.tsCentralize RLS-denial capture in the shared action wrapper +33/-0

Centralize RLS-denial capture in the shared action wrapper

• Adds a maybeCaptureRlsDenial call in the generic catch-all path so every withBaseballAction call site gets denial telemetry for free, keyed on featureArea/verb='rpc'.

src/lib/baseball/with-baseball-action.ts

watchlist.tsAdd explicit RLS-denial capture to addToWatchlist +9/-0

Add explicit RLS-denial capture to addToWatchlist

• Captures a precise RLS denial at the insert-error swallow site before rethrowing BaseballActionError.

src/app/baseball/actions/watchlist.ts

announcements.tsAdd explicit RLS-denial capture across announcement actions +35/-2

Add explicit RLS-denial capture across announcement actions

• Adds maybeCaptureRlsDenial calls at create/read/acknowledge/delete error-swallow sites for baseball announcements.

src/app/baseball/actions/announcements.ts

games.tsAdd explicit RLS-denial capture to saveFullBoxScore +11/-1

Add explicit RLS-denial capture to saveFullBoxScore

• Captures RLS denial before returning a sanitized error from the box-score RPC call.

src/app/baseball/actions/games.ts

messages.tsAdd RLS-denial capture across messaging actions +41/-0

Add RLS-denial capture across messaging actions

• Wires maybeCaptureRlsDenial into sendMessage, createConversation, and markMessagesAsRead error paths for both golf and baseball messaging tables.

src/app/actions/messages.ts

error.tsxNew route error boundary for operations page +23/-0

New route error boundary for operations page

• Adds RouteErrorBoundary for the operations dashboard route with route-specific title/message/homePath.

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

error.tsxNew route error boundary for lift builder page +23/-0

New route error boundary for lift builder page

• Adds RouteErrorBoundary for the performance/builder route.

src/app/baseball/(dashboard)/dashboard/performance/builder/error.tsx

error.tsxNew route error boundary for scouting page +23/-0

New route error boundary for scouting page

• Adds RouteErrorBoundary for the scouting route.

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

Bug fix (14) +236 / -23
push.tsLog info-level confirmation when a dead APNs token is pruned +21/-2

Log info-level confirmation when a dead APNs token is pruned

• Captures the update error/result from the device_tokens deactivation and emits an info-severity logServerEvent only when the row was actually deactivated, avoiding false prune claims.

src/lib/notifications/push.ts

post-round-trigger.tsClassify engine failure severity consistently with observeActionSoftFailure +36/-3

Classify engine failure severity consistently with observeActionSoftFailure

• Adds classifyEngineFailureSeverity to mirror the same code-based severity classification used by the withAdminObserved consumer, preventing disagreement between the two consumers of the same result.

src/lib/coachhelm/v2/post-round-trigger.ts

lifting-v11.tsFix double-fire RLS-denial capture in logSetResult +18/-1

Fix double-fire RLS-denial capture in logSetResult

• Rethrows a generic BaseballActionError instead of the raw Postgrest error after an explicit precise RLS-denial capture, preventing the wrapper's fallback from firing a second bogus denial event.

src/app/baseball/actions/lifting-v11.ts

page.tsxRedirect on unauthorized instead of raw-throwing in decision room page +13/-3

Redirect on unauthorized instead of raw-throwing in decision room page

• Wraps getDecisionRoomData with redirectOnUnauthorized so an expired session redirects to login rather than surfacing as a raw 500.

src/app/baseball/(dashboard)/dashboard/decision-room/page.tsx

page.tsxRedirect on unauthorized instead of raw-throwing in scout-packet preview page +22/-1

Redirect on unauthorized instead of raw-throwing in scout-packet preview page

• Catches BaseballUnauthorizedError from getScoutPacketPreview and redirects to login with returnTo preserved.

src/app/baseball/(dashboard)/dashboard/players/[id]/scout-packet/preview/page.tsx

page.tsxRedirect on unauthorized instead of raw-throwing in scout packets hub page +18/-1

Redirect on unauthorized instead of raw-throwing in scout packets hub page

• Catches BaseballUnauthorizedError from getScoutPacketRoster and redirects to login.

src/app/baseball/(dashboard)/dashboard/scout-packets/page.tsx

page.tsxRedirect on unauthorized in settings audit page +16/-2

Redirect on unauthorized in settings audit page

• Wraps both getProgramSettings and getSettingsAuditLog calls with redirectOnUnauthorized.

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

page.tsxRedirect on unauthorized in import sources page +16/-2

Redirect on unauthorized in import sources page

• Wraps getProgramSettings and listImportSources calls with redirectOnUnauthorized.

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

page.tsxRedirect on unauthorized in integrations page +16/-2

Redirect on unauthorized in integrations page

• Wraps getProgramSettings and listIntegrations calls with redirectOnUnauthorized.

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

page.tsxRedirect on unauthorized in permissions page +12/-1

Redirect on unauthorized in permissions page

• Wraps getPermissionMatrix with redirectOnUnauthorized.

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

page.tsxRedirect on unauthorized in program settings page +12/-2

Redirect on unauthorized in program settings page

• Wraps getProgramSettings with redirectOnUnauthorized.

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

page.tsxRedirect on unauthorized in roles page +12/-1

Redirect on unauthorized in roles page

• Wraps getRoleTemplates with redirectOnUnauthorized.

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

page.tsxRedirect on unauthorized in season settings page +12/-1

Redirect on unauthorized in season settings page

• Wraps listSeasons with redirectOnUnauthorized.

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

page.tsxRedirect on unauthorized in team settings page +12/-1

Redirect on unauthorized in team settings page

• Wraps getTeamJoinSettings with redirectOnUnauthorized.

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

Refactor (1) +8 / -44
useAdminRealtime.tsRemove dead admin_client_errors realtime subscription +8/-44

Remove dead admin_client_errors realtime subscription

• Deletes the permanently inert postgres_changes subscription for the superseded admin_client_errors table, replacing it with an explanatory comment.

src/hooks/useAdminRealtime.ts

Tests (17) +1676 / -2
page.test.tsxTest: decision room page redirects on unauthorized +91/-0

Test: decision room page redirects on unauthorized

• New test pinning the redirect-vs-rethrow behavior for BaseballUnauthorizedError.

src/app/baseball/(dashboard)/dashboard/decision-room/tests/page.test.tsx

page.test.tsxTest: scout-packet preview page redirects on unauthorized +108/-0

Test: scout-packet preview page redirects on unauthorized

• New test covering redirect, rethrow, and success paths.

src/app/baseball/(dashboard)/dashboard/players/[id]/scout-packet/preview/tests/page.test.tsx

page.test.tsxTest: scout packets hub page redirects on unauthorized +105/-0

Test: scout packets hub page redirects on unauthorized

• New test covering redirect, rethrow, and success paths.

src/app/baseball/(dashboard)/dashboard/scout-packets/tests/page.test.tsx

page.test.tsxTest: settings audit page redirects on unauthorized (both getters) +108/-0

Test: settings audit page redirects on unauthorized (both getters)

• New test covering unauthorized redirect from either of two sequential getter calls.

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

page.test.tsxTest: import sources page redirects on unauthorized (both getters) +117/-0

Test: import sources page redirects on unauthorized (both getters)

• New test covering unauthorized redirect from either of two sequential getter calls.

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

page.test.tsxTest: integrations page redirects on unauthorized (both getters) +117/-0

Test: integrations page redirects on unauthorized (both getters)

• New test covering unauthorized redirect from either of two sequential getter calls.

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

page.test.tsxTest: permissions page redirects on unauthorized +87/-0

Test: permissions page redirects on unauthorized

• New test covering redirect, rethrow, and success paths.

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

page.test.tsxTest: program settings page redirects on unauthorized +91/-0

Test: program settings page redirects on unauthorized

• New test covering redirect, rethrow, and success paths.

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

page.test.tsxTest: roles page redirects on unauthorized +91/-0

Test: roles page redirects on unauthorized

• New test covering redirect, rethrow, and success paths.

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

page.test.tsxTest: season settings page redirects on unauthorized +91/-0

Test: season settings page redirects on unauthorized

• New test covering redirect, rethrow, and success paths.

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

page.test.tsxTest: team settings page redirects on unauthorized +91/-0

Test: team settings page redirects on unauthorized

• New test covering redirect, rethrow, and success paths.

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

redirect-on-unauthorized.test.tsUnit tests for the shared redirectOnUnauthorized helper +91/-0

Unit tests for the shared redirectOnUnauthorized helper

• Pins redirect, URL-encoding, rethrow, and success-passthrough behaviors of the new helper.

src/lib/baseball/tests/redirect-on-unauthorized.test.ts

with-baseball-action-observability.test.tsTest wrapper-level RLS-denial capture and no-double-fire behavior +85/-0

Test wrapper-level RLS-denial capture and no-double-fire behavior

• New describe block exercising the real maybeCaptureRlsDenial logic against the wrapper's catch-all fallback, including the no-double-fire regression case.

src/lib/baseball/tests/with-baseball-action-observability.test.ts

lifting-v11-rls-denial.test.tsRegression test for logSetResult RLS-denial double-fire +168/-0

Regression test for logSetResult RLS-denial double-fire

• New test file pinning that logSetResult captures exactly one precise RLS denial event, not a second bogus one from the wrapper fallback.

src/app/baseball/actions/tests/lifting-v11-rls-denial.test.ts

observe-action-result.test.tsTests for code-based severity classification in observe-action-result +86/-0

Tests for code-based severity classification in observe-action-result

• New tests covering isExpectedEmptyStateCode, code-based severity overrides, and info/warning/error routing through logServerEvent vs logServerError.

src/lib/admin/tests/observe-action-result.test.ts

post-round-trigger.test.tsTests for engine-failure severity classification in postRoundTrigger +83/-0

Tests for engine-failure severity classification in postRoundTrigger

• New tests verifying postRoundTrigger classifies engine_no_recent_rounds/engine_session_expired the same way as the other consumer, plus a regression guard for unrecognized codes.

src/test/coachhelm/v2/post-round-trigger.test.ts

push.test.tsTests for APNs dead-token prune confirmation logging +66/-2

Tests for APNs dead-token prune confirmation logging

• New tests covering the info-level prune log, its absence on non-deactivation paths, and its suppression when the deactivating UPDATE itself errors.

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

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 98 rules

Grey Divider


Informational

1. Misplaced games.ts server action 📘 Rule violation ⌂ Architecture
Description
Multiple server action modules marked with 'use server' (including
src/app/baseball/actions/games.ts, announcements.ts, lifting-v11.ts, watchlist.ts, and
src/app/golf/actions/insights.ts) live outside the required src/app/actions/ directory,
violating the server-action placement convention. This scatters server actions across route groups
and non-standard directories, reducing consistency, discoverability, and increasing maintenance
overhead.
Code

src/app/baseball/actions/games.ts[7]

+import { maybeCaptureRlsDenial } from '@/lib/admin/rls-denial';
Relevance

⭐ Low

Repo uses src/app/{sport}/actions widely; no evidence enforcing src/app/actions-only. PRs keep
pattern.

PR-#547
PR-#282

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist/rule requires that any server action implementation (identified by the 'use server'
directive) be located under src/app/actions/. The cited files are explicitly marked as server
action modules yet reside under src/app/baseball/actions/ and src/app/golf/actions/ instead of
the designated directory, and the PR touches these files (e.g., adding maybeCaptureRlsDenial
imports/usage in several baseball action files and adding/changing return-type fields such as the
new code field in the golf insights action), making the non-compliant placement in-scope for
enforcement.

Rule 1519234: Place Next.js server actions in src/app/actions directory
src/app/baseball/actions/games.ts[1-8]
src/app/baseball/actions/announcements.ts[1-17]
src/app/baseball/actions/lifting-v11.ts[1-68]
src/app/baseball/actions/watchlist.ts[1-14]
src/app/golf/actions/insights.ts[1-1]
src/app/golf/actions/insights.ts[3749-3749]

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

## Issue description
Several files containing server actions (modules marked with `'use server'`) are currently located outside the required `src/app/actions/` directory (e.g., under `src/app/baseball/actions/` and `src/app/golf/actions/`), which violates the server action placement requirement.

## Issue Context
This PR modified these server action files (e.g., added `maybeCaptureRlsDenial` imports/usage in the baseball actions and added/changed a `code?: 'engine_no_recent_rounds' | 'engine_session_expired'` field in the return type in the golf insights action), so they are in scope for compliance enforcement and should be moved/organized to conform to the `src/app/actions/` placement convention.

## Fix Focus Areas
- src/app/baseball/actions/games.ts[1-8]
- src/app/baseball/actions/announcements.ts[1-17]
- src/app/baseball/actions/lifting-v11.ts[1-68]
- src/app/baseball/actions/watchlist.ts[1-14]
- src/app/golf/actions/insights.ts[1-1]
- src/app/golf/actions/insights.ts[3749-3749]

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


2. RLS feature attribution lost 🐞 Bug ◔ Observability
Description
withBaseballAction’s new wrapper-level RLS-denial capture passes table: featureArea and omits
feature, so featureForTable() cannot resolve a FeatureKey and the resulting admin_event can end
up with feature = null, breaking feature-scoped rollups/filters for these denials.
Code

src/lib/baseball/with-baseball-action.ts[R506-517]

+        maybeCaptureRlsDenial(
+          error && typeof error === 'object'
+            ? (error as { code?: string | null; message?: string | null })
+            : null,
+          {
+            table: featureArea,
+            verb: 'rpc',
+            action: name,
+            sport: 'baseball',
+            userId: observedUserId,
+          },
+        );
Relevance

⭐ Low

Wrapper fallback uses table=featureArea and omits feature by design per withBaseballAction
comments/tests.

PR-#787

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrapper uses featureArea as the RLS context table, but maybeCaptureRlsDenial only resolves
feature via featureForTable(table) when ctx.feature is omitted; featureForTable only maps
real DB primary table names, so a feature-area slug maps to null. The logger persists feature from
context.feature ?? context.featureArea, but maybeCaptureRlsDenial doesn’t pass featureArea, so
the event can persist with feature = null.

src/lib/baseball/with-baseball-action.ts[487-517]
src/lib/admin/rls-denial.ts[20-46]
src/lib/admin/feature-registry.ts[1538-1552]
src/lib/server-error-logger.ts[82-92]

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

### Issue description
`withBaseballAction` now calls `maybeCaptureRlsDenial()` in its catch-all path using `table: featureArea` (a slug like `baseball-lifting`). `maybeCaptureRlsDenial` derives the Helm Bridge `feature` from `featureForTable(ctx.table)` when `ctx.feature` is omitted, so this fallback produces RLS-denial events with `feature: null`.

### Issue Context
These wrapper-level events are intended to feed admin error filtering and feature-health rollups, but the current payload can’t be attributed to a feature key when `table` is not a real DB table.

### Fix Focus Areas
- src/lib/baseball/with-baseball-action.ts[487-517]
- src/lib/admin/rls-denial.ts[20-47]
- src/lib/admin/feature-registry.ts[1550-1552]
- src/lib/server-error-logger.ts[82-92]

### Suggested fix
Do one of:
1) Extend `maybeCaptureRlsDenial`’s ctx to accept a free-form `featureArea?: string` and include it in the `logServerEvent` context (so even when `feature` is null, the persisted event has a non-null featureArea/feature fallback).
2) In the wrapper, pass a best-effort `feature` when it is known to be a registry FeatureKey (e.g., runtime-guard against `FEATURE_REGISTRY` keys) and/or pass `featureArea` through so server-error-logger can persist it.

The goal is: wrapper-level RLS events should always be attributable (either to a real FeatureKey or at least to a stable featureArea) rather than landing with `feature: null`.

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


3. DB error details masked 🐞 Bug ◔ Observability
Description
In logSetResult, any upsert error now throws a generic BaseballActionError after
maybeCaptureRlsDenial, so non-RLS PostgREST errors (e.g., 23505) lose their original code/message
in the wrapper’s logServerException capture.
Code

src/app/baseball/actions/lifting-v11.ts[R2032-2047]

+    if (error) {
+      maybeCaptureRlsDenial(error, {
+        table: 'helm_lifting_set_results',
+        verb: 'insert',
+        action: 'logSetResult',
+        feature: 'baseball_lifting',
+        sport: 'baseball',
+        userId: ctx.user.id,
+      });
+      // Rethrow a generic BaseballActionError (not the raw `error`) so the
+      // wrapper's catch-all `maybeCaptureRlsDenial` fallback in
+      // with-baseball-action.ts doesn't see an RLS-shaped error and
+      // double-fire a second, imprecise denial (table: 'baseball-lifting')
+      // on top of the precise one just captured above. Matches
+      // watchlist.ts's addToWatchlist pattern.
+      throw new BaseballActionError();
Relevance

⭐ Low

Generic BaseballActionError rethrow is intentional to prevent RLS double-fire; documented/tested in
recent PRs.

PR-#787
PR-#768

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new logSetResult error branch always throws a generic BaseballActionError, and the wrapper
logs whatever error it receives by normalizing and passing it to logServerException; therefore,
the logged exception no longer contains the original PostgREST error code/message for non-RLS
failures.

src/app/baseball/actions/lifting-v11.ts[2032-2048]
src/lib/baseball/with-baseball-action.ts[519-531]

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

### Issue description
`logSetResult` now throws a generic `BaseballActionError` for *all* upsert failures. This prevents `withBaseballAction` from logging the underlying PostgREST error code/message for non-RLS failures, reducing diagnostic fidelity.

### Issue Context
The change was made to avoid wrapper-level RLS-denial double-capture when the underlying error is RLS-shaped. That rationale applies to 42501/row-level-security cases, but not to other DB errors.

### Fix Focus Areas
- src/app/baseball/actions/lifting-v11.ts[2032-2048]
- src/lib/baseball/with-baseball-action.ts[519-531]
- src/lib/admin/rls-denial.ts[12-18]

### Suggested fix
Split the handling:
- If the error is an RLS denial: keep the precise `maybeCaptureRlsDenial(...)` and then throw a non-RLS-shaped sentinel (or a `BaseballActionError`) to avoid wrapper fallback double-fire.
- If the error is *not* an RLS denial: either throw an `Error` that includes `error.code`/`error.message` (so the wrapper logs meaningful details), or explicitly call `logServerException`/`logServerError` with the original code/message before throwing `BaseballActionError`.

This keeps the anti-double-fire behavior for RLS while restoring actionable logging for real non-RLS DB failures.

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


Grey Divider

Qodo Logo

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

Caution

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

⚠️ Outside diff range comments (2)
src/lib/coachhelm/v2/post-round-trigger.ts (1)

60-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

sanitizeFailureReason still parses the message string for FailureCode even though a stable code is now on the same result.

classifyEngineFailureSeverity (lines 71-78) correctly uses the new code field, but sanitizeFailureReason (lines 80-89), called right below at line 153 with the same reason string, still relies on brittle substring matching (lower.includes('no completed rounds'), lower.includes('session') && lower.includes('expired')) to derive 'engine_no_recent_rounds' / 'engine_session_expired' for the coachhelm_failure_reason DB column. Since result.code already carries these exact literal values, a future edit to the user-facing error message (the very drift this PR's code field was introduced to survive, per the doc comments on EXPECTED_SOFT_FAILURE_CODES) would silently desync the DB-persisted failure reason from the telemetry classification.

♻️ Proposed fix
-function sanitizeFailureReason(reason: string): FailureCode {
+function sanitizeFailureReason(reason: string, code?: string | null): FailureCode {
+  if (code === 'engine_session_expired' || code === 'engine_no_recent_rounds') return code;
   const lower = reason.toLowerCase();
   if (lower.includes('timeout') || lower.includes('timed out')) return 'engine_timeout';
   if (lower.includes('session') && lower.includes('expired')) return 'engine_session_expired';
   if (lower.includes('membership')) return 'engine_membership_missing';
   if (lower.includes('disabled')) return 'engine_disabled';
   if (lower.includes('no completed rounds')) return 'engine_no_recent_rounds';
   if (lower.includes('generator failure') || lower.includes('tier-1')) return 'engine_generator_failure';
   return 'engine_error';
 }

And thread result.code through at the call site (line ~153):

-        coachhelm_failure_reason: sanitizeFailureReason(reason),
+        coachhelm_failure_reason: sanitizeFailureReason(reason, result.code ?? null),
🤖 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/v2/post-round-trigger.ts` around lines 60 - 89, Update
sanitizeFailureReason to accept and prioritize the stable result.code, mapping
recognized codes directly to the corresponding FailureCode instead of parsing
user-facing message text. At its call site in postRoundTrigger, pass result.code
alongside reason, while retaining message-based fallback only when code is null
or unrecognized.
src/app/golf/actions/insights.ts (1)

3837-3860: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reserve engine_session_expired for real session/auth failures

src/app/golf/actions/insights.ts:3838-3859 drops the thrown error and returns code: 'engine_session_expired' for every analyzePlayer failure. That code is treated as an expected soft failure in src/lib/admin/observe-action-result.ts:28-45 and src/lib/coachhelm/v2/post-round-trigger.ts:71-84, so engine bugs and timeouts get downgraded to handled noise and skip Sentry. Bind the error here and only emit that code when the failure is actually session-related; otherwise return the raw failure without the code.

🤖 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/insights.ts` around lines 3837 - 3860, Update the catch
block around coachHelmIntelligence.analyzePlayer to bind the thrown error and
classify it before returning. Emit code 'engine_session_expired' only for
genuine session/auth failures; for all other errors, preserve and return the
original failure details without that code so observability can report engine
bugs and timeouts. Use the analyzePlayer catch path and existing error/session
classification utilities or conventions to implement the distinction.
🤖 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/baseball/`(dashboard)/dashboard/decision-room/__tests__/page.test.tsx:
- Around line 43-54: Extract the duplicated FakeBaseballUnauthorizedError class
into a shared test helper module, preserving its status, default message, and
name. Update each affected page test to import the helper and have the
vi.mock('`@/lib/baseball/with-baseball-action`', ...) factory reference the
imported class, removing the local vi.hoisted boilerplate.
- Around line 27-31: Replace the inline Supabase mock in the decision-room page
test with the shared createFakeSupabase fixture, configuring auth.getUser as
needed while preserving the existing mock behavior. In the successful render
test, replace the weak toBeTruthy assertion with a specific assertion on the
rendered output or the props passed to the returned element.

In
`@src/app/baseball/`(dashboard)/dashboard/players/[id]/scout-packet/preview/page.tsx:
- Around line 37-57: Replace the local BaseballUnauthorizedError catch and
manual login redirect in the page’s getScoutPacketPreview handling with the
shared redirectOnUnauthorized helper, matching its usage elsewhere in the
baseball flow while preserving propagation of unrelated errors.

In `@src/app/baseball/`(dashboard)/dashboard/scout-packets/page.tsx:
- Around line 39-55: Replace the local BaseballUnauthorizedError catch and
redirect logic around getScoutPacketRoster with the shared
redirectOnUnauthorized guard, preserving propagation of non-unauthorized errors
and using the centralized predicate and login URL.

In
`@src/app/baseball/`(dashboard)/dashboard/settings/audit/__tests__/page.test.tsx:
- Around line 30-34: The test uses an ad-hoc Supabase server mock instead of the
shared test client. Update the test setup around the module-level mock to use
the canonical Supabase test client from src/test/, preserving the existing
getUser behavior through that shared client and removing the duplicated
vi.mock('`@/lib/supabase/server`', ...) implementation.

In
`@src/app/baseball/`(dashboard)/dashboard/settings/program/__tests__/page.test.tsx:
- Around line 27-31: The test currently defines a local Supabase server mock
instead of using the shared test client. Replace the
vi.mock('`@/lib/supabase/server`', ...) setup in page.test.tsx with the shared
Supabase test client from src/test/, and configure its auth.getUser behavior
through that client.

In `@src/app/baseball/actions/__tests__/lifting-v11-rls-denial.test.ts`:
- Around line 156-167: Strengthen the non-RLS error test for logSetResult by
asserting the arguments passed to logServerException, not only its call count.
Verify the logged exception or message preserves the original duplicate-key
error details, including code 23505 and its message, while still confirming no
RLS event is emitted.

In `@src/app/baseball/actions/announcements.ts`:
- Around line 156-162: Update all four maybeCaptureRlsDenial calls in
createAnnouncementAction, getAnnouncementsWithMetaImpl,
acknowledgeAnnouncementImpl, and deleteAnnouncementAction to include the
relevant userId: use ctx.user.id for action handlers, user.id in
acknowledgeAnnouncementImpl, and pass the existing _userId parameter through
getAnnouncementsWithMetaImpl. Rename that parameter if needed to reflect its
use.

In `@src/app/baseball/actions/games.ts`:
- Around line 933-942: Update saveFullBoxScoreAction’s error handling to pass
_ctx.user.id as the userId field in the maybeCaptureRlsDenial call, matching the
attribution pattern used by announcements.ts; retain the existing table, verb,
action, and feature metadata.

In `@src/app/baseball/actions/lifting-v11.ts`:
- Around line 2032-2048: In logSetResult, preserve diagnostics for non-RLS
database failures by logging the original error's full Postgrest metadata (code,
message, details, and hint) through the existing server-error logging mechanism
before throwing the sanitized BaseballActionError. Keep maybeCaptureRlsDenial
and the generic rethrow unchanged for RLS handling, and mirror sendMessage's
logging pattern in messages.ts.

In `@src/lib/baseball/redirect-on-unauthorized.ts`:
- Around line 33-41: Validate the decoded `returnTo` value in the login page
before navigating with `router.push()`: allow only internal paths beginning with
`/baseball/`, and fall back to `/baseball/dashboard/command-center` for missing,
malformed, or external values. Update the redirect flow associated with
`redirectOnUnauthorized` to use this allowlisted destination.

---

Outside diff comments:
In `@src/app/golf/actions/insights.ts`:
- Around line 3837-3860: Update the catch block around
coachHelmIntelligence.analyzePlayer to bind the thrown error and classify it
before returning. Emit code 'engine_session_expired' only for genuine
session/auth failures; for all other errors, preserve and return the original
failure details without that code so observability can report engine bugs and
timeouts. Use the analyzePlayer catch path and existing error/session
classification utilities or conventions to implement the distinction.

In `@src/lib/coachhelm/v2/post-round-trigger.ts`:
- Around line 60-89: Update sanitizeFailureReason to accept and prioritize the
stable result.code, mapping recognized codes directly to the corresponding
FailureCode instead of parsing user-facing message text. At its call site in
postRoundTrigger, pass result.code alongside reason, while retaining
message-based fallback only when code is null or unrecognized.
🪄 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: 26ac96a8-ae7f-4f8f-aed2-bed1d012c047

📥 Commits

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

📒 Files selected for processing (44)
  • src/app/actions/messages.ts
  • src/app/baseball/(dashboard)/dashboard/decision-room/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/decision-room/page.tsx
  • src/app/baseball/(dashboard)/dashboard/operations/error.tsx
  • src/app/baseball/(dashboard)/dashboard/performance/builder/error.tsx
  • src/app/baseball/(dashboard)/dashboard/players/[id]/scout-packet/preview/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/players/[id]/scout-packet/preview/page.tsx
  • src/app/baseball/(dashboard)/dashboard/scout-packets/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/scout-packets/page.tsx
  • src/app/baseball/(dashboard)/dashboard/scouting/error.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/audit/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/audit/page.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/imports/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/imports/page.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/integrations/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/integrations/page.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/permissions/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/permissions/page.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/program/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/program/page.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/roles/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/roles/page.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/season/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/season/page.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/teams/__tests__/page.test.tsx
  • src/app/baseball/(dashboard)/dashboard/settings/teams/page.tsx
  • src/app/baseball/actions/__tests__/lifting-v11-rls-denial.test.ts
  • src/app/baseball/actions/announcements.ts
  • src/app/baseball/actions/games.ts
  • src/app/baseball/actions/lifting-v11.ts
  • src/app/baseball/actions/watchlist.ts
  • src/app/golf/actions/insights.ts
  • src/hooks/useAdminRealtime.ts
  • src/lib/admin/__tests__/observe-action-result.test.ts
  • src/lib/admin/observe-action-result.ts
  • src/lib/baseball/__tests__/redirect-on-unauthorized.test.ts
  • src/lib/baseball/__tests__/with-baseball-action-observability.test.ts
  • src/lib/baseball/redirect-on-unauthorized.ts
  • src/lib/baseball/with-baseball-action.ts
  • src/lib/coachhelm/v2/post-round-trigger.ts
  • src/lib/coachhelm/v2/trigger-insights-bridge.ts
  • src/lib/notifications/push.ts
  • src/test/coachhelm/v2/post-round-trigger.test.ts
  • src/test/lib/notifications/push.test.ts

Comment on lines +27 to +31
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(async () => ({
auth: { getUser: mocks.getUser },
})),
}));

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for an existing shared Supabase test client convention.
fd . src/test -e ts -e tsx
rg -n "createClient" src/test -A3 -B3
rg -n "supabase" src/test -il

Repository: njrini99-code/helmv3

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/test/fixtures/fake-supabase.ts ---'
cat -n src/test/fixtures/fake-supabase.ts | sed -n '1,220p'

echo
echo '--- src/app/baseball/(dashboard)/dashboard/decision-room/__tests__/page.test.tsx ---'
cat -n 'src/app/baseball/(dashboard)/dashboard/decision-room/__tests__/page.test.tsx' | sed -n '1,220p'

echo
echo '--- nearby shared-test-client usage in baseball dashboard tests ---'
rg -n "fake-supabase|createFakeSupabase|createClient: vi\.fn\(\s*async|toBeTruthy\(\)" src/app/baseball src/test -g '*.test.ts' -g '*.test.tsx' -A2 -B2

Repository: njrini99-code/helmv3

Length of output: 50377


Use the shared Supabase fixture here

  • Replace the inline createClient mock at src/app/baseball/(dashboard)/dashboard/decision-room/__tests__/page.test.tsx:27-31 with createFakeSupabase from src/test/fixtures/fake-supabase.ts:1-20; this page test only needs auth.getUser(), and the shared fixture is the repo convention.
  • Tighten the success case at src/app/baseball/(dashboard)/dashboard/decision-room/__tests__/page.test.tsx:84-88; toBeTruthy() on the returned element is too weak—assert the rendered output or passed props instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/decision-room/__tests__/page.test.tsx
around lines 27 - 31, Replace the inline Supabase mock in the decision-room page
test with the shared createFakeSupabase fixture, configuring auth.getUser as
needed while preserving the existing mock behavior. In the successful render
test, replace the weak toBeTruthy assertion with a specific assertion on the
rendered output or the props passed to the returned element.

Source: Path instructions

Comment on lines +43 to +54
const { FakeBaseballUnauthorizedError } = vi.hoisted(() => ({
FakeBaseballUnauthorizedError: class FakeBaseballUnauthorizedError extends Error {
readonly status = 401;
constructor(message = 'You must be signed in.') {
super(message);
this.name = 'BaseballUnauthorizedError';
}
},
}));
vi.mock('@/lib/baseball/with-baseball-action', () => ({
BaseballUnauthorizedError: FakeBaseballUnauthorizedError,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated FakeBaseballUnauthorizedError boilerplate into a shared test helper.

This exact ~12-line class + vi.mock('@/lib/baseball/with-baseball-action', ...) block is copy-pasted verbatim across every page test in this cohort (imports, integrations, permissions, roles, season, teams). Since vi.mock factories can safely reference values imported from a separate module (regular ES imports are resolved before module evaluation, unlike local const which hits the hoisting TDZ), this can be centralized.

♻️ Proposed shared helper
// src/test/baseball/fake-unauthorized-error.ts
export class FakeBaseballUnauthorizedError extends Error {
  readonly status = 401;
  constructor(message = 'You must be signed in.') {
    super(message);
    this.name = 'BaseballUnauthorizedError';
  }
}
+import { FakeBaseballUnauthorizedError } from '`@/test/baseball/fake-unauthorized-error`';
-const { FakeBaseballUnauthorizedError } = vi.hoisted(() => ({
-  FakeBaseballUnauthorizedError: class FakeBaseballUnauthorizedError extends Error {
-    readonly status = 401;
-    constructor(message = 'You must be signed in.') {
-      super(message);
-      this.name = 'BaseballUnauthorizedError';
-    }
-  },
-}));
 vi.mock('`@/lib/baseball/with-baseball-action`', () => ({
   BaseballUnauthorizedError: FakeBaseballUnauthorizedError,
 }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/decision-room/__tests__/page.test.tsx
around lines 43 - 54, Extract the duplicated FakeBaseballUnauthorizedError class
into a shared test helper module, preserving its status, default message, and
name. Update each affected page test to import the helper and have the
vi.mock('`@/lib/baseball/with-baseball-action`', ...) factory reference the
imported class, removing the local vi.hoisted boilerplate.

Comment on lines +37 to +57
//
// getScoutPacketPreview independently re-resolves auth (withBaseballAction),
// so a session that expires in the narrow window between the context check
// above and this call throws BaseballUnauthorizedError here. Left uncaught,
// that raw-throws through this Server Component's render straight to
// error.tsx and the error tracker (Sentry/Vercel) instead of the honest
// "please sign in again" redirect — the same class of bug fixed on the
// baseball announcements page. Redirect on that specific case; any other
// thrown error (e.g. a real capability failure for a signed-in coach) is a
// genuine failure and should keep propagating to error.tsx.
let model: Awaited<ReturnType<typeof getScoutPacketPreview>>;
try {
model = await getScoutPacketPreview(id);
} catch (error) {
if (error instanceof BaseballUnauthorizedError) {
redirect(
`/baseball/login?returnTo=${encodeURIComponent(`/baseball/dashboard/players/${id}/scout-packet/preview`)}`,
);
}
throw 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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target page and shared helper.
git ls-files 'src/app/baseball/**/preview/page.tsx' 'src/lib/baseball/**' | sed -n '1,200p'
printf '\n--- helper references ---\n'
rg -n "redirectOnUnauthorized|BaseballUnauthorizedError|getScoutPacketPreview|withBaseballAction" src/app/baseball src/lib/baseball -g '!**/node_modules/**' | sed -n '1,240p'

Repository: njrini99-code/helmv3

Length of output: 32977


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the target page and the helper implementation/call sites.
sed -n '1,220p' src/app/baseball/(dashboard)/dashboard/players/[id]/scout-packet/preview/page.tsx
printf '\n--- helper ---\n'
sed -n '1,220p' src/lib/baseball/redirect-on-unauthorized.ts
printf '\n--- similar pages ---\n'
rg -n "redirectOnUnauthorized\(" src/app/baseball/(dashboard)/dashboard -g 'page.tsx' -g '!**/node_modules/**' | sed -n '1,240p'

Repository: njrini99-code/helmv3

Length of output: 211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- target file ---'
sed -n '1,220p' 'src/app/baseball/(dashboard)/dashboard/players/[id]/scout-packet/preview/page.tsx'

echo
echo '--- helper file candidates ---'
git ls-files 'src/lib/baseball/*redirect*' 'src/lib/baseball/**/redirect*' | sed -n '1,120p'

echo
echo '--- helper definition/calls ---'
rg -n "redirectOnUnauthorized|BaseballUnauthorizedError" src/lib/baseball src/app/baseball -g '!**/node_modules/**' | sed -n '1,240p'

Repository: njrini99-code/helmv3

Length of output: 31817


Use redirectOnUnauthorized here. src/app/baseball/(dashboard)/dashboard/players/[id]/scout-packet/preview/page.tsx:37-57 duplicates the same BaseballUnauthorizedError → login redirect path already used elsewhere in this baseball flow; switch to the shared helper to keep redirect behavior consistent.

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

In
`@src/app/baseball/`(dashboard)/dashboard/players/[id]/scout-packet/preview/page.tsx
around lines 37 - 57, Replace the local BaseballUnauthorizedError catch and
manual login redirect in the page’s getScoutPacketPreview handling with the
shared redirectOnUnauthorized helper, matching its usage elsewhere in the
baseball flow while preserving propagation of unrelated errors.

Comment on lines +39 to +55
// getScoutPacketRoster independently re-resolves auth (withBaseballAction),
// so a session that expires in the narrow window between the checks above
// and this call throws BaseballUnauthorizedError here. Left uncaught, that
// raw-throws through this Server Component's render straight to error.tsx
// and the error tracker (Sentry/Vercel) instead of the honest "please sign
// in again" redirect — the same class of bug fixed on the baseball
// announcements page. Redirect on that specific case; any other thrown
// error (a real failure for a signed-in coach) keeps propagating to error.tsx.
let roster: Awaited<ReturnType<typeof getScoutPacketRoster>>;
try {
roster = await getScoutPacketRoster();
} catch (error) {
if (error instanceof BaseballUnauthorizedError) {
redirect('/baseball/login?returnTo=/baseball/dashboard/scout-packets');
}
throw 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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== target file ==\n'
sed -n '1,220p' 'src/app/baseball/(dashboard)/dashboard/scout-packets/page.tsx'

printf '\n== helper implementation ==\n'
rg -n "redirectOnUnauthorized" src/app src/lib | sed -n '1,120p'

printf '\n== likely related baseball pages ==\n'
rg -n "BaseballUnauthorizedError|redirectOnUnauthorized|returnTo=/baseball/dashboard/scout-packets|returnTo=/baseball/dashboard" src/app/baseball src/lib | sed -n '1,200p'

Repository: njrini99-code/helmv3

Length of output: 34989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' 'src/lib/baseball/redirect-on-unauthorized.ts'
printf '\n== tests ==\n'
sed -n '1,220p' 'src/lib/baseball/__tests__/redirect-on-unauthorized.test.ts'

Repository: njrini99-code/helmv3

Length of output: 5260


Use redirectOnUnauthorized here. src/app/baseball/(dashboard)/dashboard/scout-packets/page.tsx:39-55 duplicates the shared unauthorized redirect guard already used on the other baseball Server Components, so the predicate and login URL stay centralized in one place.

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

In `@src/app/baseball/`(dashboard)/dashboard/scout-packets/page.tsx around lines
39 - 55, Replace the local BaseballUnauthorizedError catch and redirect logic
around getScoutPacketRoster with the shared redirectOnUnauthorized guard,
preserving propagation of non-unauthorized errors and using the centralized
predicate and login URL.

Comment on lines +30 to +34
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(async () => ({
auth: { getUser: mocks.getUser },
})),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inline Supabase mock instead of the shared test client.

Path instructions for test files call for mocking Supabase via the shared test client in src/test/, not an ad-hoc vi.mock('@/lib/supabase/server', ...). This works today but duplicates the mock shape and will drift from the canonical test client as the real createClient() contract evolves.

As per path instructions, "Mock Supabase via the test client in src/test/."

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

In
`@src/app/baseball/`(dashboard)/dashboard/settings/audit/__tests__/page.test.tsx
around lines 30 - 34, The test uses an ad-hoc Supabase server mock instead of
the shared test client. Update the test setup around the module-level mock to
use the canonical Supabase test client from src/test/, preserving the existing
getUser behavior through that shared client and removing the duplicated
vi.mock('`@/lib/supabase/server`', ...) implementation.

Source: Path instructions

Comment on lines +156 to +167
it('a non-RLS error still surfaces as a single unexpected-failure log (no RLS event, no regression to over-suppression)', async () => {
upsertError = { code: '23505', message: 'duplicate key value violates unique constraint' };

await expect(logSetResult(BASE_INPUT)).rejects.toBeInstanceOf(BaseballActionError);

const rlsEvents = mocks.logServerEvent.mock.calls.filter(
(call): call is [string, ...unknown[]] =>
typeof call[0] === 'string' && call[0].startsWith('RLS denial:'),
);
expect(rlsEvents).toHaveLength(0);
expect(mocks.logServerException).toHaveBeenCalledTimes(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 | 🔵 Trivial | ⚡ Quick win

Third test asserts logServerException call count but not its content — won't catch the detail-loss concern raised on lifting-v11.ts:2032-2048.

Since logSetResult now always throws a generic BaseballActionError() regardless of whether the underlying error was RLS-shaped or a genuine constraint violation, this test's assertion (toHaveBeenCalledTimes(1)) can't distinguish "logged with rich context" from "logged with a generic, uninformative message." Consider asserting on the exception object/message passed to logServerException to pin down whether the original error's code/message survives.

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

In `@src/app/baseball/actions/__tests__/lifting-v11-rls-denial.test.ts` around
lines 156 - 167, Strengthen the non-RLS error test for logSetResult by asserting
the arguments passed to logServerException, not only its call count. Verify the
logged exception or message preserves the original duplicate-key error details,
including code 23505 and its message, while still confirming no RLS event is
emitted.

Comment on lines +156 to +162
maybeCaptureRlsDenial(annError, {
table: 'baseball_announcements',
verb: 'insert',
action: 'createAnnouncement',
feature: 'baseball_announcements',
sport: 'baseball',
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

All four maybeCaptureRlsDenial calls omit userId, unlike the equivalent calls in messages.ts/lifting-v11.ts/watchlist.ts.

  • Line 156-162 (createAnnouncementAction): ctx.user.id is available (per BaseballActionContext).
  • Line 420-429 (deleteAnnouncementAction): same, ctx.user.id available.
  • Line 349-355 (acknowledgeAnnouncementImpl): user.id is resolved locally at line 300.
  • Line 205-214 (getAnnouncementsWithMetaImpl): the function already takes a _userId param (currently unused/underscore-prefixed) that could be wired through.

Without userId, the RLS denial: ... on baseball_announcements admin events land with userId: null, making it impossible to correlate a specific coach/player's denial in the admin feed — undercutting the whole point of this telemetry.

🔧 Proposed fix (createAnnouncementAction shown; same pattern applies to the other 3 sites)
       maybeCaptureRlsDenial(annError, {
         table: 'baseball_announcements',
         verb: 'insert',
         action: 'createAnnouncement',
         feature: 'baseball_announcements',
         sport: 'baseball',
+        userId: ctx.user.id,
       });

Also applies to: 205-214, 349-355, 420-429

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

In `@src/app/baseball/actions/announcements.ts` around lines 156 - 162, Update all
four maybeCaptureRlsDenial calls in createAnnouncementAction,
getAnnouncementsWithMetaImpl, acknowledgeAnnouncementImpl, and
deleteAnnouncementAction to include the relevant userId: use ctx.user.id for
action handlers, user.id in acknowledgeAnnouncementImpl, and pass the existing
_userId parameter through getAnnouncementsWithMetaImpl. Rename that parameter if
needed to reflect its use.

Comment on lines +933 to +942
if (error) {
maybeCaptureRlsDenial(error, {
table: 'baseball_games',
verb: 'rpc',
action: 'saveFullBoxScore',
feature: 'baseball_games',
sport: 'baseball',
});
return { success: false, error: sanitizeDbError(error, 'games') };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same userId gap as announcements.ts_ctx.user.id is available but not passed.

saveFullBoxScoreAction's handler signature is (_ctx, gameId, ...); the leading underscore just means the context resolution (requireActiveContext: false) is skipped, not that _ctx.user is unavailable — auth still runs first in withBaseballAction. Wire it through so RLS denial: rpc on baseball_games events are attributable.

     if (error) {
       maybeCaptureRlsDenial(error, {
         table: 'baseball_games',
         verb: 'rpc',
         action: 'saveFullBoxScore',
         feature: 'baseball_games',
         sport: 'baseball',
+        userId: _ctx.user.id,
       });
       return { success: false, error: sanitizeDbError(error, 'games') };
     }
📝 Committable suggestion

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

Suggested change
if (error) {
maybeCaptureRlsDenial(error, {
table: 'baseball_games',
verb: 'rpc',
action: 'saveFullBoxScore',
feature: 'baseball_games',
sport: 'baseball',
});
return { success: false, error: sanitizeDbError(error, 'games') };
}
if (error) {
maybeCaptureRlsDenial(error, {
table: 'baseball_games',
verb: 'rpc',
action: 'saveFullBoxScore',
feature: 'baseball_games',
sport: 'baseball',
userId: _ctx.user.id,
});
return { success: false, error: sanitizeDbError(error, 'games') };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/actions/games.ts` around lines 933 - 942, Update
saveFullBoxScoreAction’s error handling to pass _ctx.user.id as the userId field
in the maybeCaptureRlsDenial call, matching the attribution pattern used by
announcements.ts; retain the existing table, verb, action, and feature metadata.

Comment on lines +2032 to +2048
if (error) {
maybeCaptureRlsDenial(error, {
table: 'helm_lifting_set_results',
verb: 'insert',
action: 'logSetResult',
feature: 'baseball_lifting',
sport: 'baseball',
userId: ctx.user.id,
});
// Rethrow a generic BaseballActionError (not the raw `error`) so the
// wrapper's catch-all `maybeCaptureRlsDenial` fallback in
// with-baseball-action.ts doesn't see an RLS-shaped error and
// double-fire a second, imprecise denial (table: 'baseball-lifting')
// on top of the precise one just captured above. Matches
// watchlist.ts's addToWatchlist pattern.
throw new BaseballActionError();
}

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 | 🔵 Trivial | ⚡ Quick win

Solid double-fire fix, correctly includes userId — but non-RLS errors now lose all diagnostic detail.

The precise maybeCaptureRlsDenial call + generic BaseballActionError() rethrow is well documented and correctly avoids the wrapper's imprecise fallback (verified against with-baseball-action-observability.test.ts and lifting-v11-rls-denial.test.ts). Good.

However, for a non-RLS failure (e.g. the '23505' unique-constraint case exercised in the regression test), maybeCaptureRlsDenial no-ops and the only thing that ultimately reaches logServerException is a fresh BaseballActionError('Something went wrong. Please try again.') — the original Postgrest code/message/details/hint are discarded entirely at this call site. Contrast with messages.ts's sendMessage (lines 141-151), which calls logServerError with the full error metadata before throwing a sanitized message. logSetResult has no equivalent call, so a genuine constraint violation on an upsert becomes an untraceable generic error in Sentry/admin_events.

🩹 Suggested addition (log full detail before throwing, mirroring messages.ts)
     if (error) {
       maybeCaptureRlsDenial(error, {
         table: 'helm_lifting_set_results',
         verb: 'insert',
         action: 'logSetResult',
         feature: 'baseball_lifting',
         sport: 'baseball',
         userId: ctx.user.id,
       });
+      await logServerError(`[logSetResult] Upsert failed: ${error.message}`, {
+        action: 'logSetResult',
+        metadata: { code: error.code, details: error.details, hint: error.hint, userId: ctx.user.id },
+      });
       throw new BaseballActionError();
     }
📝 Committable suggestion

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

Suggested change
if (error) {
maybeCaptureRlsDenial(error, {
table: 'helm_lifting_set_results',
verb: 'insert',
action: 'logSetResult',
feature: 'baseball_lifting',
sport: 'baseball',
userId: ctx.user.id,
});
// Rethrow a generic BaseballActionError (not the raw `error`) so the
// wrapper's catch-all `maybeCaptureRlsDenial` fallback in
// with-baseball-action.ts doesn't see an RLS-shaped error and
// double-fire a second, imprecise denial (table: 'baseball-lifting')
// on top of the precise one just captured above. Matches
// watchlist.ts's addToWatchlist pattern.
throw new BaseballActionError();
}
if (error) {
maybeCaptureRlsDenial(error, {
table: 'helm_lifting_set_results',
verb: 'insert',
action: 'logSetResult',
feature: 'baseball_lifting',
sport: 'baseball',
userId: ctx.user.id,
});
await logServerError(`[logSetResult] Upsert failed: ${error.message}`, {
action: 'logSetResult',
metadata: { code: error.code, details: error.details, hint: error.hint, userId: ctx.user.id },
});
// Rethrow a generic BaseballActionError (not the raw `error`) so the
// wrapper's catch-all `maybeCaptureRlsDenial` fallback in
// with-baseball-action.ts doesn't see an RLS-shaped error and
// double-fire a second, imprecise denial (table: 'baseball-lifting')
// on top of the precise one just captured above. Matches
// watchlist.ts's addToWatchlist pattern.
throw new BaseballActionError();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/actions/lifting-v11.ts` around lines 2032 - 2048, In
logSetResult, preserve diagnostics for non-RLS database failures by logging the
original error's full Postgrest metadata (code, message, details, and hint)
through the existing server-error logging mechanism before throwing the
sanitized BaseballActionError. Keep maybeCaptureRlsDenial and the generic
rethrow unchanged for RLS handling, and mirror sendMessage's logging pattern in
messages.ts.

Comment on lines +33 to +41
try {
return await action();
} catch (error) {
if (isUnauthorized(error)) {
redirect(`/baseball/login?returnTo=${encodeURIComponent(loginReturnTo)}`);
}
throw 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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the login route validates returnTo is a relative internal path.
fd -t f 'page.tsx' src/app/baseball -x rg -nP 'returnTo' {} \;
rg -nP 'returnTo' src/app/baseball --glob '**/login/**' -C3

Repository: njrini99-code/helmv3

Length of output: 5752


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n-- redirect-on-unauthorized.ts --\n'
sed -n '1,120p' src/lib/baseball/redirect-on-unauthorized.ts

printf '\n-- login page --\n'
sed -n '1,140p' src/app/baseball/(auth)/login/page.tsx

printf '\n-- search for any returnTo validation helpers in baseball auth --\n'
rg -n "returnTo|isRelative|startsWith\\('/baseball'\\)|router\\.push\\(" src/app/baseball/(auth) src/lib/baseball -g '!**/*.map'

Repository: njrini99-code/helmv3

Length of output: 2109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n-- login page --\n'
sed -n '1,160p' "src/app/baseball/(auth)/login/page.tsx"

printf '\n-- auth returnTo usage --\n'
rg -n "returnTo|router\\.push|redirect\\(" "src/app/baseball/(auth)" "src/lib/baseball"

Repository: njrini99-code/helmv3

Length of output: 10904


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,180p' "src/app/baseball/(auth)/login/page.tsx"

printf '\n-- returnTo-related auth routes --\n'
rg -n "returnTo|router\\.push|redirect\\(" "src/app/baseball/(auth)" "src/lib/baseball"

Repository: njrini99-code/helmv3

Length of output: 11549


Allowlist returnTo before router.push()
src/app/baseball/(auth)/login/page.tsx:83-96 reads returnTo from the query string and navigates to it directly. Restrict this to internal /baseball/... paths and fall back to /baseball/dashboard/command-center; otherwise this is an open redirect.

🤖 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/redirect-on-unauthorized.ts` around lines 33 - 41, Validate
the decoded `returnTo` value in the login page before navigating with
`router.push()`: allow only internal paths beginning with `/baseball/`, and fall
back to `/baseball/dashboard/command-center` for missing, malformed, or external
values. Update the redirect flow associated with `redirectOnUnauthorized` to use
this allowlisted destination.

… annotate realtime-messages revalidate exception

- push.ts: replace raw fetch with SUPABASE_SERVICE_ROLE_KEY header by
  supabase.functions.invoke on the existing admin client; error body is
  duck-typed from the error context so 410 shouldDeactivateToken pruning
  behavior is unchanged (tests updated to mock functions.invoke).
- messages.ts: nosemgrep on sendMessage matching the repo idiom — the
  messages UI is realtime-subscribed and revalidatePath caused a reload
  loop on every send (documented in-file since the original removal).

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

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

BASE..HEAD is a two-dot tree diff, but scanners run against the PR
merge-ref checkout; a branch lagging main's deletions feeds semgrep
paths that don't exist there ('Invalid scanning root', exit 2). Filter
the diff to on-disk paths so the gate judges findings, not phantoms.

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

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@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/lib/notifications/push.ts (1)

220-244: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Duplicate prune telemetry possible under concurrent sends — comment's "exactly once" guarantee doesn't hold.

The comment at Line 228 states the prune log "fires exactly once per dead token" because of the active: true read-gate at Line 162. But that gate is checked at select time, not enforced atomically at update time — the .update(...) at Line 220 filters only by .eq('token', ...). Two concurrent sendPushNotification calls for the same user could both pass the token-select gate before either write commits, both succeed the update, and both emit the "pruned" log.

Low blast radius (duplicate info-level log, no data corruption), but the fix is trivial: guard the update with .eq('active', true) and only log if a row was actually affected.

🔧 Proposed fix
           // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-          const { error: deactivateError } = await (supabase as any)
+          const { data: deactivatedRows, error: deactivateError } = await (supabase as any)
             .from('device_tokens')
             .update({
               failed_count: (currentToken?.failed_count || 0) + 1,
               ...(shouldDeactivateToken ? { active: false } : {}),
             })
-            .eq('token', deviceToken.token) as { error: { message: string } | null };
+            .eq('token', deviceToken.token)
+            .eq('active', true)
+            .select('token') as { data: unknown[] | null; error: { message: string } | null };

           // Prune confirmation: the row's `active: true` gate on the token
           // read above means a deactivated token is never selected again, so
           // this fires exactly once per dead token. Logged at info (not
           // error/warning) so it surfaces as a routine Sentry message / admin
           // feed entry, not an Error issue competing with real incidents.
-          if (shouldDeactivateToken && !deactivateError) {
+          if (shouldDeactivateToken && !deactivateError && deactivatedRows && deactivatedRows.length > 0) {
🤖 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/notifications/push.ts` around lines 220 - 244, Prevent duplicate
prune telemetry by making deactivation atomic in the device-token update within
sendPushNotification: add an active=true predicate to the existing token update,
request or otherwise inspect the affected-row count, and emit the prune log only
when the update actually affected a row. Update the nearby comment to describe
this atomic update-based guarantee rather than relying solely on the earlier
read gate.
🤖 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/lib/notifications/push.ts`:
- Around line 220-244: Prevent duplicate prune telemetry by making deactivation
atomic in the device-token update within sendPushNotification: add an
active=true predicate to the existing token update, request or otherwise inspect
the affected-row count, and emit the prune log only when the update actually
affected a row. Update the nearby comment to describe this atomic update-based
guarantee rather than relying solely on the earlier read gate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f70c2d1a-99dd-413d-85ea-17a36ad03ad9

📥 Commits

Reviewing files that changed from the base of the PR and between fc48386 and 285508a.

📒 Files selected for processing (4)
  • .github/scripts/changed-files.sh
  • src/app/actions/messages.ts
  • src/lib/notifications/push.ts
  • src/test/lib/notifications/push.test.ts

njrini99-code pushed a commit that referenced this pull request Jul 10, 2026
…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
@njrini99-code njrini99-code merged commit d63ab26 into main Jul 10, 2026
40 checks passed
@njrini99-code njrini99-code deleted the fix/error-sweep branch July 10, 2026 19:51
@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

security Auth, secrets, RLS, PII, webhooks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant