Commit graph

461 commits

Author SHA1 Message Date
079ecec129 Fix draft timer display drifting due to server-client clock skew (#74)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m35s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m23s
🚀 Deploy / 🐳 Build (push) Successful in 1m11s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
## Summary

- The countdown display used `data.expiresAt` (server-side Unix timestamp) subtracted from `client Date.now()`. When the server clock runs ~2-3s ahead of the client (observed in production), the display was inflated by that delta.
- Fixes three linked symptoms: timer starting at 2:02 instead of 2:00, chess-clock increment crediting 1:54 instead of 1:55, and the commissioner's adjust-time-bank dialog disagreeing with the visible countdown.
- Fix: replace all server `expiresAt` timestamps with a client-local expiry from the server-provided `timeRemaining` integer (`Date.now() + timeRemaining * 1000`). The skew then cancels algebraically when the server computes credit.

## Sites changed

- `handleTimerPickStarted` — fires after every pick (the main fix)
- `handleDraftStateSync` — reconnect path
- `useState` initializer in the draft room — initial page load seed from loader data (uses `loaderTimerNow` server timestamp to compute remaining server-side, then re-anchors to client clock)

## Test plan

- [ ] Start a chess-clock draft (2:00 bank, 15s increment) — timer should show exactly **2:00** when a team goes on the clock
- [ ] Pick at 1:40 — new bank should show **1:55** (100 + 15s)
- [ ] Commissioner adjust-time-bank dialog — displayed remaining should match the countdown
- [ ] Force autopick at any displayed time T — new bank should be T + increment
- [ ] Reconnect mid-pick — timer should resume at the correct remaining time

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #74
2026-06-06 07:30:45 +00:00
60aa8a8c9a Merge pull request 'Add idempotent migration for picks_expires_at / picks_started_at' (#73) from fix/migration-picks-expires-at into main
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 3m43s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m19s
🚀 Deploy / 🐳 Build (push) Successful in 29s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
Reviewed-on: #73
2026-06-06 06:49:20 +00:00
Chris Parsons
81bacf421d Add idempotent migration for picks_expires_at / picks_started_at columns
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m34s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m18s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Migration 0116 added these columns to draft_timers but was generated
with an out-of-order timestamp and was never applied to the DB. Migration
0117 re-adds them with IF NOT EXISTS guards so the apply is safe
regardless of the DB's current state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 23:44:49 -07:00
f96c8f5244 Fix draft timer: broadcasts, increments, reconnect sync, overnight pause (#72)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m40s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m26s
🚀 Deploy / 🐳 Build (push) Successful in 1m14s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
## Summary

- **Timer bank broadcasts**: emit `timer-bank-updated` after every pick so all clients immediately see the updated bank instead of waiting for the next `timer-pick-started`
- **Increment accuracy**: capture `pickMadeAt` at route entry (before auth/DB overhead) and use `Math.ceil` so credited seconds always match the client countdown display
- **Race condition fix**: hold `schedulingInProgress` lock for the full timer callback to prevent the recovery interval from scheduling a duplicate timeout mid-pick
- **force-autopick fix**: call `rescheduleTimer` so the next team's clock starts immediately instead of waiting for the old timeout to naturally expire
- **adjust-time-bank fix**: for on-clock teams, shift `picksExpiresAt` by the adjustment and reschedule so the client countdown updates; block adjustments that would reduce the bank to zero
- **New socket events**: `timer-pick-started`, `timer-overnight-paused`, `timer-bank-updated` with full type definitions; removed dead `timer-update` event
- **Reconnect sync**: `draft-state-sync` now includes `expiresAt` for the active timer and `isOvernightPause` state so reconnecting clients see accurate countdown and pause banner immediately without a page reload
- **Room closure countdown**: capture client-side timestamp when draft completes so the "Room closes in X" countdown actually ticks down before the loader revalidates with `draftCompletedAt`
- **Countdown interval**: run at 500ms with `Math.ceil` to prevent skipped seconds under event loop pressure
- **Overnight pause UX**: `canPick` only blocks on commissioner pause — overnight pause freezes the timer but the on-clock player can still pick early
- **Overnight pause refactor**: extract `checkOvernightPause` to `server/overnight-pause-check.ts`, breaking the `timer↔socket` circular import and sharing the timezone cache across both callers with correct eviction
- **PostgreSQL type fix**: cast `varchar` owner ID to `uuid` in `getTeamTimezone` join

## Test plan

- [ ] Manual pick: all clients see bank increment immediately after pick
- [ ] Timeout pick: all clients see bank update (0 → increment); next clock starts within ~1s
- [ ] Force-autopick: next team's clock starts immediately; no "Pick already made" log
- [ ] Force-manual-pick: all clients see bank increment
- [ ] Pause while clock running: countdown freezes on all clients
- [ ] Resume: clock continues from frozen value
- [ ] adjust-time-bank on on-clock team: countdown shifts immediately
- [ ] adjust-time-bank to zero: returns 400 error
- [ ] Reconnect (socket disconnect/connect): countdown resumes for correct team
- [ ] Hard refresh mid-draft: on-clock indicator and countdown correct immediately
- [ ] Draft complete: "Room closes in X" counts down
- [ ] Overnight pause: banner shows, pick buttons still enabled, timer frozen
- [ ] `npm run test:run` — all 158 files / 2351 tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #72
2026-06-06 05:57:46 +00:00
f657293430 claude/sports-templates-conversion-ILWAM (#71)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m32s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m18s
🚀 Deploy / 🐳 Build (push) Successful in 1m10s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #71
2026-06-04 21:31:49 +00:00
a528aecc86 Fix MLB simulator: projected wins drive seeding, perf fix, + surface simulate errors (#65)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m34s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m19s
🚀 Deploy / 🐳 Build (push) Successful in 1m9s
🚀 Deploy / 🚀 Deploy (push) Successful in 13s
## Summary

- **MLB simulator seeding now responds to projected wins**: replaced hardcoded FanGraphs `p_div`/`p_wc` probability draws with Binomial regular-season simulation. Admin-entered projected wins (via Elo) now drive both playoff _qualification_ odds and in-bracket game win probability, not just the latter.
- **Reads current standings mid-season**: `getRegularSeasonStandings` is called so synced `wins`/`gamesPlayed` feed into each simulation iteration's projected final standings.
- **27× seeding performance improvement**: `sampleBinomial` now uses a Box-Muller normal approximation for `n ≥ 30`, dropping the seeding phase from ~243M to ~3M `Math.random()` calls per 50K-sim run (pre-season, 162 remaining games × 30 teams).
- **Missing-standings warning**: when standings exist for some teams but not all recognized ones (partial sync), a `logger.warn` now surfaces which teams are falling back to 0 wins / 162 remaining instead of silently distorting seeding.
- **Cleanup**: `eloToRDif` calls `rawWinRateFromElo` instead of inlining the same formula.
- **Simulate errors surface inline**: moved simulation action from dedicated route to parent page `intent="simulate"` so errors display inline rather than crashing to an error page.

## Test plan

- [ ] `npm run test:run -- mlb-simulator` — 49 tests pass
- [ ] `npm run typecheck` — clean
- [ ] Trigger a simulation from the admin panel for an MLB season with projected wins entered; confirm teams with higher projected wins show meaningfully higher EVs
- [ ] Confirm that running simulate with no projected wins entered (pre-season defaults) still produces a valid bracket
- [ ] Check logs after simulating a season where some teams lack standings rows — confirm warning appears

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #65
2026-06-01 20:50:03 +00:00
ad910d4f40 Fix simulate route crash and surface actual simulation errors (#64)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m20s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m16s
🚀 Deploy / 🐳 Build (push) Successful in 1m8s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
## Problem

Running a simulation on MLB (and any sport where the readiness check fails) showed a confusing React Router 400 error instead of the real failure reason. The cause: the simulate route had only an \`action\` export. When the action **threw** instead of redirecting, React Router tried to render the route via GET to display the error — hit the missing loader — and replaced the real error with its own internal 400.

A secondary issue: the interim fix used \`?simulationError=\` query params, which had three problems identified in code review:
- Stale errors re-appeared when pressing browser Back after fixing the issue
- The param reflected arbitrary text into the UI (social engineering surface)
- The styling didn't match the established \`bg-destructive/15\` error pattern

## Changes

**\`admin.sports-seasons.\$id.simulate.tsx\`** — strip the action; keep a loader-only redirect stub so bookmarked URLs degrade gracefully to the season page.

**\`admin.sports-seasons.\$id.tsx\`** — add \`intent="simulate"\` to the action dispatcher (matching \`delete\`, \`rescore\`, \`sync-standings\`, \`finalize-standings\`). On success: redirect to expected-values (unchanged). On failure: return \`{ simulateError: message }\` displayed inline with the correct \`bg-destructive/15\` styling.

## Test plan

- [ ] Click Run Simulation on a season with a missing readiness input → error message appears inline below the description text, styled consistently with other errors on the page
- [ ] Click Run Simulation on a fully-configured season → redirects to expected-values as before
- [ ] Navigate directly to \`/admin/sports-seasons/<id>/simulate\` in browser → redirects to season page (no 400)
- [ ] Run simulation, see error, fix the issue, run again successfully → no stale error message persists

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #64
2026-06-01 06:47:30 +00:00
Chris Parsons
ac22ff3a12 Fix Docker build: copy root server.ts into build-env stage
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m28s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m23s
🚀 Deploy / 🐳 Build (push) Successful in 2m38s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
The build:server script (esbuild) expects server.ts at the working
directory root, but the Dockerfile only copied the server/ directory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:56:17 -07:00
c0503a91a6 Merge pull request 'Optimize CI pipeline: merge check jobs, remove docker install, improve layer caching' (#63) from ci/optimize-pipeline-further into main
Some checks failed
🚀 Deploy / 🧪 Test (push) Successful in 2m27s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m26s
🚀 Deploy / 🐳 Build (push) Failing after 1m43s
🚀 Deploy / 🚀 Deploy (push) Has been skipped
Reviewed-on: #63
2026-06-01 04:42:13 +00:00
Chris Parsons
36db8f73ca Optimize CI pipeline: merge check jobs, remove docker install, improve layer caching
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m26s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Merge typecheck + lint into a single `check` job to eliminate one full
  job lifecycle and one npm ci invocation (~2-4 min savings)
- Remove `apt-get install docker.io` step — Docker CLI and buildx v0.23.0
  are now pre-installed in the custom brackt-runner:latest image
- Reorder Dockerfile build-env COPY layers so node_modules and config
  files are copied before source, improving registry cache hit granularity
- Expand .dockerignore to exclude .forgejo, .git, docs, plans, cypress,
  logs, .env files, and tsbuildinfo files from the build context

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:37:01 -07:00
add196902b Fix MLB standings 406 error and refactor ESPN adapter shared code (#62)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m21s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m18s
🚀 Deploy / 🔍 Lint (push) Successful in 49s
🚀 Deploy / 🐳 Build (push) Successful in 12m18s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
## Summary

- **Root cause**: `statsapi.mlb.com` returns 406 (deprecated). Switched MLB to ESPN's free standings API (`site.api.espn.com/apis/v2/sports/baseball/mlb/standings`), consistent with NBA, WNBA, and MLS.
- **Refactor**: Extracted a shared `espn.ts` utility module, eliminating 4× duplication of `statsMap()`, `flattenEspnStandings()`, ESPN interfaces, and the `playoffSeed` → conference rank logic across adapters.
- **Bug fixes** found during review and applied across all affected adapters:
  - `parseConferenceRank`: `|| undefined` falsy-zero bug replaced with `isNaN` guard
  - Sort comparators: stable alphabetical tiebreaker added to MLB, NBA, WNBA
  - `winPct`: falls back to `wins/(wins+losses)` if ESPN omits the stat (was silently 0)
  - statsMap pre-built once per entry before sorting in all adapters (was rebuilt per comparison)
  - WNBA `parseEntry`: accepts pre-computed `sm` instead of rebuilding it internally
  - MLB `gamesBack`: tests updated to reflect ESPN returns numeric `0` for division leaders (old API used `"-"` → `undefined`)

## Test plan

- [ ] All 60 standings-sync unit tests pass (`npm run test:run -- app/services/standings-sync`)
- [ ] Trigger MLB standings sync from admin panel and confirm it returns data without a 406
- [ ] Confirm NBA, WNBA, MLS syncs still work (adapters touched but behaviour unchanged)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #62
2026-06-01 03:31:18 +00:00
03726f1a66 Fix simulator correctness bugs and reduce test iteration counts (#61)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m27s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m18s
🚀 Deploy / 🔍 Lint (push) Successful in 47s
🚀 Deploy / 🐳 Build (push) Successful in 15m5s
🚀 Deploy / 🚀 Deploy (push) Successful in 32s
## Summary

- **Bug fix**: EPL Elo/odds fallback was gated on \`dbEloMap.size === 0\` — if any team had a direct \`sourceElo\`, teams with only \`sourceOdds\` were never populated and the simulator threw "Missing Elo" instead of using their odds data
- **Iterations**: WorldCupSimulator default 50k→10k; manifest \`defaultConfig\` for \`world_cup\` and \`epl_standings\` now explicitly override \`BASE_CONFIG\` with 10k so the production fallback matches; test iterations reduced (WorldCup 500→100, EPL 10k→500) to fix timeout failures on slower CI
- **Cleanup**: Extracted \`runKnockoutRound\` out of the 10k-iteration simulation loop; replaced two hot-loop \`toSorted()\` calls on 2-element arrays with a conditional string comparison; removed local \`normalizeProbabilities\` in favour of the shared \`normalizeSimulationResultColumns\`

## Test plan

- [ ] \`npm run test:run\` — all simulator unit tests pass without timeout
- [ ] Typecheck passes (\`npm run typecheck\`)
- [ ] EPL simulate with mixed Elo+odds data no longer throws

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #61
2026-05-31 17:39:43 +00:00
10f23839cb Remove redundant flaky world-cup-simulator test (#60)
Some checks failed
🚀 Deploy / 🧪 Test (push) Failing after 2m31s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m17s
🚀 Deploy / 🔍 Lint (push) Successful in 48s
🚀 Deploy / 🐳 Build (push) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Has been skipped
## Summary

- Removes the "probabilities are all non-negative" test from \`world-cup-simulator.test.ts\`, which was timing out intermittently in CI (5s budget, 500 Monte Carlo iterations × 48 teams)
- The test is redundant: the "column sums ≈1.0" test immediately above it already catches negative probabilities (a negative value in one column forces a sibling above 1.0, which \`toBeCloseTo(1.0, 2)\` would fail)
- Non-negative assertions for meaningful edge cases are also covered by the "SF losers land in 3rd or 4th" test

## Test plan

- [ ] \`npm run test:run\` passes with no failures

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #60
2026-05-31 17:07:59 +00:00
08474631c1 Optimize CI build pipeline (~15 min → ~3-5 min) (#59)
Some checks failed
🚀 Deploy / 🧪 Test (push) Failing after 1m34s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m23s
🚀 Deploy / 🔍 Lint (push) Successful in 49s
🚀 Deploy / 🐳 Build (push) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Has been skipped
## Summary

- **Dockerfile layer caching**: `development-dependencies-env` now copies only `package.json`/`package-lock.json` before `npm ci` (was `COPY . /app`), so the npm install layer is cached on every code-only commit instead of rebuilt from scratch
- **Skip QEMU**: `platforms: linux/amd64` added to `build-push-action`, cutting `setup-buildx-action` from ~5 min to ~30 sec
- **npm cache in CI jobs**: Manual `actions/cache@v3` blocks (copy-pasted 3×) replaced with `actions/setup-node@v4` + `cache: 'npm'`, which handles path/key/restore automatically and pins Node 20 explicitly
- **Harden `.npmrc`**: Switched from `COPY .npmrc` to `--mount=type=secret,id=npmrc` in both `npm ci` stages — the file is available during install but never written into a Docker layer, so it cannot leak through the registry build cache regardless of future contents

## Expected timing

| Step | Before | After (code change) |
|---|---|---|
| Setup buildx | ~5 min | ~30 sec |
| Build + push | ~5 min | ~1-2 min |
| Deploy (docker pull) | ~5 min | ~1-2 min |
| **Total** | **~15 min** | **~3-5 min** |

## Test plan

- [ ] Push a code-only commit to main — confirm `setup-buildx-action` logs ~30s (no QEMU), Docker build shows `CACHED` for npm install layers, build+push completes in ~1-2 min
- [ ] Check deploy job — `docker compose pull` should show most layers as `Already exists`
- [ ] Push a commit that changes `package.json` — confirm npm layer correctly re-runs (not cached)
- [ ] Confirm deployed app is functional

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #59
2026-05-31 04:35:05 +00:00
ef29c1fcd9 claude/discord-pick-notification-refactor-FeusJ (#58)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m36s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m23s
🚀 Deploy / 🔍 Lint (push) Successful in 50s
🚀 Deploy / 🐳 Build (push) Successful in 14m19s
🚀 Deploy / 🚀 Deploy (push) Successful in 13s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #58
2026-05-29 16:24:22 +00:00
4a2f542fb1 Fix Discord pick notifications silently dropped during autodraft chains (#57)
All checks were successful
🚀 Deploy / 🔍 Lint (push) Successful in 50s
🚀 Deploy / 🐳 Build (push) Successful in 14m19s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Deploy / 🧪 Test (push) Successful in 1m31s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m21s
## Summary

- **Per-league queue**: replaced the single global notification queue with a \`Map<leagueId, queue>\` so concurrent drafts in different leagues drain independently and don't block each other
- **Deadlock fix**: wrapped the drainer loop in \`try/finally\` so \`drainingLeagues\` is always cleaned up even if a task throws unexpectedly — previously a crash would leave the flag set permanently, silently dropping all future notifications for that league
- **Wait cap fix**: moved \`Math.min\` to wrap \`(retryAfter * (attempt+1))\` so the 10s ceiling applies to the final product, not just the base value — settings actions that directly await webhook calls were previously blockable for up to 30s on a rate-limited third attempt

## Root cause of the original problem

During an autodraft chain, \`notifyPickMadeOnDiscord\` was called fire-and-forget for each pick in the chain, causing all webhook requests to fly concurrently. Discord's per-webhook rate limit (~5 req/2s) rejected most of them; the single retry logic fired for all simultaneously, hit the limit again, and the \`.catch()\` swallowed the errors silently.

## Test plan

- [ ] Enable autodraft for 5+ consecutive teams; trigger the chain; confirm all picks announce in Discord in order
- [ ] Confirm a slow/rate-limited webhook in League A does not delay League B's announcements
- [ ] \`npm run test:run\` — 158 test files, 2336 tests, all pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #57
2026-05-27 16:41:08 +00:00
8a40953019 Fix unhandled rejection from session ping when network is unavailable (#56)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m34s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m23s
🚀 Deploy / 🔍 Lint (push) Successful in 50s
🚀 Deploy / 🐳 Build (push) Successful in 14m52s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
## Summary

- The 15-minute session keep-alive interval in `useDraftAuthRecovery` had no `try-catch`. When `authClient.getSession()` is called while the network is down (e.g. a laptop waking from sleep), `better-fetch` throws a `TypeError: Failed to fetch` — producing an unhandled promise rejection that was reported in Sentry.
- Fixed by wrapping the interval callback in a `try-catch`. Network-level `TypeError`s are silently swallowed because real session expiry (HTTP 401) is returned in-band as `{ data: null }` by `better-fetch` and already caught by the `!session` check.
- Improved the comment to explain the mechanism rather than citing socket offline detection (which doesn't validate auth).
- Added 3 tests covering: network throw (no authDegraded), null session response (authDegraded set), valid session (no change).

## Root cause

User left the draft page open overnight. The 15-minute ping fired repeatedly. At some point the computer woke from sleep with the network not yet ready — `getSession()` threw, `setInterval` dropped the rejected promise, Sentry caught it as an unhandled rejection. Confirmed via Sentry breadcrumbs: last activity May 26 8:32 PM, error fired May 27 12:23 PM (~28 hours later).

## Test plan

- [x] 3 new tests pass
- [x] typecheck clean
- [ ] After deploy: monitor Sentry for recurrence

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #56
2026-05-27 16:14:14 +00:00
a9d4954242 Fix World Cup group stage display and upcoming events (#55)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m28s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m17s
🚀 Deploy / 🔍 Lint (push) Successful in 49s
🚀 Deploy / 🐳 Build (push) Successful in 14m50s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
## Summary

- **Group stage match list**: Removed MD 1/2/3 headings; matches now listed chronologically under date separators (e.g. "Jun 14"). Kick-off time appears below the team names instead of between them.
- **Sort order**: \`findMatchesByGroupIds\` / \`findMatchesByEventId\` now order by \`scheduledAt ASC NULLS LAST\` so unscheduled matches always trail scheduled ones.
- **Groups/Bracket toggle**: When both group stage and knockout bracket exist, a toggle appears (mirroring the NBA/AFL standings toggle). Sports with both regular-season standings and a group stage get all views available.
- **Upcoming events**: \`getUpcomingEventsForDraftedParticipants\` now also queries \`groupStageMatches\`, so World Cup group fixtures appear on the home page calendar sorted by kick-off time with a label like "Group A — France vs Germany".
- **\`isAllCompete\` fix**: \`UpcomingEventsCard\` and \`UpcomingCalendarPanel\` now treat \`group_stage_match\` as a bracket-style event, showing team badges instead of "N of your picks".

Fixes #53

## Test plan

- [ ] Navigate to a World Cup sports season page — groups should be listed chronologically under date headers with no MD labels; kick-off time should appear below each match row
- [ ] Once the knockout bracket has matches, confirm the Groups/Bracket toggle appears and both views render correctly
- [ ] On the home page, a user with drafted World Cup participants should see upcoming group fixtures in the calendar panel with individual team badges (not a participant count)
- [ ] NBA/AFL/NHL pages unaffected — their standings/playoffs/finished toggle still works normally
- [ ] \`npm run typecheck\` and \`npm run test:run\` pass

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #55
2026-05-27 05:43:08 +00:00
a02f603575 Fix slow bracket save and stale projected points (#54)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m31s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m20s
🚀 Deploy / 🔍 Lint (push) Successful in 51s
🚀 Deploy / 🐳 Build (push) Successful in 15m9s
🚀 Deploy / 🚀 Deploy (push) Successful in 25s
## Summary
- Swap side-effect order so `updateProbabilitiesAfterResult` runs before `recalculateAffectedLeagues` in `processMatchResult`, `processPlayoffEvent`, and the `set-round-winners` route — projected points now reflect the new result immediately instead of waiting on the next simulation.
- Eliminate the N+1 EV lookup in `calculateTeamProjectedScore` by pre-fetching every EV the season needs in a single `inArray` query inside `recalculateStandings` and passing the map down.
- Add `skipProbabilities` option to `processPlayoffEvent`, used from `autoCompleteRoundIfDone` (callers have already refreshed EVs), avoiding a redundant pass on round-completing saves.

Fixes #31

## Test plan
- [x] `npm run typecheck`
- [x] `npx vitest run app/models/__tests__/team-projected-score.test.ts app/models/__tests__/process-match-result.test.ts app/services/__tests__/probability-updater.test.ts` — 57/57 passing
- [ ] Manual: save a round of bracket winners on a sports season with at least one fantasy league and confirm (a) the request returns faster and (b) the team standings show projected points updated to reflect the result without re-running the simulator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #54
2026-05-27 04:28:10 +00:00
b4c224b368 Merge pull request 'Add robots.txt and sitemap for SEO' (#52) from feat/sitemap-robots into main
Some checks failed
🚀 Deploy / 🧪 Test (push) Failing after 1m33s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m21s
🚀 Deploy / 🔍 Lint (push) Successful in 49s
🚀 Deploy / 🐳 Build (push) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Has been skipped
Reviewed-on: #52
2026-05-25 05:21:25 +00:00
Chris Parsons
8e73296067 Fix robots.txt and sitemap issues from code review
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m32s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m23s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 49s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Point Sitemap directive at /sitemap.xml (not .gz) so crawlers receive
  plain XML without needing Content-Encoding headers
- Add Disallow: /leagues/ to block all auth-gated league routes
- Gitignore public/sitemap.xml* so they are always generated fresh at
  build time rather than serving a stale committed snapshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:16:49 -07:00
Chris Parsons
2978ba787d Add robots.txt and sitemap.xml.gz for SEO
Adds a static robots.txt blocking admin/API/auth routes, a sitemap
generation script (scripts/generate-sitemap.mjs) that writes both
sitemap.xml and sitemap.xml.gz to public/, and hooks it into the
build so the sitemap is always fresh on deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:05:37 -07:00
874a2db853 Merge pull request 'Fix Discord pick announcements arriving out of order and add 429 retry' (#51) from fix/discord-pick-announcement-ordering into main
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m27s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m18s
🚀 Deploy / 🔍 Lint (push) Successful in 47s
🚀 Deploy / 🐳 Build (push) Successful in 14m54s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
Reviewed-on: #51
2026-05-25 04:34:47 +00:00
Chris Parsons
77f520823c Fix Discord pick announcements arriving out of order and add 429 retry
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m30s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Move notifyPickMadeOnDiscord() before checkAndTriggerNextAutodraft() in
  both executeAutoPick() and the manual pick action. Previously the
  triggering pick's announcement fired after all chained autodraft picks
  had already announced, causing Discord to show e.g. pick #106 before
  #105 before #104. Now each pick announces itself before the next pick
  in the chain is triggered.

- Add a single retry with Retry-After delay in sendDiscordWebhook() on
  HTTP 429. A burst of consecutive autodraft picks could hit Discord's
  per-webhook rate limit and silently drop announcements. Use || 1
  (not ?? 1) to handle non-numeric/NaN headers, and cap at 10 s so
  that awaited callers such as the draft-order settings actions are
  never hung for a full global rate-limit window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:29:02 -07:00
425b4724d2 Fix autodraft settings panel not reflecting auto-disable after pick (#50)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m21s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m13s
🚀 Deploy / 🔍 Lint (push) Successful in 46s
🚀 Deploy / 🐳 Build (push) Successful in 14m31s
🚀 Deploy / 🚀 Deploy (push) Successful in 9s
2026-05-24 01:35:13 +00:00
Chris Parsons
e3a7a1a9b1 Fix autodraft settings panel not reflecting auto-disable after pick
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m24s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m13s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 45s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
When next_queue autodraft fires and turns itself off, two bugs prevented
the UI from behaving correctly:

1. After reconnect, useDraftAuthRecovery synced autodraftStatus (draft
   grid) but not userAutodraft (settings panel), leaving the settings
   badge stale until the next page load.

2. Toast notifications for autodraft auto-disable were unreliable: the
   condition checked prev.mode rather than why the server disabled it,
   causing the wrong toast for manual disables and queue-empty events.

Fix the reconnect sync by calling setUserAutodraft in the revalidation
effect alongside setAutodraftStatus. Fix the toast logic by adding a
reason field ("pick_complete" | "queue_empty") to the server's
autodraft-updated emit, so the client can show the right message without
guessing from local state. Also removes the now-unused userAutodraftRef.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 18:29:13 -07:00
3518bd77e8 Merge pull request 'Enforce unique team names within a season' (#49) from fix/team-name-uniqueness into main
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m26s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m16s
🚀 Deploy / 🔍 Lint (push) Successful in 48s
🚀 Deploy / 🐳 Build (push) Successful in 14m33s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
Reviewed-on: #49
2026-05-24 00:07:28 +00:00
Chris Parsons
c9fdb12a09 Enforce unique team names within a season
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m31s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m19s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Add findTeamByNameInSeason (exact case-insensitive match via lower()) to the team model
- Validate uniqueness in the user-facing team settings rename action
- Validate uniqueness in admin assign-owner and remove-owner paths that auto-generate names
- Reject whitespace-only team names that would trim to an empty string
- Add DB-level unique index on (season_id, lower(name)) to close the TOCTOU race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:55:08 -07:00
79e7591dc7 Merge pull request 'Fix invite link showing http and add draft board link during live draft' (#48) from fix/invite-link-https-draft-board-link into main
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m23s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m15s
🚀 Deploy / 🔍 Lint (push) Successful in 46s
🚀 Deploy / 🐳 Build (push) Successful in 14m25s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
Reviewed-on: #48
2026-05-23 05:36:33 +00:00
Chris Parsons
3f9c1d99e2 Fix invite link showing http and show draft board card during live draft
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m25s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m14s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Use x-forwarded-proto header (allowlisted to http/https only) when
  building the invite link origin so it shows https behind a TLS proxy
- Show the "View Draft Board" bare card in the right column while a
  draft is running, matching the card already shown for active/completed
  seasons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:31:52 -07:00
4fa927e66b Merge pull request 'Fix Discord double-ping when autodraft fires after manual pick' (#47) from fix/discord-autodraft-double-ping into main
All checks were successful
🚀 Deploy / ʦ TypeScript (push) Successful in 1m15s
🚀 Deploy / 🔍 Lint (push) Successful in 48s
🚀 Deploy / 🧪 Test (push) Successful in 1m24s
🚀 Deploy / 🐳 Build (push) Successful in 14m25s
🚀 Deploy / 🚀 Deploy (push) Successful in 15s
Reviewed-on: #47
2026-05-23 03:46:48 +00:00
Chris Parsons
1b51cb554b Fix Discord double-ping when autodraft fires immediately after manual pick
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m33s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 47s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
When a manual pick triggered an immediate autodraft chain, both Discord
announcements re-read currentPickNumber from the DB, which had already
advanced past the chained pick. Both messages showed the same "On the
clock" person, pinging them twice.

Fix: pass nextPickNumber (the pick immediately following each specific
pick, before any chain) as an explicit param to notifyPickMadeOnDiscord
instead of re-reading from the DB. Also removes a now-unnecessary DB
round-trip on every pick announcement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:38:35 -07:00
841760541d Fix snake draft Discord pick numbering and small UX tweaks (#2)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m27s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m16s
🚀 Deploy / 🔍 Lint (push) Successful in 1m55s
🚀 Deploy / 🐳 Build (push) Successful in 14m23s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
## Summary

- **Discord snake draft fix**: pick notifications now show the sequential pick-in-round (e.g. "Round 2, Pick 9") rather than the snake-adjusted slot position ("Round 2, Pick 5"). Also removes the now-unused \`pickInRound\` param from \`notifyPickMadeOnDiscord\` and adds a regression test for the 13-team case.
- **League card**: league name in draft-in-progress cards is now a link to the league homepage (the Enter Draft button still goes to the draft room).
- **Overnight pause label**: shortened to "🌙 Pause" — the "Resumes 4:00 AM" line below already provides context, so "Overnight" was just causing wrapping.
- **Queue & Picks backgrounds**: items use \`bg-card\` instead of \`bg-muted\` for better visual separation from the panel background.

## Test plan

- [x] Discord: in a snake draft, verify pick #22 of 13 shows "Round 2, Pick 9" in Discord
- [x] League card: dashboard with a draft-in-progress league — click name → league homepage, click Enter Draft → draft room
- [x] Overnight pause: pause a draft overnight — cell shows "🌙 Pause" on one line, "Resumes X:XX" below
- [x] Queue/Picks: open draft room and confirm queue items and recent picks visually pop from the sidebar background
- [x] Unit tests: `npm run test:run` passes (13 discord tests)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #2
2026-05-22 04:18:11 +00:00
Chris Parsons
4f9f9b5c6f Install Docker CLI in build job
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m33s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m23s
🚀 Deploy / 🔍 Lint (push) Successful in 49s
🚀 Deploy / 🐳 Build (push) Successful in 15m5s
🚀 Deploy / 🚀 Deploy (push) Successful in 31s
node:20-bookworm doesn't include the Docker CLI; install docker.io via
apt before the buildx and registry steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:05:34 -07:00
85b3317f9a Merge pull request 'Migrate CI/CD from GitHub Actions to Forgejo Actions' (#1) from chore/forgejo-actions into main
Some checks failed
🚀 Deploy / 🧪 Test (push) Successful in 1m29s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m21s
🚀 Deploy / 🔍 Lint (push) Successful in 48s
🚀 Deploy / 🐳 Build (push) Has been cancelled
🚀 Deploy / 🚀 Deploy (push) Has been cancelled
Reviewed-on: #1
2026-05-22 01:39:03 +00:00
Chris Parsons
b9a1ea2cb4 Remove setup-node from CI jobs
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m26s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m17s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Node 20 is already in the node:20-bookworm runner image; setup-node
was hanging ~5min trying to reach an unconfigured npm cache service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 17:04:12 -07:00
Chris Parsons
2192f1790d Add oxlint config and fix unicorn/no-array-sort violation
Some checks failed
🚀 Deploy / ʦ TypeScript (pull_request) Has been cancelled
🚀 Deploy / 🔍 Lint (pull_request) Has been cancelled
🚀 Deploy / 🐳 Build (pull_request) Has been cancelled
🚀 Deploy / 🚀 Deploy (pull_request) Has been cancelled
🚀 Deploy / 🧪 Test (pull_request) Has been cancelled
Promote unicorn/no-array-sort to error in oxlint.json and replace
[...teams].sort() with teams.toSorted() in discord.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 17:01:02 -07:00
Chris Parsons
0b8f7edc08 Fix postgres service for Forgejo Docker runner
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Failing after 21m9s
🚀 Deploy / ʦ TypeScript (pull_request) Failing after 11m24s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 10m2s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Remove host port binding (conflicts with local postgres on runner) and
use service name hostname instead of localhost for DATABASE_URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 16:14:27 -07:00
Chris Parsons
9ac7fb5b31 Migrate CI/CD from GitHub Actions to Forgejo Actions
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Failing after 2m57s
🚀 Deploy / ʦ TypeScript (pull_request) Failing after 10m55s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 10m36s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Move workflows from .github/ to .forgejo/ so they only run on Forgejo
- Replace styfle/cancel-workflow-action with native concurrency block
- Use full GitHub URLs for all third-party actions to bypass mirror
- Switch Docker layer cache from type=gha to type=registry
- Drop label-subissues workflow (relies on GitHub-specific GraphQL API)
- Add timeout-minutes to all jobs; cancel-in-progress only for non-main branches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 15:52:00 -07:00
Chris Parsons
fbcecb490e
Add Discord draft pick announcements (#460)
* Add Discord draft pick announcements

Posts a message to the league Discord webhook each time a pick is made, announcing the picked participant and pinging the next team on the clock if their owner has opted into Discord notifications. Enabled via a separate toggle in league settings (independent from standings update notifications).

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

* Address code review feedback on Discord pick announcements

- Fix "on the clock" timing: read currentPickNumber fresh from DB post-autodraft-chain (matches sendOnTheClockEmail guarantee)
- Remove outer try/catch from notifyPickMadeOnDiscord so callers' .catch() is not dead code
- Add missing draft-discord.server.test.ts with 12 tests covering all early-exit and happy paths
- Fix silent empty-string fallback for missing pickedSlot: warn and skip instead
- Eliminate sequential season→league DB queries by accepting leagueId as a direct param
- Show "save webhook URL to configure options" hint when URL is typed but not yet saved
- Remove block-scope braces at both call sites (plain const declarations)
- Remove redundant "Round N, Pick M" description line (title already carries this info)
- Inline pickInRoundFor helper to avoid circular import with draft-utils

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

* Fix lint errors from review fixes

- Remove unused logger import (no longer needed after removing try/catch)
- Remove unused OWNER_ID constant in test fixture
- Use toSorted() instead of sort() in sendDraftOrderNotification

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 19:55:48 -07:00
Chris Parsons
b8b58ec9f8
Add draft email notifications feature (#459)
* Add on-the-clock email notifications and fix push notification modal

- Add `draft_email_notifications_enabled` column to users table
- New `sendOnTheClockEmail` service sends an email when a user's turn arrives
  in a draft; detects back-to-back picks and sends one combined email instead
  of two; skips notification if the team has autodraft enabled
- Email is triggered after every manual pick (make-pick route) and every
  timer-expiry pick (executeAutoPick) once the full autodraft chain settles,
  so the notified user is always the first human on the clock
- Add email notification toggle to the user settings Notifications section
- Fix push notification dropdown in the draft room: the bare absolute div is
  replaced with a Radix Popover so clicking outside dismisses it without
  toggling notifications off
- Rename draft room label from "Pick Notifications" to "Push Notifications"
  to distinguish from the new email notifications

https://claude.ai/code/session_01LMGxgYvtE3CF8Jf3u2pXgA

* Address code review feedback on email notification feature

- sendOnTheClockEmail now fetches season (and currentPickNumber) internally,
  removing the blocking postChainSeason pre-fetch from both make-pick.ts and
  executeAutoPick; callers are now true fire-and-forget with no IIFE needed
- Parallel Promise.all for team, autodraftSettings, and league queries reduces
  sequential DB round-trips from 5 to 3
- Remove team.ownerId type cast; assign to local after the null guard
- Combine the two calculatePickInfo calls for the same pick into one
- Fix popoverOpen/enabled divergence: remove the {enabled && ...} guard on
  PopoverContent so popoverOpen is the sole visibility control
- Unify NotificationsSection feedback pattern: both Discord and email sections
  now read success/error directly from their respective fetcher data via a
  shared feedbackFromFetcher helper; removes the success/error props and the
  verbose cast that was used for email
- Add 13 unit tests for sendOnTheClockEmail covering: early-exit paths
  (no season, past totalPicks, no owner, notifications disabled, autodraft
  enabled, no league), single vs back-to-back subject selection, draft room
  link presence, error logging without throwing, and parallel query execution

https://claude.ai/code/session_01LMGxgYvtE3CF8Jf3u2pXgA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 15:35:43 -07:00
Chris Parsons
7809864674
Add Discord account linking from settings page (#457)
* Add Discord account linking from settings page

Adds a "Connect Discord" button in the Account settings section that
triggers BetterAuth's linkSocial flow to attach a Discord account to
an existing user without requiring a sign-out/sign-in. After the OAuth
callback, the ?section= URL param returns the user to the Account tab.
The Notifications toggle was already gated on Discord being linked.

https://claude.ai/code/session_01UerTpTuBWjreTNEns8srkp

* Fix Discord linking error handling and section routing issues

- Reset linkingDiscord state and show error message when linkSocial throws,
  so users can retry if the OAuth flow fails
- Move VALID_SECTION_IDS to module level and derive it from SECTIONS to
  avoid per-render allocation and prevent the two from drifting out of sync
- Remove premature SectionId cast before the allowlist validation in
  the ?section= URL param handling
- Add tests for loading state and error recovery in AccountSection

Fixes #455

https://claude.ai/code/session_01UerTpTuBWjreTNEns8srkp

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 13:38:44 -07:00
Chris Parsons
80973d3212
Fix draft Summary tab showing wrong avatar for teams without custom logos (#453)
DraftSummaryView was not declaring or forwarding ownerAvatarData, so
TeamAvatar fell back to a generated flag instead of the owner's avatar.
DraftGrid and DraftGridSection already threaded ownerAvatarData correctly;
this makes the Summary tab consistent. No route changes needed — the loader
already enriches draftSlots with ownerAvatarData.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 00:02:19 -07:00
Chris Parsons
ea125e0d5e
Fix draft room Summary tab layout and board scroll clipping (#452)
- Add display:contents to rowgroup divs in DraftSummaryView so cells
  flow correctly into the CSS grid (sports as rows, teams as columns)
- Use opaque bg-muted on team column headers to prevent content
  bleeding through during sticky scroll
- Remove whitespace-nowrap and add break-words on sport row headers
  so long names like "FIFA Men's World Cup" wrap within the column
- Add pb-4 to DraftGridSection scroll wrapper so the last pick row
  isn't flush against the scroll boundary

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 23:31:40 -07:00
Chris Parsons
461847c414
Add public sports overview page (#451) 2026-05-19 23:02:58 -07:00
Chris Parsons
0ba2b391f8
Fix draft order initialization for teams added mid-season (#449)
* Fix draft order showing only new team when league size increased before order was set

When a commissioner increased the team count on a league that had teams
but no draft order set yet, the server created draft slots only for the
newly-added teams. This left the DB with N+K teams but only K slots,
causing the drag-and-drop list to display only the K new teams.

Two fixes:
1. Server: only append new draft slots when an order was already set
   (existingSlots.length > 0). If no order exists yet, skip slot creation
   so the page correctly treats the order as unset for all teams.
2. Frontend: buildDraftOrderTeams() appends any unslotted teams after the
   slotted ones, so a partially-corrupt DB state still shows all teams.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Address code review feedback on draft order bug fix

- Move buildDraftOrderTeams to app/lib/draft-order.ts so it is importable
  and testable; remove the local copy from the settings component
- Add unit tests for buildDraftOrderTeams covering the empty, full, and
  partial-slot cases
- Tighten the draft slot guard from existingSlots.length > 0 to
  existingSlots.length === currentTeamCount so partial legacy states are
  treated the same as "order not set"
- Condense the 3-line server comment to a single line per project style
- Rename getNumTeamsInSeason → getNumDraftSlotsBySeasonId to reflect what
  the function actually counts, and update its one call site
- Add tests for the server-side slot-creation guard logic

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Fix lint: move shouldAppendDraftSlots to outer scope

oxlint (consistent-function-scoping) requires functions that don't close
over any variables to be defined at the outer scope rather than inside a
describe block.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 18:22:47 -07:00
Chris Parsons
06d415d95c
Change Socket.IO connection error logging from error to log level (#448)
* fix: stop reporting transient WebSocket connect_error to Sentry

connect_error fires on every retry attempt (e.g. iOS Safari dropping
WebSocket during network transitions). Using logger.error routed each
attempt to Sentry.captureException, generating noise for a self-healing
failure. Switch to logger.log (no-op in production); the real failure
path (reconnect_failed) still uses logger.error and surfaces in Sentry.

https://claude.ai/code/session_01TpZ9W111Trkv2g4CkCWFpB

* refactor(useDraftSocket): clean up reconnect event handling

- Remove redundant reconnect_attempt listener: connect_error already
  sets isReconnecting(true), and connectionError is never set before
  reconnect_attempt fires, making that handler a no-op
- Wrap reconnect_failed in an Error object so Sentry receives a proper
  exception with stack trace instead of a plain captureMessage string
- Store handleReconnectFailed as a named reference so socket.io.off()
  removes only this listener rather than all reconnect_failed listeners

https://claude.ai/code/session_01TpZ9W111Trkv2g4CkCWFpB

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 15:47:41 -07:00
Chris Parsons
43dbdf0040
Add not-participating exclusion support for event results (#446)
* Add notParticipating flag to allow excluding withdrawn participants from qualifying-points simulators

Adds a `not_participating` boolean column to `event_results` so admins can
mark a participant as not competing in a specific upcoming major (e.g. Alcaraz
withdrawing from Wimbledon due to injury). The golf, tennis, and CS2 major
simulators now query this flag for incomplete events and exclude those
participants from the event's draw/field, redistributing probability weight
to the remaining field. Admin UI for qualifying major_tournament events gains
a "Not Participating" card to mark/unmark withdrawals before the event runs.

https://claude.ai/code/session_01HxNPLEXzr5Km3suWrJe2F9

* Address code review feedback on not-participating flag

Security: unmark-not-participating now validates the result exists, belongs
to this event, and is actually a DNP row before deleting. mark-not-participating
now returns a user-friendly error on duplicate-key constraint violations.

Code quality: extract shared getExcludedByEventMap() utility to event-result
model, eliminating the duplicated 20-line exclusion-loading block that was
copy-pasted into all three simulators. Fix hasParticipantResult() to exclude
notParticipating rows so it correctly reflects actual competition participation.
Remove optional chaining on the non-optional notParticipatingIds field in the
admin UI. Fix misleading empty-state message.

Tests: replace the misleading first tennis DNP test (which never used the
activeIds variable it created) with a test that explicitly validates the
fallback behaviour when too few players remain after exclusion. Add three CS2
DNP tests covering the excluded-team-gets-zero-QP path, the redistribution
of wins, and per-event pool independence.

https://claude.ai/code/session_01HxNPLEXzr5Km3suWrJe2F9

* Fix lint errors: replace non-null assertions in CS2 DNP test

https://claude.ai/code/session_01HxNPLEXzr5Km3suWrJe2F9

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 11:13:32 -07:00
Chris Parsons
3c757c1f73
Replace settings redirect with toast notification (#445)
* Show toast instead of redirecting after league settings save

Commishes now stay on the settings page when saving, with a Sonner
toast confirming the save rather than being navigated to the league
homepage.

https://claude.ai/code/session_01PewdKxVfAbqasJbmk2UGw4

* Fix toast firing for unrelated intents after settings save

Tag update responses with intent: "update" so the success useEffect
only fires for the main settings form, not for owner/commissioner/
reset-draft actions which also return { success: true }. Also replaces
the remaining ?updated=true redirect (no-season edge case) with the
same response shape for consistency.

https://claude.ai/code/session_01PewdKxVfAbqasJbmk2UGw4

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 16:08:10 -07:00
Chris Parsons
68193e31c5
Block bot probe requests before routing (#444)
Fixes #344

* Add bot probe filter middleware and Sentry noise suppression

Block common bot scanning paths (wp-admin, .env, .php, etc.) in Express
before React Router handles them, preventing spurious Sentry errors.
Also filter Sentry events for React Flight protocol probes ($1:aa:aa
multipart body pattern) and any remaining bot-path 404 noise.

https://claude.ai/code/session_01Y5Ca6oxd5zyyM89acmogf7

* Tighten bot probe regex and React Flight Sentry filter

- Remove .xml, .log, .git from bot probe blocklist to avoid false positives
  on legitimate routes (e.g. /sitemap.xml)
- Tighten React Flight error filter from broad string match to precise
  wire-format pattern (/\$\d+:[a-z]/) to avoid swallowing real errors
- Remove redundant URL check from beforeSend (ignoreErrors already covers it)
- Fix missing newline at end of instrument.server.mjs

https://claude.ai/code/session_01Y5Ca6oxd5zyyM89acmogf7

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 11:17:18 -07:00