Commit graph

45 commits

Author SHA1 Message Date
Chris Parsons
a447ba54de
Add Cloudinary-backed avatar system (#385) 2026-05-06 14:47:37 -07:00
Chris Parsons
48b1d470f1
Canonical tournament layer:
cutover + cleanup (2/2) (#367)

* refactor(schema): rename per-window tables to season_* prefix

Renames participants, participant_expected_values, participant_qualifying_totals,
participant_results, participant_surface_elos to season_* prefixed names.
Renames event_results.participant_id to season_participant_id.
Phase 1a of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: rename participant.ts model file to season-participant.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(models): update model layer to use renamed schema exports

Updated all model files to use the renamed schema exports from Task 1:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantQualifyingTotals → seasonParticipantQualifyingTotals
- participantResults → seasonParticipantResults
- participantSurfaceElos → seasonParticipantSurfaceElos
- eventResults.participantId → eventResults.seasonParticipantId
- db.query relation accessors updated
- Relation field .participant → .seasonParticipant where applicable
- Import paths updated: ./participant → ./season-participant

Files updated (14 model files + 3 test files):
- draft-pick.ts
- draft-utils.ts
- event-result.ts
- group-stage-match.ts
- participant-result.ts
- qualifying-points.ts
- scoring-calculator.ts
- scoring-event.ts
- sports-season.ts
- surface-elo.ts
- team-score-events.ts
- cs2-major-stage.ts
- golf-skills.ts
- participant-expected-value.ts
- __tests__/sports-season.clone.test.ts
- __tests__/auto-pick.test.ts
- __tests__/executeAutoPick.timer.test.ts

Typecheck errors decreased: 779 → 499 (280 fewer)
All model file errors related to renamed schemas resolved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(routes): update route layer to use renamed schema exports

- Update model import from ~/models/participant to ~/models/season-participant
- Rename schema.participants to schema.seasonParticipants
- Rename schema.participantResults to schema.seasonParticipantResults
- Rename db.query.participants to db.query.seasonParticipants
- Update 9 route files and 1 test file

Affected files:
- admin.sports-seasons.$id.events.$eventId.bracket.server.ts
- admin.sports-seasons.$id.participants.tsx
- api/draft.force-manual-pick.ts
- api/draft.make-pick.ts
- api/draft.replace-pick.ts
- api/seasons.$seasonId.draft.ts
- leagues/$leagueId.draft-board.$seasonId.tsx
- leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts
- admin/__tests__/sports-seasons-participants.test.ts

Error count reduced from 499 to 453 (46 errors fixed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(routes): update route files for schema rename

Update route imports from ~/models/participant to ~/models/season-participant
and fix references to .participant/.participantId on event results to use
.seasonParticipant/.seasonParticipantId after schema rename.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(services): update simulators and services for renamed schema

Update all simulators, services, and server files to use renamed schema tables:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantResults → seasonParticipantResults
- eventResults.participantId → eventResults.seasonParticipantId

Files updated:
- 20 sport simulators (NBA, NHL, NFL, MLB, etc.)
- probability-updater.ts
- standings-sync/index.ts
- sports-data-sync.server.ts
- server/socket.ts

Typecheck errors reduced from 365 to 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* migration: rename per-window tables to season_* prefix

* fix(tests): update mock query keys after participants table rename

Change mock db.query.participants to db.query.seasonParticipants in test
files to match the schema rename from commit 66145a9. This fixes
"Cannot read properties of undefined (reading 'findFirst'/'findMany')"
errors that occurred when production code queries db.query.seasonParticipants
but test mocks only defined the old participants key.

Files updated:
- app/services/simulations/__tests__/world-cup-simulator.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts
- app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts
- server/__tests__/timer-autodraft.test.ts
- app/models/__tests__/team-score-events.test.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): update remaining mock paths and keys after schema rename

* fix(tests): final two mock stragglers after schema rename

- draft-pick.test.ts: assertion on db.query.participantQualifyingTotals
- process-match-result.test.ts: mock key participants → seasonParticipants

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: add post-phase1a baseline capture (temp, for diff verification)

* chore: capture pre-migration baselines

* chore: remove post-phase1a capture helper after verification

* schema: add canonical tournament & participant tables

Adds tournaments, participants (canonical), tournament_results, and
participant_surface_elos (canonical). Adds nullable tournament_id to
scoring_events and nullable participant_id to season_participants.
Phase 1b of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(models): add canonical tournament, participant, result, surface-elo models

Adds CRUD modules for the canonical tables created in commit 775b905.
Each module mirrors existing app/models conventions (database() from
~/database/context, schema from ~/database/schema, mock-based tests).

Key implementation notes:
- participant.ts exports use "Canonical" prefix (CanonicalParticipant,
  createCanonicalParticipant, etc.) to avoid collision with existing
  season-participant.ts exports
- All four models include comprehensive unit tests following the
  audit-log.test.ts pattern
- Tests use mocked db responses (no real database access)
- Upsert functions use onConflictDoUpdate for appropriate unique constraints

Part of Phase 1b of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* migration: create canonical tables, add nullable FKs

* scripts: add extractTournamentIdentity helper for backfill

Pure function that derives canonical (name, year) identity from a
scoring_events row, stripping trailing 4-digit years from the name or
falling back to eventDate. Used by the Phase 2 backfill to group
per-window events into canonical tournaments.

* scripts: add backfill orchestrator for canonical layer

Populates canonical tournaments, participants, tournament_results, and
participant_surface_elos from per-window data for qualifying-points
sports. Skips already-linked rows, is idempotent, and supports dry-run
mode.

Critical invariants enforced by the implementation:
- qualifying_points_awarded is never copied to tournament_results
- season_participant_qualifying_totals is never touched
- conflicting surface-Elo values between windows raise a loud error
  (recorded in report.errors) rather than overwriting

* scripts: add backfill CLI with dry-run default

Wires backfill-canonical-layer.ts to a CLI entry point exposed as
`npm run backfill:canonical`. Defaults to --dry-run; requires --apply
to actually write. Supports --sport=<uuid> to limit to a single sport.
Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts).

* fix(backfill-cli): wrap runBackfill in DatabaseContext.run

The orchestrator uses database() from ~/database/context, which requires
AsyncLocalStorage to be populated. Wrap the CLI invocation with
DatabaseContext.run(db, ...) using server/db's cached connection pool.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(backfill-cli): exit 0 on success so pg pool doesn't block

The cached postgres connection pool keeps the Node event loop open after
main() returns. Explicit process.exit(0) on success mirrors the pattern
in scripts/capture-baseline.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* schema: add check constraint requiring tournament_id for qualifying events

Team sports (NHL, NBA, etc.) have scoring_events with no canonical
tournament, so we can't require tournament_id globally. The constraint
only applies when is_qualifying_event=true.

Phase 3 Task 1 of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(BatchResultEntry): make form intent configurable

Canonical tournament admin page will submit with a different intent
(batch-upsert-results). Existing per-window callers continue to get
"batch-add-results" as the default, so no behavior changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(services): syncTournamentResults fan-out service

* schema: add unique (scoring_event_id, season_participant_id) on event_results

syncTournamentResults uses check-then-write on event_results. The
unique index defends against race conditions (unlikely today — admin
is single-writer — but safe to enforce).

WARNING: if existing prod data has duplicate (scoring_event_id,
season_participant_id) pairs, the migration will fail. Verify with:
  SELECT scoring_event_id, season_participant_id, count(*)
  FROM event_results
  GROUP BY 1, 2
  HAVING count(*) > 1;

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(surface-elo): getSurfaceEloMap reads from canonical

Switch the tennis simulator's Elo source from the per-window table
to the canonical participant_surface_elos via season_participants.participant_id.
Map key stays season_participant.id so the simulator's call pattern is unchanged.

Other functions in this file still read/write the per-window table
(seasonParticipantSurfaceElos). They back the old admin surface-elo page
and are removed in Phase 4 after the canonical admin flow replaces them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(admin): canonical tournament list + result entry routes

New routes /admin/tournaments and /admin/tournaments/:id. The detail
page lets admins enter tournament results once via paste-and-parse,
then calls syncTournamentResults to fan out to every linked window.

Phase 3 Task 4 of canonical tournament layer migration.

* feat: per-window batch-add-results routes through canonical when linked

If the scoring_event has a tournament_id, translate season_participant ids
to canonical participant ids, upsert tournament_results, and fan out via
syncTournamentResults. Team sports (no tournament link) keep the legacy
direct-write path.

Phase 3 Task 8 of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(surface-elo): mirror batchUpsert writes to canonical table

The simulator now reads surface Elo from the canonical table
(participant_surface_elos). The existing per-window admin page still
posts to batchUpsertSurfaceElos; without this change, those edits
would silently fail to affect simulator output. Mirror every write
to both tables until Phase 4 removes the per-window path entirely.

Phase 3 Task 5 (partial — canonical write path; dedicated canonical
admin UI deferred; existing page continues to work and now
edits both tables).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat: auto-provision canonical tournaments and participants on create

Creating new qualifying scoring_events or season_participants now
auto-upserts the matching canonical rows. Needed so the check constraint
doesn't reject qualifying events, and so new season participants
flow through syncTournamentResults.

- Extract tournament identity shared between backfill and event-creation
  into app/lib/tournament-identity.ts; scripts/backfill re-exports it.
- createScoringEvent + bulkCreateScoringEvents now accept tournamentId.
- Event creation routes auto-compute (name, year) and upsert tournament.
- createParticipant + createManyParticipants auto-link to canonical
  participants by (sportId, name).

Phase 3 Task 7 of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): update surface-elo tests for canonical mirror

