brackt/CLAUDE.md

273 lines
12 KiB
Markdown
Raw Normal View History

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Brackt.com is a fantasy sports drafting platform built with React Router 7, featuring real-time draft functionality with Socket.IO, PostgreSQL database with DrizzleORM, and Clerk authentication.
## Tech Stack
- **Framework**: React Router 7 with SSR
- **Runtime**: Node.js with Express server
- **Database**: PostgreSQL with DrizzleORM
- **Auth**: Clerk (@clerk/react-router)
- **Real-time**: Socket.IO (server/socket.ts)
- **Styling**: TailwindCSS + ShadCN UI components
- **Testing**: Vitest (unit/component) + Cypress (E2E)
- **TypeScript**: Strict mode enabled
## Development Commands
```bash
# Development (HMR enabled)
npm run dev
# Build
npm run build # Build both Remix and server
npm run build:remix # Build React Router app only
npm run build:server # Build Express server only
# Database
npm run db:generate # Generate Drizzle migrations
npm run db:migrate # Run migrations
# Testing
npm test # Unit tests (watch mode)
npm run test:run # Unit tests (run once)
npm run test:ui # Unit tests with UI
npm run test:coverage # Coverage report
npm run test:e2e # Cypress interactive
npm run test:e2e:headless # Cypress headless
npm run test:all # Run all tests
# Type checking
npm run typecheck # Check all TypeScript files
# Production
npm start # Start production server (requires build)
npm run start:production # Run migrations + start server
```
## Architecture Overview
### Server Architecture
The application uses a dual-server architecture:
1. **server.ts** - Main entry point that creates the HTTP server and initializes Socket.IO
2. **server/app.ts** - Express app with React Router request handler and database context
3. **server/socket.ts** - Socket.IO server for real-time draft updates
4. **server/timer.ts** - Draft timer system that runs every second to update active draft timers
**Important**: The timer system (`server/timer.ts`) must be started for draft functionality. It uses a dedicated database connection and setInterval to manage all active draft timers.
### Database Layer
- **Schema**: `database/schema.ts` - All Drizzle table definitions
- **Context**: Database connection provided via AsyncLocalStorage context (`database/context.ts`)
- **Models**: `app/models/` - Business logic and database queries organized by domain
- Each model exports typed functions for CRUD operations
- Models use Drizzle relations for joins
- Example: `app/models/league.ts`, `app/models/season.ts`, `app/models/draft-pick.ts`
### Frontend Architecture
- **Routes**: Defined in `app/routes.ts` using React Router config syntax
- **IMPORTANT**: React Router 7 requires ALL routes to be explicitly registered in `app/routes.ts`
- **When creating a new route file, you MUST also add it to `app/routes.ts`**
- Example: If you create `app/routes/admin.sports-seasons.$id.new-feature.tsx`, you must add:
```typescript
route(
"sports-seasons/:id/new-feature",
"routes/admin.sports-seasons.$id.new-feature.tsx"
),
```
- **File-based routing**: Route files in `app/routes/` map to URL patterns
- `$param` = dynamic segment (e.g., `$leagueId.tsx`)
- `_index.tsx` = index route
- `.` separator for nested routes (e.g., `leagues/$leagueId.settings.tsx`)
- **Loaders/Actions**: Use React Router's loader/action pattern for data fetching and mutations
- **Components**: Organized in `app/components/` with UI components in `app/components/ui/`
### Real-time Features
Socket.IO is used for live draft updates:
1. **Join Draft**: Clients join a room for their season (`join-draft` event)
2. **Timer Updates**: Server broadcasts `timer-update` every second during active drafts
3. **Pick Made**: Broadcasts `pick-made` when a participant is drafted
4. **Autodraft**: Broadcasts `autodraft-updated` when teams enable/disable autodraft
5. **Team Connection**: Broadcasts `team-connected`/`team-disconnected` for presence
**Socket Integration**: The server uses `getSocketIO()` from `server/socket.ts` to emit events. Clients use `useSocket()` hook (if implemented) to listen for events.
## Key Domain Models
### Fantasy Draft System
1. **League** - Container for multiple seasons, has commissioners
2. **Season** - Single year/iteration of a league with specific settings
- Status: `pre_draft``draft``active``completed`
- Stores draft settings (rounds, timer settings, etc.)
3. **Team** - Belongs to a season, owned by a user
4. **Draft Slot** - Defines draft order for teams in a season
5. **Draft Pick** - Record of each pick made during draft
6. **Draft Queue** - Pre-ordered list of participants a team wants to draft
7. **Draft Timer** - Active timer for current team on the clock
8. **Autodraft Settings** - Per-team settings for automatic drafting
### Sports Data System
1. **Sport** - Base sport (e.g., NFL, NBA) with type (team/individual)
2. **Sports Season** - Specific season of a sport (e.g., "2024 NFL Season")
3. **Participant** - Individual athlete or team that can be drafted
4. **Season Template** - Reusable configuration for league seasons
5. **Season Sport** - Links a fantasy season to sports it covers
6. **Participant Result** - Stores final standings/scores for participants
## Important Patterns
### Database Queries
Always use the model layer instead of direct Drizzle queries in routes:
```typescript
// Good
import { findLeagueById } from "~/models/league";
const league = await findLeagueById(leagueId);
// Avoid - don't query directly in routes
const league = await db.query.leagues.findFirst({...});
```
### Authentication
- User data synced from Clerk via webhook (`routes/api/webhooks/clerk.ts`)
- Check `clerkId` for ownership validation
- Admin checks via `isAdmin` boolean on users table
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
### Scoring Event Dates
Game/event dates are stored in **two different places** depending on the event type, and any date-based query must account for both:
1. **`scoringEvents.eventDate`** (`date` column, `YYYY-MM-DD` string) — set directly on the event. Used for non-bracket events (majors, final standings) and sometimes for bracket events when the admin sets a single date for the whole round.
2. **`playoffMatchGames.scheduledAt`** (`timestamp` column) — set on individual games within a bracket match. Bracket events (e.g. a playoff series) often have `eventDate = null` on the scoring event itself, with each game's exact time stored here instead.
**Pattern for date-range queries:** Use a two-step approach:
1. `selectDistinct` event IDs via left joins through `playoffMatches → playoffMatchGames`, filtering with `eventDate IN [dates] OR (scheduledAt >= dayStart AND scheduledAt <= dayEnd)`
2. `findMany` on the matched IDs to load full event data with relations
See `getEventsForDates` and `getUpcomingEventsForDraftedParticipants` in `app/models/scoring-event.ts` for the reference implementation. All timestamp bounds use UTC (`T00:00:00.000Z` / `T23:59:59.999Z`).
### Draft Logic
The draft system implements snake draft:
- Draft slots define the base order (1, 2, 3, ...)
- Each round alternates: odd rounds go 1→N, even rounds go N→1
- Current pick determined by `currentPickNumber` on season
- Timer system in `server/timer.ts` handles autopicks when time expires
### Testing Strategy
- **Unit tests**: Pure functions and utilities in `__tests__/` directories
- **Component tests**: React components using React Testing Library
- **E2E tests**: Critical user flows in `cypress/e2e/`
- **Fixtures**: Reusable test data in `app/test/fixtures/`
- See `TESTING.md` for comprehensive testing guide
**IMPORTANT: Tests are required for new features.** When adding any new feature, you must include appropriate tests:
- New model functions → unit tests in a co-located `__tests__/` directory
- New route loaders/actions → unit or integration tests for the logic
- New utility functions → unit tests
- New components with non-trivial logic → component tests
- Critical user flows → Cypress E2E tests
Do not consider a feature complete until tests are written and passing.
## Common Workflows
### Adding a New Route
**CRITICAL**: Both steps 1 and 2 are required - the route will not work if you skip step 1!
1. **FIRST**: Add route config to `app/routes.ts`
- Example: `route("admin/sports-seasons/:id/new-page", "routes/admin.sports-seasons.$id.new-page.tsx")`
- Nested admin routes go inside the `route("admin", ...)` array
2. **THEN**: Create file in `app/routes/` with corresponding name
3. Export loader/action if needed
4. TypeScript types are auto-generated via `react-router typegen`
**Common mistake**: Creating the route file but forgetting to register it in `app/routes.ts` - this will result in a 404!
### Database Changes
1. Update `database/schema.ts` with new tables/columns
2. Run `npm run db:generate` to create migration
Clean up migration chain and add verification migration (#268) * Fix broken drizzle migration chain and add prevention guidelines The migration system was broken due to three issues: 1. Snapshot 0067 had an invalid `autoincrement` field (PostgreSQL doesn't support this) causing Zod validation to fail with "data is malformed" 2. Migrations 0068 and 0069 were missing snapshot files, breaking the snapshot chain required by `drizzle-kit generate` 3. Orphaned file 0048_mean_enchantress.sql existed outside the journal Fixed by removing the invalid field, regenerating migrations 0068-0069 with proper snapshots via `drizzle-kit generate`, deleting the orphaned file, and adding a no-op verification migration (0070). Made migration 0069 idempotent with IF EXISTS/IF NOT EXISTS guards for production safety. Updated CLAUDE.md with rules to prevent manual migration/journal editing and ensure snapshots are always generated. https://claude.ai/code/session_01JuVHpRPa974MKSHoXNGkgU * Update package-lock.json from npm install https://claude.ai/code/session_01JuVHpRPa974MKSHoXNGkgU * Preserve original migration tags and SQL to match production hashes Restores the original tag name (0068_cs2_major_simulator) and exact SQL content for migrations 0068 and 0069 so their hashes match what's already recorded in the production __drizzle_migrations table. Also fixes the snapshot id/prevId chain to use consistent tag-based identifiers. https://claude.ai/code/session_01JuVHpRPa974MKSHoXNGkgU --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-04-06 10:53:51 -04:00
3. **Verify** the generated migration SQL in `drizzle/` is correct
4. **Verify** a corresponding snapshot was created in `drizzle/meta/` (e.g., `0070_snapshot.json`)
5. Run `npm run db:migrate` to apply migration
6. Add model functions in `app/models/` for new queries
7. Update TypeScript types if needed
**Drizzle Migration Rules (CRITICAL):**
- **NEVER** manually create migration SQL files or journal entries. Always use `drizzle-kit generate` or `drizzle-kit generate --custom` so that snapshot files are created automatically.
- **NEVER** edit `drizzle/meta/_journal.json` by hand. The journal and snapshots must stay in sync — drizzle-kit manages both.
- Every migration **must** have a matching snapshot in `drizzle/meta/`. Missing snapshots cause `drizzle-kit generate` to fail with "data is malformed" errors, which blocks all future migration generation.
- Snapshot JSON must only contain PostgreSQL-valid fields. Do not add `"autoincrement"` to column definitions — PostgreSQL uses sequences/serial instead, and invalid fields cause Zod validation failures.
- If `drizzle-kit generate` prompts for column renames interactively, use `drizzle-kit generate --custom` instead and write the migration SQL by hand (include data backfill logic as needed).
- After generating, run `drizzle-kit generate` again to confirm "No schema changes" — this validates the snapshot chain is healthy.
- For destructive migrations (dropping columns, renaming), make the SQL idempotent with `IF EXISTS`/`IF NOT EXISTS` guards where possible.
### Adding Socket.IO Events
1. Define event types in `server/socket.d.ts` interfaces
2. Add event handler in `server/socket.ts`
3. Emit events from server code using `getSocketIO().to(room).emit()`
4. Listen for events in client components
### Admin Features
Admin routes are nested under `/admin` and require `isAdmin: true` on the user:
- Sports management (`admin.sports.*`)
- Sports seasons (`admin.sports-seasons.*`)
- Season templates (`admin.templates.*`)
- Data sync utilities (`admin.data-sync.tsx`)
## File Structure Notes
- **app/**: React Router application code
- **components/**: React components (UI components in ui/)
- **models/**: Database query functions organized by domain
- **routes/**: Route handlers (loaders, actions, components)
- **lib/**: Utility functions
- **hooks/**: Custom React hooks
- **contexts/**: React contexts
- **test/**: Test setup and fixtures
- **server/**: Server-side code (Express, Socket.IO, timers)
- **database/**: Drizzle schema and context
- **drizzle/**: Generated migrations
- **cypress/**: E2E tests
## Environment Variables
Required in `.env`:
- `DATABASE_URL` - PostgreSQL connection string
- Clerk variables for authentication (see `.env.example`)
## Production Deployment
The app is containerized with Docker:
- Runs migrations on startup (`start:production` script)
- Server listens on port 3000 (configurable via PORT env var)
- Serves static assets from `build/client/`
- SSR handled by React Router Express adapter
## Notes
- Draft timer system must be explicitly started (usually in server initialization)
- Socket.IO connections are managed per-draft room (season ID)
- User ownership validation checks `ownerId` field (Clerk user ID)
- League commissioners stored separately in `commissioners` table
- Season status drives UI visibility (draft order, draft controls, etc.)