* 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>
336 lines
10 KiB
Markdown
336 lines
10 KiB
Markdown
# Omni League Draft Rules - Implementation Summary
|
||
|
||
## Overview
|
||
Successfully implemented Omni league draft eligibility rules that enforce:
|
||
1. **Team Flex Pick Limits**: Teams can only use (totalRounds - totalSports) flex picks
|
||
2. **Global Sport Availability**: Prevents teams from drafting in a way that blocks other teams from meeting the "at least one from each sport" requirement
|
||
|
||
## Implementation Status: ✅ COMPLETE
|
||
|
||
All phases have been completed and type-checked successfully.
|
||
|
||
---
|
||
|
||
## Phase 1: Core Logic ✅
|
||
|
||
### Files Created
|
||
- `app/lib/draft-eligibility.ts` - Core eligibility calculation with full TypeScript types
|
||
- `app/lib/__tests__/draft-eligibility.test.ts` - Comprehensive test suite (13 tests, all passing)
|
||
|
||
### Key Functions
|
||
- `calculateDraftEligibility()` - Main eligibility calculator
|
||
- Inputs: team picks, all picks, all participants, sports, rounds, teams
|
||
- Returns: eligible sports, ineligible reasons, flex pick status, sport availability
|
||
- `getEligibilitySummary()` - Human-readable status summary
|
||
|
||
### Test Coverage
|
||
- Team flex pick constraints (3 tests)
|
||
- Global sport availability constraints (4 tests)
|
||
- Combined constraints (2 tests)
|
||
- Edge cases (3 tests)
|
||
- Summary generation (1 test)
|
||
|
||
All tests pass ✅
|
||
|
||
---
|
||
|
||
## Phase 2: Backend Validation ✅
|
||
|
||
### Model Layer Enhancements
|
||
|
||
**`app/models/draft-pick.ts`**
|
||
- Added `getDraftPicksWithSports()` - Get all picks with sport data
|
||
- Added `getTeamDraftPicksWithSports()` - Get team-specific picks with sport data
|
||
|
||
**`app/models/participant.ts`**
|
||
- Added `getParticipantsForSeasonWithSports()` - Get all participants with sport association
|
||
|
||
**`app/models/season-sport.ts`**
|
||
- Added `getSeasonSportsSimple()` - Get simple sport list for calculations
|
||
|
||
**`app/models/draft-utils.ts`**
|
||
- Updated `autoPickForTeam()` - Now respects eligibility constraints
|
||
- Filters queue by eligible sports
|
||
- Falls back to eligible sports when queue is empty
|
||
- Signature changed to require `draftRounds` and `allTeamIds`
|
||
- Updated `getTopAvailableParticipant()` - Now accepts optional `eligibleSportIds` filter
|
||
|
||
### API Endpoints Updated
|
||
|
||
**`app/routes/api/draft.make-pick.ts`** ✅
|
||
- Added eligibility validation before creating pick
|
||
- Returns specific error reason if sport is ineligible
|
||
- Validates both team flex limits and global availability
|
||
|
||
**`app/routes/api/draft.force-manual-pick.ts`** ✅
|
||
- Added same eligibility validation
|
||
- Commissioner picks must also respect rules
|
||
|
||
**`app/routes/api/draft.force-autopick.ts`** ✅
|
||
- Refactored to use updated `autoPickForTeam()` function
|
||
- Automatically respects eligibility via autodraft logic
|
||
|
||
### Validation Flow
|
||
1. Load all necessary data (picks, participants, sports, teams)
|
||
2. Calculate eligibility using `calculateDraftEligibility()`
|
||
3. Check if participant's sport is in `eligibleSportIds`
|
||
4. Return 400 error with specific reason if ineligible
|
||
5. Proceed with pick if eligible
|
||
|
||
---
|
||
|
||
## Phase 3: Frontend UI ✅
|
||
|
||
### Draft Room Updates (`app/routes/leagues/$leagueId.draft.$seasonId.tsx`)
|
||
|
||
**New Imports**
|
||
- `useMemo` from React
|
||
- `calculateDraftEligibility` and `getEligibilitySummary` from eligibility lib
|
||
|
||
**Eligibility Calculation**
|
||
- Added `useMemo` hook to calculate user team's eligibility
|
||
- Recalculates when picks, participants, or team changes
|
||
- Transforms loader data to match eligibility function signature
|
||
|
||
**Force Pick Eligibility**
|
||
- Separate `useMemo` for force manual pick dialog
|
||
- Calculates eligibility for the selected team (not current user)
|
||
|
||
### Visual Indicators
|
||
|
||
**Participant Table**
|
||
1. **Row Styling**
|
||
- Ineligible participants: red background (`bg-red-50/30 dark:bg-red-950/20`)
|
||
- Drafted participants: muted with opacity
|
||
- Eligible participants: normal hover state
|
||
|
||
2. **Badges**
|
||
- "Ineligible" badge (destructive variant) for blocked sports
|
||
- Shows ineligible reason on hover
|
||
- "Drafted" badge for already picked participants
|
||
- "Queued" badge only shown if participant is also eligible
|
||
|
||
3. **Buttons**
|
||
- "Pick" button disabled when sport is ineligible
|
||
- "Add to Queue" button disabled for ineligible sports
|
||
- Tooltips explain why buttons are disabled
|
||
|
||
### Status Banner (in "My Queue" section)
|
||
|
||
**Displays:**
|
||
- Flex picks used/available with color coding:
|
||
- Green when flexes remain
|
||
- Red when exhausted
|
||
- Picks by sport breakdown (e.g., "NFL: 2, NBA: 1")
|
||
- Active restrictions with reasons (first 2 shown)
|
||
- Example: "NFL: Would prevent other teams from getting NFL (2 left for 3 teams)"
|
||
|
||
**Styling:**
|
||
- Bordered card with muted background
|
||
- Clear hierarchy with sections
|
||
- Color-coded warnings
|
||
|
||
### Force Manual Pick Dialog
|
||
|
||
**Enhancements:**
|
||
- Uses `forcePickEligibility` calculation for target team
|
||
- Filters out ineligible participants:
|
||
- Red background for ineligible rows
|
||
- "Ineligible" badge with reason
|
||
- Disabled "Draft" button
|
||
- Shows participant sport and EV
|
||
- Maintains existing search and filter functionality
|
||
|
||
---
|
||
|
||
## Technical Details
|
||
|
||
### Eligibility Algorithm
|
||
|
||
**Team Flex Pick Check:**
|
||
```typescript
|
||
flexPicksUsed = sum of (picks in sport - 1) for each sport with >1 pick
|
||
flexPicksAvailable = totalRounds - totalSports
|
||
hasFlexesRemaining = flexPicksUsed < flexPicksAvailable
|
||
```
|
||
|
||
**Global Sport Availability:**
|
||
```typescript
|
||
For each sport:
|
||
undraftedCount = total participants - drafted participants
|
||
teamsStillNeeding = teams with 0 picks in this sport
|
||
|
||
canDraftAsFirst = undraftedCount >= teamsStillNeeding
|
||
canDraftAsFlex = undraftedCount > teamsStillNeeding
|
||
```
|
||
|
||
**Combined Logic:**
|
||
- If team hasn't drafted from sport: allowed if `canDraftAsFirst`
|
||
- If team has drafted from sport (flex): allowed if `hasFlexesRemaining AND canDraftAsFlex`
|
||
|
||
### Data Flow
|
||
|
||
1. **Loader** loads all necessary data (picks, participants, sports, teams)
|
||
2. **Component** calculates eligibility in `useMemo`
|
||
3. **UI** renders with eligibility state
|
||
4. **User action** triggers API call
|
||
5. **API** validates eligibility server-side
|
||
6. **Socket** broadcasts pick to all clients
|
||
7. **Clients** update state and recalculate eligibility
|
||
|
||
### Performance
|
||
|
||
- Eligibility calculation: O(teams × rounds) ≈ O(200) for typical draft
|
||
- Memoized in React - only recalculates when picks change
|
||
- No database changes needed
|
||
- Minimal API overhead (one calculation per pick attempt)
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
### Unit Tests ✅
|
||
- **21 comprehensive tests** covering all eligibility scenarios:
|
||
- Team flex pick constraints (3 tests)
|
||
- Global sport availability constraints (4 tests)
|
||
- Combined constraints (2 tests)
|
||
- Edge cases (3 tests)
|
||
- Summary generation (1 test)
|
||
- **Exact boundary conditions (6 tests)** - Testing critical thresholds
|
||
- **Draft progression simulation (1 test)** - Multi-pick scenarios
|
||
- **Late-draft forced choices (2 tests)** - Constraint-driven selections
|
||
- Run with: `npm test -- app/lib/__tests__/draft-eligibility.test.ts`
|
||
- **All 21 tests passing** ✅
|
||
|
||
### Test Coverage Details
|
||
The test suite thoroughly validates:
|
||
- ✅ Team cannot exceed flex pick limit
|
||
- ✅ Global sport availability protects future teams
|
||
- ✅ Exact boundary conditions (e.g., undrafted === teamsNeeding)
|
||
- ✅ Eligibility changes as draft progresses
|
||
- ✅ Late-draft scenarios where only one sport is eligible
|
||
- ✅ Complex multi-team scenarios
|
||
|
||
### Type Safety ✅
|
||
- Full TypeScript coverage
|
||
- Run with: `npm run typecheck`
|
||
- No errors
|
||
|
||
### Integration Testing Notes
|
||
- The core eligibility algorithm is thoroughly unit tested (21 tests)
|
||
- Backend API endpoints include inline eligibility validation
|
||
- Autodraft logic (`draft-utils.ts`) uses the tested eligibility function
|
||
- Manual/E2E testing recommended for full integration validation
|
||
|
||
### Manual Testing Needed
|
||
- [ ] E2E test for draft flow with eligibility restrictions
|
||
- [ ] Test autodraft respects eligibility in live draft
|
||
- [ ] Test force picks respect eligibility
|
||
- [ ] Test UI shows correct eligibility status and messaging
|
||
- [ ] Test with various team/sport/round configurations
|
||
|
||
---
|
||
|
||
## Files Modified
|
||
|
||
### Created
|
||
1. `app/lib/draft-eligibility.ts`
|
||
2. `app/lib/__tests__/draft-eligibility.test.ts`
|
||
|
||
### Modified
|
||
1. `app/models/draft-pick.ts`
|
||
2. `app/models/participant.ts`
|
||
3. `app/models/season-sport.ts`
|
||
4. `app/models/draft-utils.ts`
|
||
5. `app/routes/api/draft.make-pick.ts`
|
||
6. `app/routes/api/draft.force-manual-pick.ts`
|
||
7. `app/routes/api/draft.force-autopick.ts`
|
||
8. `app/routes/leagues/$leagueId.draft.$seasonId.tsx`
|
||
|
||
---
|
||
|
||
## Usage Examples
|
||
|
||
### Backend Validation
|
||
```typescript
|
||
// In API route
|
||
const eligibility = calculateDraftEligibility(
|
||
teamId,
|
||
teamPicks,
|
||
allPicks,
|
||
allParticipants,
|
||
seasonSports,
|
||
season.draftRounds,
|
||
allTeams
|
||
);
|
||
|
||
if (!eligibility.eligibleSportIds.has(participant.sport.id)) {
|
||
return Response.json({
|
||
error: eligibility.ineligibleReasons[participant.sport.id]
|
||
}, { status: 400 });
|
||
}
|
||
```
|
||
|
||
### Frontend UI
|
||
```typescript
|
||
// In component
|
||
const eligibility = useMemo(() => {
|
||
if (!userTeam) return null;
|
||
return calculateDraftEligibility(/*...*/);
|
||
}, [userTeam, picks, availableParticipants, season]);
|
||
|
||
// In render
|
||
const isEligible = eligibility?.eligibleSportIds.has(participant.sport.id);
|
||
const ineligibleReason = eligibility?.ineligibleReasons[participant.sport.id];
|
||
|
||
<Button disabled={!isEligible} title={ineligibleReason}>
|
||
Pick
|
||
</Button>
|
||
```
|
||
|
||
---
|
||
|
||
## Edge Cases Handled
|
||
|
||
1. **Single sport league**: Flex picks still apply (rounds - 1)
|
||
2. **No eligible sports**: API returns clear error, autodraft falls back
|
||
3. **All teams drafted from sport**: Global constraint becomes `undrafted > 0`
|
||
4. **Empty participant pool**: Returns null, handled gracefully
|
||
5. **Mid-draft state changes**: Eligibility recalculates on every pick
|
||
|
||
---
|
||
|
||
## Future Enhancements
|
||
|
||
As documented in the plan:
|
||
1. Pre-draft validation (warn commissioners of impossible configs)
|
||
2. Queue validation warnings
|
||
3. Scarcity indicators on participant list
|
||
4. Draft strategy recommendations
|
||
5. Configurable rules per league
|
||
6. Position-based restrictions within sports
|
||
7. Commissioner undo/correction tools
|
||
8. Analytics on constraint triggers
|
||
|
||
---
|
||
|
||
## Deployment Checklist
|
||
|
||
- [x] All code written and tested
|
||
- [x] TypeScript compilation successful
|
||
- [x] Unit tests passing
|
||
- [ ] Manual testing complete
|
||
- [ ] E2E tests written
|
||
- [ ] Database migrations (none needed)
|
||
- [ ] Documentation updated
|
||
- [ ] Code review completed
|
||
|
||
---
|
||
|
||
## Notes
|
||
|
||
- No database schema changes required
|
||
- All validation happens in application layer
|
||
- Backward compatible (will work with existing drafts)
|
||
- Performance impact minimal (simple calculations)
|
||
- User experience significantly improved with clear feedback
|