getSurfaceEloMap now joins through season_participants and returns
rows keyed by seasonParticipantId (was participantId).

batchUpsertSurfaceElos now performs a second db.select to resolve
canonical links before mirroring writes; tests mock the lookup
explicitly and exercise both the "no canonical link" short-circuit
path and the "mirror to canonical" path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* migration: add check constraint + unique index for canonical

* refactor(surface-elo): canonical-only; drop per-window path

batchUpsertSurfaceElos now writes only to the canonical
participant_surface_elos table (the mirror step is now the only step).
getSurfaceElosForSeason joins through season_participants → canonical
participants so the admin UI keeps its existing (id, participantId,
sportsSeasonId, ...) row shape.

season_participants without a canonical link get silently skipped. In
practice every qualifying-points roster entry is canonical-linked after
Phase 2 backfill + Phase 3 auto-linking on createParticipant.

Phase 4 Tasks 2 + 3 of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: remove legacy direct-write fallback in batch-add-results

Every qualifying scoring_event now has tournament_id (check constraint
enforces it), so the non-canonical fallback is dead. Replace with an
explicit error surface so any configuration regression becomes visible
instead of silently bypassing canonical.

Phase 4 Task 4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* schema: drop season_participant_surface_elos table

Table had no remaining readers after Phase 3 and no remaining writers
after Phase 4 Task 2. Also removes the now-unrunnable Phase 2 backfill
scripts and the backfill:canonical npm script — the backfill ran once
and is preserved in git history (commits 85bca8b, 8186dbb, 6f3438b,
bc0f530, 327c7b9).

Phase 4 Task 5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(agents): describe canonical/per-window split

Update database.md with the canonical/per-window model explanation,
the syncTournamentResults fan-out contract, the check constraint,
and the tennis-Men/Women quirk.

Update domain-models.md to reflect the renamed per-window entities
and the new canonical layer.

Also update the drizzle-kit gotchas section with lessons from the
migration: do not use --custom for renames; FK constraint naming
can mismatch Drizzle convention.

Phase 4 Task 6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* migration: drop season_participant_surface_elos

* feat(admin): add Tournaments link to admin sidebar

Missed in Phase 3 Task 4 — the /admin/tournaments route exists but
was not reachable from the admin nav without typing the URL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(lint): address 4 oxlint errors on canonical-layer-pr2

- Remove unused filterNewResults import (events batch-add-results legacy
  path was removed in Phase 4 Task 4).
- Replace import() type annotation in sync-tournament-results test with
  a top-level type import (consistent-type-imports rule).
- Hoist tableName helper out of makeFakeDb closure so it's not recreated
  per call (consistent-function-scoping rule).
- eqeqeq: replace `!= null` with explicit null/undefined checks in
  tournament-identity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Chris Parsons <chrisp@extrahop.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 21:04:48 -07:00
Chris Parsons
2848231235
Canonical tournament layer: schema + backfill (1/2) (#365)
* refactor(schema): rename per-window tables to season_* prefix

Renames participants, participant_expected_values, participant_qualifying_totals,
participant_results, participant_surface_elos to season_* prefixed names.
Renames event_results.participant_id to season_participant_id.
Phase 1a of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: rename participant.ts model file to season-participant.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(models): update model layer to use renamed schema exports

Updated all model files to use the renamed schema exports from Task 1:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantQualifyingTotals → seasonParticipantQualifyingTotals
- participantResults → seasonParticipantResults
- participantSurfaceElos → seasonParticipantSurfaceElos
- eventResults.participantId → eventResults.seasonParticipantId
- db.query relation accessors updated
- Relation field .participant → .seasonParticipant where applicable
- Import paths updated: ./participant → ./season-participant

Files updated (14 model files + 3 test files):
- draft-pick.ts
- draft-utils.ts
- event-result.ts
- group-stage-match.ts
- participant-result.ts
- qualifying-points.ts
- scoring-calculator.ts
- scoring-event.ts
- sports-season.ts
- surface-elo.ts
- team-score-events.ts
- cs2-major-stage.ts
- golf-skills.ts
- participant-expected-value.ts
- __tests__/sports-season.clone.test.ts
- __tests__/auto-pick.test.ts
- __tests__/executeAutoPick.timer.test.ts

Typecheck errors decreased: 779 → 499 (280 fewer)
All model file errors related to renamed schemas resolved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(routes): update route layer to use renamed schema exports

- Update model import from ~/models/participant to ~/models/season-participant
- Rename schema.participants to schema.seasonParticipants
- Rename schema.participantResults to schema.seasonParticipantResults
- Rename db.query.participants to db.query.seasonParticipants
- Update 9 route files and 1 test file

Affected files:
- admin.sports-seasons.$id.events.$eventId.bracket.server.ts
- admin.sports-seasons.$id.participants.tsx
- api/draft.force-manual-pick.ts
- api/draft.make-pick.ts
- api/draft.replace-pick.ts
- api/seasons.$seasonId.draft.ts
- leagues/$leagueId.draft-board.$seasonId.tsx
- leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts
- admin/__tests__/sports-seasons-participants.test.ts

Error count reduced from 499 to 453 (46 errors fixed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(routes): update route files for schema rename

Update route imports from ~/models/participant to ~/models/season-participant
and fix references to .participant/.participantId on event results to use
.seasonParticipant/.seasonParticipantId after schema rename.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(services): update simulators and services for renamed schema

Update all simulators, services, and server files to use renamed schema tables:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantResults → seasonParticipantResults
- eventResults.participantId → eventResults.seasonParticipantId

Files updated:
- 20 sport simulators (NBA, NHL, NFL, MLB, etc.)
- probability-updater.ts
- standings-sync/index.ts
- sports-data-sync.server.ts
- server/socket.ts

Typecheck errors reduced from 365 to 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* migration: rename per-window tables to season_* prefix

* fix(tests): update mock query keys after participants table rename

Change mock db.query.participants to db.query.seasonParticipants in test
files to match the schema rename from commit 66145a9. This fixes
"Cannot read properties of undefined (reading 'findFirst'/'findMany')"
errors that occurred when production code queries db.query.seasonParticipants
but test mocks only defined the old participants key.

Files updated:
- app/services/simulations/__tests__/world-cup-simulator.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts
- app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts
- server/__tests__/timer-autodraft.test.ts
- app/models/__tests__/team-score-events.test.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): update remaining mock paths and keys after schema rename

* fix(tests): final two mock stragglers after schema rename

- draft-pick.test.ts: assertion on db.query.participantQualifyingTotals
- process-match-result.test.ts: mock key participants → seasonParticipants

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: add post-phase1a baseline capture (temp, for diff verification)

* chore: capture pre-migration baselines

* chore: remove post-phase1a capture helper after verification

* schema: add canonical tournament & participant tables

Adds tournaments, participants (canonical), tournament_results, and
participant_surface_elos (canonical). Adds nullable tournament_id to
scoring_events and nullable participant_id to season_participants.
Phase 1b of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(models): add canonical tournament, participant, result, surface-elo models

Adds CRUD modules for the canonical tables created in commit 775b905.
Each module mirrors existing app/models conventions (database() from
~/database/context, schema from ~/database/schema, mock-based tests).

Key implementation notes:
- participant.ts exports use "Canonical" prefix (CanonicalParticipant,
  createCanonicalParticipant, etc.) to avoid collision with existing
  season-participant.ts exports
- All four models include comprehensive unit tests following the
  audit-log.test.ts pattern
- Tests use mocked db responses (no real database access)
- Upsert functions use onConflictDoUpdate for appropriate unique constraints

Part of Phase 1b of canonical tournament layer migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* migration: create canonical tables, add nullable FKs

* scripts: add extractTournamentIdentity helper for backfill

Pure function that derives canonical (name, year) identity from a
scoring_events row, stripping trailing 4-digit years from the name or
falling back to eventDate. Used by the Phase 2 backfill to group
per-window events into canonical tournaments.

* scripts: add backfill orchestrator for canonical layer

Populates canonical tournaments, participants, tournament_results, and
participant_surface_elos from per-window data for qualifying-points
sports. Skips already-linked rows, is idempotent, and supports dry-run
mode.

Critical invariants enforced by the implementation:
- qualifying_points_awarded is never copied to tournament_results
- season_participant_qualifying_totals is never touched
- conflicting surface-Elo values between windows raise a loud error
  (recorded in report.errors) rather than overwriting

* scripts: add backfill CLI with dry-run default

Wires backfill-canonical-layer.ts to a CLI entry point exposed as
`npm run backfill:canonical`. Defaults to --dry-run; requires --apply
to actually write. Supports --sport=<uuid> to limit to a single sport.
Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts).

* fix(backfill-cli): wrap runBackfill in DatabaseContext.run

The orchestrator uses database() from ~/database/context, which requires
AsyncLocalStorage to be populated. Wrap the CLI invocation with
DatabaseContext.run(db, ...) using server/db's cached connection pool.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(backfill-cli): exit 0 on success so pg pool doesn't block

The cached postgres connection pool keeps the Node event loop open after
main() returns. Explicit process.exit(0) on success mirrors the pattern
in scripts/capture-baseline.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Chris Parsons <chrisp@extrahop.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:13:18 -07:00
Chris Parsons
65478e1ef2 Add react-is as explicit dependency to fix production Docker build
recharts declares react-is as a peerDependency. With legacy-peer-deps=true,
npm no longer auto-installs peer deps, so react-is was missing from the
production image and recharts failed to load at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:29:13 -07:00
Chris Parsons
ba9bf64e37
Migrate authentication from Clerk to BetterAuth (#324)
* Migrate authentication from Clerk to BetterAuth (#322)

Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.

**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field

**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict

better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
Chris Parsons
9ed0282fd0
New design (#309)
* Redesign home page with new layout and component system

- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Responsive league row layout and mobile polish

- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Improve claude file.

* Add StandingsPreview card component with podium row styling

- New StandingsPreview component with gold/silver/bronze row tints for
  top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
  with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
  preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
  BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
  coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
  fix was sufficient once gradientUnits was corrected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update components on league homepage.

* Finish up league page styling.

* Work on standings page.

* Add story for RecentScoresCard

* Update Point Progression Chart.

* Sort point progression legend by ranking and add team links to standings rows

* Fix standings discrepancy on change.

* Create draft cell component.

* Update draft board page

* Draft room improvements.

* Update some draft room styling.

* Fix context menu missing.

* Move tab navigation and autodraft to header row, narrow sidebar

* Virtualize available participants list, memoize draft room props

Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update draft room UI.

* More draft room fixes.

* Draft room tweaks.

* Fix Rosters page.

* Queue Section fixes.

* Mobile Draft fixes.

* Fix draft board page.

* Create bracket look.

* Bracket work.

* Finish bracket page.

* Homepage initial styling

* homepage copy

* Add privacy policy. Fixes #88.

* how to play copy

* rules copy

* Fix brackets on homepage.

* Add footer to website.

* Glow on dots.

* Landing page copy.

* Fix sidebar.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
Chris Parsons
f1af4b5171
Decouple database migrations from server startup (#265)
* Use Docker Compose init service pattern for database migrations

Replaces the fragile double-migration approach (server.ts startup +
drizzle-kit CLI in CI) with a one-shot migrate service in Docker Compose
that the app depends on via condition: service_completed_successfully.

- Add scripts/migrate.mjs: programmatic drizzle-orm migration (uses
  production deps, not drizzle-kit which is devOnly and absent from image)
- Dockerfile: copy scripts/ into image, remove drizzle.config.ts (only
  needed by drizzle-kit CLI)
- server.ts: remove runMigrations() entirely; migrations are now handled
  by the migrate container before the app starts
- deploy.yml: remove explicit docker run migration step; replace sleep 10
  health check with a poll loop and explicit migrate exit-code check

Production server's docker-compose.yaml needs a one-time manual update
to add the migrate service — see plan for exact config.

https://claude.ai/code/session_01ReaqH3o9NVH4QU4qE9WMMQ

* Move drizzle-kit to devDependencies; use docker compose wait in deploy

drizzle-kit is a dev-only tool (schema generation and local migrations).
Now that production migrations run via the programmatic drizzle-orm API
in the migrate init container, drizzle-kit has no runtime role.

Also replaces the polling health check loop with docker compose wait,
which blocks until the migrate service exits and returns its exit code
cleanly — no sleep or manual status inspection needed.

https://claude.ai/code/session_01ReaqH3o9NVH4QU4qE9WMMQ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-06 00:07:36 -04:00
Chris Parsons
139061e702
Add production database sync script with sanitization (#258)
* Add prod-to-dev database sync script

Adds scripts/sync-prod-db.sh and npm run db:sync-prod to safely copy
production data to the development database. After restoring, the script
nulls all Discord webhook URLs and anonymizes non-admin user PII (email,
name, avatar) to prevent notification leaks and reduce PII exposure on
developer machines. Admin accounts are preserved intact.

https://claude.ai/code/session_01EiYh5ZiuWAnpo1PND45Yi1

* Fix six code review issues in sync-prod-db.sh

- Add upfront check for pg_dump/pg_restore/psql availability (#5)
- Fix password masking to use sed instead of broken bash glob (#6)
- Replace DROP/CREATE DATABASE with pg_restore --clean --if-exists,
  which works on managed cloud databases without superuser access (#2)
- Remove dead ADMIN_DB assignment that was immediately overwritten (#3)
- Handle pg_restore exit code 1 (non-fatal warnings) gracefully
  instead of aborting via set -e (#1)
- Count webhook URLs before the UPDATE for an accurate cleared count (#4)

https://claude.ai/code/session_01EiYh5ZiuWAnpo1PND45Yi1

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-03 15:17:58 -07:00
Chris Parsons
28a670666c
Fix test:run failing in CI due to missing Playwright browser (#246)
Scope test:run to the unit project only so it doesn't attempt to launch
Playwright/Chromium (needed by the storybook browser project) in environments
where browsers aren't installed. Add test:storybook script for running
storybook stories explicitly when Playwright is available.

https://claude.ai/code/session_01PqDTVMzP7dMtLpkn5Sw442

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-30 14:42:23 -07:00
Chris Parsons
131945ddeb
Install Storybook with React Router Vite plugin fix, closes #229 (#243)
- Install Storybook 10 with @storybook/react-vite framework
- Fix React Router Vite plugin conflict by filtering it out in viteFinal
  (reactRouter() returns a plugin array, so the filter must flatten first)
- Integrate Storybook stories as a vitest browser project via @storybook/addon-vitest
- Point stories glob at app/ instead of scaffold stories/ dir
- Remove unused @chromatic-com/storybook and @storybook/addon-onboarding addons
- Clean up duplicate __dirname in vitest.config.ts

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 15:20:48 -07:00
Chris Parsons
29ea93ad6d
Add support page with Discord link and contact form, fixes #233 (#234)
- New /support route with Discord join button and contact form
- Contact form sends via Resend to support@brackt.com (email stays hidden from users)
- Spam protection: honeypot field + Cloudflare Turnstile verification
- Server-side input length limits (subject: 200, message: 5000 chars)
- Added Support nav link to navbar (desktop + mobile)

Fixes #233

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 17:17:49 -07:00
Chris Parsons
1bdd5c3372 Add snooker bracket simulator and Elo ratings admin UI, fixes #119
- SnookerSimulator: Monte Carlo simulation of the 32-player World
  Championship bracket using per-frame Bernoulli win probabilities
  derived from Elo ratings (ELO_DIVISOR=700). Two paths: bracket-
  populated (respects completed matches) and pre-bracket (simulates
  qualifying, then seeds full draw).
- Admin route /sports-seasons/:id/elo-ratings: bulk-import and per-
  player Elo entry with fuzzy name matching; saves ratings and auto-
  runs the simulation in one action.
- schema: add snooker_bracket simulator type, source_elo column on
  participant_expected_values, unique index on (participant_id,
  sports_season_id).
- Fix batchSaveSourceElos to use INSERT ... ON CONFLICT DO UPDATE
  instead of N+1 SELECT+UPDATE loop.
- Fix existingElos returned as plain Record (Map doesn't survive JSON
  serialization through useLoaderData).
- Fix "How It Works" formula to show correct divisor (700, not 400).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:24:28 -07:00
Chris Parsons
e2b178221a
Add oxlint linting setup with zero errors (#194)
* Add oxlint and fix all lint errors

- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix no-explicit-any warnings and upgrade tsconfig to ES2023

- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
  participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Promote no-explicit-any to error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
Chris Parsons
0277c4d25e
Add per-event configurable region config for NCAA-style brackets (#149) (#149)
Replaces the hardcoded 2026 Men's region structure with a per-event
configurable system so the same ncaa_68 template works for Women's
March Madness (different region names and play-in placements each year).

- Add `bracket_region_config jsonb` column to `scoring_events`
- Export `ALL_16_SEEDS` and `BracketRegion`/`BracketPlayIn` interfaces
  from bracket-templates.ts so both server and client share the type
- Admin bracket entry form now shows a "Configure Regions" section:
  editable region names + add/remove play-in seed selectors (ShadCN
  Select) per region; slot map updates live as config changes
- Server action parses region config from form, validates total = 68,
  stores it on the event, and passes it to bracket generation
- `advanceFirstFourWinner` reads stored `bracketRegionConfig` from the
  event row first, falls back to template.regions for existing events
- Fix opponentSeed formula (was `seedNum <= 8 ? 17-n : n`, always `17-n`)
- Remove duplicate ALL_SEEDS inline definitions (now a shared constant)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:52:47 -07:00
Chris Parsons
f97ee4e4d8
Add NProgress navigation progress bar (#139)
Installs nprogress and wires it to React Router's useNavigation() hook
so a progress bar appears at the top of the page during route transitions.
Themed to the site's --electric CSS variable with the spinner disabled.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 22:02:48 -07:00
Chris Parsons
3dc6549640 MCP update 2026-03-11 21:45:19 -07:00
Chris Parsons
cbae4cc0c4
fix: replace react-router-serve with node dist/server.js in start script (#133)
The project uses a custom Express server (built to dist/server.js) rather
than the react-router-serve CLI, which was never installed. Also adds the
start:production alias referenced in CLAUDE.md.

https://claude.ai/code/session_011hnBY83ui5P5MzWt3iYppc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 09:06:11 -07:00
Chris Parsons
35a3b71579
feat: add Sentry error monitoring (#132)
* feat: add Sentry error monitoring (#77)

Installs and configures @sentry/react-router with server and client
instrumentation. Disabled in development to avoid noise; only active
in production.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add VSCode Sentry MCP server config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: pass vite env to sentryReactRouter plugin

sentryReactRouter requires the ConfigEnv as a second argument to read
the vite `command` property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 23:35:33 -07:00
Chris Parsons
35fe84a1dd
Update claude.md to push more tests. (#84)
* Update claude.md to push more tests.

* Archiving old plans.

* fix: run DB migrations programmatically on server startup

Replace unreliable drizzle-kit CLI migration with drizzle-orm's built-in
migrator running in the server process before accepting connections. This
ensures migrations are always applied on deploy and fails fast if they error.

- Add runMigrations() to server.ts using drizzle-orm/postgres-js/migrator
- Skip migrations in development (handled manually via db:migrate)
- Add DATABASE_URL guard with a clear error message
- Remove start:production script (now identical to start)
- Update Dockerfile CMD to use npm run start

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:31:04 -08:00
Chris Parsons
9ae00a8385
Add Zod validation and expand export/import for EV and results data (#14)
* Include participant EV/futures data in data sync export/import

The export now includes the full participantExpectedValues records
(placement probabilities, source, sourceOdds) alongside participants.
On import, EV records are upserted in merge mode and created fresh in
replace mode (cascade handles cleanup). Backward-compatible with older
exports that lack the participantExpectedValues field.

https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B

* Address all data-sync code review findings

- Wrap importSportsDataFromJSON in a db.transaction() so any mid-import
  failure rolls back cleanly instead of leaving partial data
- Add zod validation of imported JSON before any DB writes, giving a
  clear error on malformed/incompatible files
- Fix N+1 queries: build participantIdMap during participant import so
  the EV and results sections resolve IDs from memory, not extra queries
- Fix merge mode silently skipping existing participants — now updates
  shortName, externalId, and expectedValue
- Add participantResults to export/import (was deleted in replace mode
  but never exported, so results were permanently lost)
- Restore calculatedAt timestamp on EV import instead of defaulting to
  now(); stored as ISO string in the export for portability
- Document that onDelete:cascade covers participantExpectedValues and
  participantResults when participants is deleted; remove the now-
  redundant explicit participantResults delete from replace mode
- Bump export version to 1.1; old v1.0 files still import cleanly since
  participantExpectedValues, participantResults, and calculatedAt are
  all optional in the zod schema
- Collapse all six DB fetches in exportSportsDataToJSON into one
  Promise.all for parallel execution
- Add countAllParticipants() and countAllParticipantEVs() model funcs
- Show Participants and EV Records counts on the dashboard stat cards
- Replace raw DOM form creation/submit in handleImport with useFetcher,
  giving proper loading state, no full-page reload, and type-safe data
- Remove dead export intent handler from the action function

https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 20:25:25 -08:00
Chris Parsons
1584d34b89
Redesign to dark-mode-only with navy palette and accent colors (#13)
Removes light mode entirely in favour of a permanent dark theme with a
navy-tinted background and three signature accents (electric blue,
amber/gold, coral) exposed as CSS custom properties and Tailwind
utilities (bg-electric, text-amber-accent, text-coral-accent).

- Set class="dark" on <html> and apply Clerk dark base theme
- Rewrite app.css: single :root palette (oklch navy values), custom
  --electric / --amber-accent / --coral-accent variables, remove
  duplicate .dark block and light-mode bg-white/bg-gray-950 rule
- Install @clerk/themes for Clerk dark modal support
- Replace hardcoded Tailwind colors across 30+ files:
  - Draft grid cells: blue-50/blue-950 → electric/15, green-50/950 → emerald/10
  - Timer: green-600/yellow-600/red-600 → emerald-400/amber-accent/coral-accent
  - Status badges: blue-50/green-50/gray-50 → electric/emerald/muted variants
  - Success messages: green-500/15 text-green-700 dark:text-green-400 → emerald-500/15 text-emerald-400
  - Info cards: blue-50 dark:bg-blue-950 → electric/10
  - Warning cards: yellow-500 → amber-accent variants
  - Medal/placement badges: yellow-500/orange-600 → amber-accent/coral-accent
  - Movement indicators: green-600/red-600 → emerald-400/coral-accent
  - Connection dots: green-500/red-500 → emerald-500/coral-accent
- Remove dark:hidden/dark:block logo toggle in welcome.tsx (always dark)
- Update DraftGrid test assertions to match new class names

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:26:11 -08:00
Chris Parsons
6ef829f667 feat: add recharts library and implement PointProgressionChart component for historical views
- Added recharts dependency to package.json for data visualization.
- Implemented PointProgressionChart component to display team point progression over time.
- Updated scoring-system.md to reflect completion of historical views and added implementation notes.
- Created unit tests for PointProgressionChart to ensure correct rendering and data handling.
- Developed season completion detection and percentage calculation functions in season-helpers.server.ts.
- Added tests for season completion logic and point progression data formatting.
- Enhanced league loader to fetch necessary data for current season and teams.
2025-11-14 21:18:34 -08:00
Chris Parsons
98b84095fd feat: Implement drag-and-drop functionality for queue items; add duplicate prevention for participants in queue 2025-10-25 21:02:16 -07:00
Chris Parsons
32980428f2 feat: Integrate accordion component for sidebar and update autodraft settings to sync with props 2025-10-25 10:04:21 -07:00
Chris Parsons
2cd4096e70 feat: Implement draft room redesign with collapsible sidebar and tabbed content
- Added AvailableParticipantsSection for participant search and filtering.
- Created DraftGridSection for displaying the draft grid with team timers and context menu.
- Developed QueueSection for managing participant queue and autodraft settings.
- Introduced RecentPicksSection to show recent draft picks.
- Implemented TeamsDraftedGrid to visualize drafted participants by team and sport.
- Added collapsible UI components for better layout management.
- Established tabbed navigation for draft board, recent picks, and teams drafted.
- Enhanced responsiveness for mobile and desktop views.
- Updated styles for a cohesive design across components.
2025-10-25 03:23:41 -07:00
Chris Parsons
f3050908e8 chore: add shadcn package to dependencies 2025-10-23 16:56:46 -07:00
Chris Parsons
ff97e25438 feat: add autodraft status and team connection indicators to draft grid 2025-10-21 23:22:17 -07:00
Chris Parsons
0d4ce1d339 ci: add test job to GitHub workflow and setup testing dependencies 2025-10-21 12:47:11 -07:00
Chris Parsons
2bad3dd75e refactor: migrate server code from JavaScript to TypeScript with build pipeline 2025-10-18 22:16:04 -07:00
Chris Parsons
9d41508fd0 feat: add draft API routes and implement draft room pick controls 2025-10-18 14:55:26 -07:00
Chris Parsons
12fdc6943c chore: add socket.io and socket.io-client dependencies 2025-10-17 11:13:37 -07:00
Chris Parsons
b09c55f22c fix: update Docker file paths and simplify production start script 2025-10-17 11:04:16 -07:00
Chris Parsons
b9743aacc1 Revert Phase 8 and 9 implementations back to 0bef91e 2025-10-16 10:17:31 -07:00
Chris Parsons
61ef5f3fe7 refactor: migrate server.js to TypeScript and update dependencies for socket.io support 2025-10-16 00:56:31 -07:00
Chris Parsons
dd9d385f16 feat: add draft date/time selection to league creation and settings 2025-10-15 22:02:21 -07:00
Chris Parsons
0999e1c980 feat: add sports selection and team management to league settings 2025-10-13 19:59:34 -07:00
Chris Parsons
5f6d59b68c chore: move drizzle-kit from devDependencies to dependencies 2025-10-13 11:04:40 -07:00
Chris Parsons
f72b98fdda refactor: consolidate migration and app containers into single production image 2025-10-13 11:01:12 -07:00
Chris Parsons
37de6787bf feat: add admin routes and checkbox component for sports management 2025-10-12 21:54:49 -07:00
Chris Parsons
9749fc8b77 feat: add user model, routes, and Clerk webhook integration 2025-10-11 00:53:39 -07:00
Chris Parsons
9dc033b2a0 feat: add commissioner model and league management features 2025-10-11 00:29:04 -07:00
Chris Parsons
5c957249ad feat: add fantasy league schema and new league routes 2025-10-11 00:07:39 -07:00
Chris Parsons
6d998a5f85 feat: add theme system with dark mode support and navbar component 2025-10-10 23:30:26 -07:00
Chris Parsons
d9f96efd44 feat: add Clerk authentication with sign-in button and user profile UI 2025-10-10 23:18:16 -07:00
Chris Parsons
3117080592 Initial commit from create-react-router 2025-10-10 23:04:50 -07:00