Files
gridpilot.gg/docs/ALPHA_PLAN.md
2025-12-03 00:46:08 +01:00

1063 lines
24 KiB
Markdown

# GridPilot Alpha Development Plan
## Overview
**Mission:** Prove automation + unified management value proposition through functional alpha.
**Scope:** In-memory web app demonstrating core league management workflows integrated with companion automation.
**Timeline:** Alpha validates technical approach; production follows with persistent infrastructure.
**Value Proof:**
- Automation reduces admin workload (companion integration)
- Unified platform consolidates fragmented tools (web app)
- Clean Architecture enables rapid iteration (in-memory → production swap)
---
## Technical Approach
### Architecture Strategy
**Production domain logic, swappable infrastructure:**
```
Domain (Production) ← Business rules, invariants
Application (Production) ← Use cases, orchestration
Infrastructure (Swappable) ← In-memory adapters for alpha
Presentation (Production) ← Next.js UI components
```
**Alpha = Real code + Fake persistence**
- Domain entities enforce actual business rules
- Application use cases implement real workflows
- Infrastructure uses in-memory adapters (no database)
- Presentation renders production-ready UI
**Post-Alpha Migration:**
```typescript
// Alpha
const driverRepo = new InMemoryDriverRepository();
// Production
const driverRepo = new PostgresDriverRepository(pool);
```
Same ports, same use cases, different adapters.
---
### Mode System Design
**Current:** `pre-launch` | `post-launch`
**Alpha:** `pre-launch` | `alpha`
**Mode Behavior:**
- `pre-launch`: Landing page only (current state)
- `alpha`: Full app UI with in-memory data (no auth, no persistence)
**Implementation:**
```typescript
// lib/mode.ts
export type AppMode = 'pre-launch' | 'alpha';
// Middleware gates
if (mode === 'alpha') {
// Enable /dashboard, /leagues, /profile routes
// Inject in-memory repositories
}
```
**Environment Control:**
```
GRIDPILOT_MODE=alpha npm run dev
```
---
## Features
### F1: Driver Profile Creation
**User Story:** Driver creates single profile to participate in leagues.
**Acceptance Criteria:**
- Driver enters: name, racing number, iRacing ID
- System validates: unique number per league, valid ID format
- Profile displays: basic info, empty race history
- Single profile per user (alpha constraint)
**Domain Model:**
```typescript
class Driver {
id: DriverId
name: string
racingNumber: number
iRacingId: string
createdAt: Date
}
```
**UI Flow:**
1. `/profile/create` form
2. Validation feedback (inline)
3. Success → redirect to `/dashboard`
4. Profile visible at `/profile`
---
### F2: League Management
**User Story:** Admin creates league, drivers join, schedule displays.
**Acceptance Criteria:**
- Admin creates league: name, description, rules
- System generates: league ID, creation timestamp
- Drivers browse available leagues
- Drivers join single league (alpha constraint)
- League page shows: roster, schedule placeholder, standings
**Domain Model:**
```typescript
class League {
id: LeagueId
name: string
adminId: DriverId
description: string
rules: string
members: DriverId[]
createdAt: Date
}
```
**UI Flow:**
1. `/leagues/create` (admin only)
2. `/leagues` (browse/join)
3. `/leagues/:id` (league detail)
4. Join button → updates roster
5. Leave button available
---
### F3: Race Scheduling via Companion
**User Story:** Admin schedules race through companion, web app displays schedule.
**Acceptance Criteria:**
- Admin configures race in companion app
- Companion sends race metadata to web app (IPC/API stub)
- Web app displays: track, cars, date/time, session settings
- Schedule shows upcoming races
- Past races marked as completed
**Domain Model:**
```typescript
class Race {
id: RaceId
leagueId: LeagueId
trackId: string
carIds: string[]
scheduledAt: Date
sessionName: string
status: 'scheduled' | 'completed' | 'cancelled'
}
```
**Integration:**
```typescript
// Companion → Web App (simulated in alpha)
interface RaceScheduleRequest {
leagueId: string
sessionConfig: HostedSessionConfig
scheduledAt: Date
}
```
**UI Flow:**
1. `/leagues/:id/schedule` displays races
2. Each race shows: date, track, cars
3. Status badge: upcoming/completed
4. Click race → detail view
---
### F4: Results Display (Manual Import)
**User Story:** Admin imports results CSV, system displays classification and updates standings.
**Acceptance Criteria:**
- Admin uploads iRacing results CSV
- System parses: positions, drivers, incidents, lap times
- Classification displays immediately
- Standings auto-calculate
- Results linked to scheduled race
**Domain Model:**
```typescript
class RaceResult {
id: ResultId
raceId: RaceId
driverId: DriverId
position: number
incidents: number
fastestLap?: number
status: 'finished' | 'dnf' | 'dsq'
}
```
**UI Flow:**
1. `/leagues/:id/races/:raceId/results/import`
2. CSV upload field
3. Preview parsed results
4. Confirm → save
5. Redirect to `/leagues/:id/races/:raceId/results`
---
### F5: Standings Calculation
**User Story:** System calculates driver and team standings from race results.
**Acceptance Criteria:**
- Points awarded per position (configurable preset)
- Standings update after each result import
- Display: position, driver, points, races
- Team standings aggregate member points
- Drop weeks not implemented (alpha limitation)
**Domain Model:**
```typescript
class Standing {
leagueId: LeagueId
driverId: DriverId
position: number
points: number
racesCompleted: number
}
class PointsPreset {
name: string
pointsMap: Map<number, number> // position → points
}
```
**Calculation:**
```typescript
class CalculateStandingsUseCase {
execute(leagueId: LeagueId): Standing[] {
const results = resultRepo.findByLeague(leagueId);
const points = applyPointsPreset(results);
return sortByPoints(points);
}
}
```
**UI Flow:**
1. `/leagues/:id/standings` (driver view)
2. Table: pos, driver, points, races
3. Auto-updates after result import
4. Team standings at `/leagues/:id/standings/teams`
---
## Data Model (In-Memory)
### Core Entities
```typescript
// Domain/entities/Driver.ts
class Driver {
id: DriverId
name: string
racingNumber: number
iRacingId: string
currentLeagueId?: LeagueId
createdAt: Date
}
// Domain/entities/League.ts
class League {
id: LeagueId
name: string
adminId: DriverId
description: string
rules: string
memberIds: DriverId[]
pointsPreset: PointsPreset
createdAt: Date
}
// Domain/entities/Race.ts
class Race {
id: RaceId
leagueId: LeagueId
trackId: string
carIds: string[]
scheduledAt: Date
sessionName: string
status: RaceStatus
hostedSessionId?: string // Links to companion
}
// Domain/entities/RaceResult.ts
class RaceResult {
id: ResultId
raceId: RaceId
driverId: DriverId
position: number
incidents: number
fastestLapTime?: number
status: ResultStatus
}
// Domain/entities/Standing.ts
class Standing {
leagueId: LeagueId
driverId: DriverId
position: number
points: number
racesCompleted: number
updatedAt: Date
}
```
### Value Objects
```typescript
// Domain/value-objects/DriverId.ts
class DriverId {
value: string
static generate(): DriverId
}
// Domain/value-objects/RaceStatus.ts
type RaceStatus = 'scheduled' | 'completed' | 'cancelled';
// Domain/value-objects/ResultStatus.ts
type ResultStatus = 'finished' | 'dnf' | 'dsq';
// Domain/value-objects/PointsPreset.ts
class PointsPreset {
name: string
pointsMap: Map<number, number>
static F1_2024 = new PointsPreset('F1 2024', new Map([
[1, 25], [2, 18], [3, 15], [4, 12], [5, 10],
[6, 8], [7, 6], [8, 4], [9, 2], [10, 1]
]));
}
```
---
## UI/UX Flow
### User Journey: Driver Registration → Race → Results
**Step 1: Landing (Pre-Launch Mode)**
- Visit `/`
- See landing page
- Email signup (not functional in alpha)
**Step 2: Switch to Alpha Mode**
- `GRIDPILOT_MODE=alpha`
- Reload page
- Landing replaced with dashboard redirect
**Step 3: Create Profile**
- Visit `/profile/create`
- Enter: "John Doe", number 42, iRacing ID "123456"
- Submit → profile created
- Redirect `/dashboard`
**Step 4: Join League**
- Visit `/leagues`
- See available leagues
- Click "European GT League"
- Review schedule, rules
- Click "Join League"
- Confirmation modal
- Success → league roster updated
**Step 5: View Schedule**
- Dashboard shows next race
- Visit `/leagues/:id/schedule`
- See upcoming: "Monza GP - March 15, 2025"
- Race details: GT3, 60min, 19:00 CET
**Step 6: Race Day (External)**
- Driver joins iRacing hosted session
- Companion automation created session
- Driver races (outside GridPilot)
**Step 7: Results Import (Admin)**
- Admin downloads iRacing CSV
- Visit `/leagues/:id/races/:raceId/results/import`
- Upload CSV
- Preview shows classification
- Confirm import
**Step 8: View Results & Standings**
- Driver visits `/leagues/:id/races/:raceId/results`
- Sees: 3rd place, 2x incidents, 1:42.5 fastest lap
- Click "Standings"
- Driver standings show: 15 points, 1 race
- Team standings aggregate teammates
---
## Companion Integration
### Race Scheduling Flow
**Companion Side:**
```typescript
// Companion creates hosted session
const sessionConfig: HostedSessionConfig = {
sessionName: 'European GT - Monza GP',
trackId: 'monza-gp',
carIds: ['porsche-911-gt3-r'],
scheduledAt: new Date('2025-03-15T19:00:00Z'),
// ... full config
};
// Send to web app (alpha: IPC stub)
await webAppClient.scheduleRace({
leagueId: 'league-123',
sessionConfig,
});
```
**Web App Side:**
```typescript
// Application/use-cases/ScheduleRaceUseCase.ts
class ScheduleRaceUseCase {
async execute(req: ScheduleRaceRequest): Promise<Race> {
// Validate league exists
const league = await leagueRepo.findById(req.leagueId);
// Create race entity
const race = Race.create({
leagueId: req.leagueId,
trackId: req.sessionConfig.trackId,
carIds: req.sessionConfig.carIds,
scheduledAt: req.scheduledAt,
sessionName: req.sessionConfig.sessionName,
status: 'scheduled',
});
// Persist (in-memory for alpha)
await raceRepo.save(race);
return race;
}
}
```
**Alpha Implementation:**
- Companion → Web App via simulated IPC
- Web app stores race in `InMemoryRaceRepository`
- Schedule view renders from repository
- No actual Playwright automation in web app
**Post-Alpha:**
- Companion → Web App via REST API
- Web app stores in PostgreSQL
- Companion polls for scheduled races
- Full automation execution
---
### Result Import Flow
**Manual Import (Alpha):**
```typescript
// Application/use-cases/ImportRaceResultsUseCase.ts
class ImportRaceResultsUseCase {
async execute(raceId: RaceId, csvData: string): Promise<RaceResult[]> {
// Parse CSV
const parsed = parseIRacingCSV(csvData);
// Validate race exists and is completed
const race = await raceRepo.findById(raceId);
// Map to domain entities
const results = parsed.map(row => RaceResult.create({
raceId,
driverId: findDriverByIRacingId(row.customerId),
position: row.finishPos,
incidents: row.incidents,
fastestLapTime: row.bestLapTime,
status: row.reasonOut ? 'dnf' : 'finished',
}));
// Persist
await resultRepo.saveMany(results);
// Trigger standings recalculation
await calculateStandingsUseCase.execute(race.leagueId);
return results;
}
}
```
**Post-Alpha Automation:**
- Web app calls iRacing API directly
- Automated result fetch after session end
- No CSV upload required
---
## Testing Strategy
### Unit Tests
**Domain Layer:**
```typescript
describe('Driver', () => {
it('validates racing number uniqueness per league', () => {
// Test domain invariants
});
});
describe('Standing', () => {
it('calculates points from results', () => {
// Test scoring logic
});
});
describe('PointsPreset', () => {
it('applies F1 2024 points correctly', () => {
// Test point mapping
});
});
```
**Application Layer:**
```typescript
describe('CreateDriverProfileUseCase', () => {
it('creates driver with valid data', async () => {
const repo = new InMemoryDriverRepository();
const useCase = new CreateDriverProfileUseCase(repo);
const driver = await useCase.execute({
name: 'John Doe',
racingNumber: 42,
iRacingId: '123456',
});
expect(driver.name).toBe('John Doe');
});
it('rejects duplicate racing number in same league', async () => {
// Test business rule enforcement
});
});
```
### Integration Tests
**Repository Layer:**
```typescript
describe('InMemoryDriverRepository', () => {
it('persists and retrieves driver', async () => {
const repo = new InMemoryDriverRepository();
const driver = Driver.create({...});
await repo.save(driver);
const found = await repo.findById(driver.id);
expect(found).toEqual(driver);
});
it('handles concurrent writes', async () => {
// Test in-memory consistency
});
});
```
**Use Case Workflows:**
```typescript
describe('Race Result Import Workflow', () => {
it('imports CSV and updates standings', async () => {
// Setup: league, drivers, scheduled race
// Execute: import CSV
// Assert: results saved, standings updated
});
});
```
### E2E Tests (Playwright)
**User Flows:**
```typescript
test('driver can create profile and join league', async ({ page }) => {
await page.goto('/profile/create');
await page.fill('[name="name"]', 'John Doe');
await page.fill('[name="racingNumber"]', '42');
await page.fill('[name="iRacingId"]', '123456');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await page.goto('/leagues');
await page.click('text=European GT League');
await page.click('text=Join League');
await expect(page.locator('.roster')).toContainText('John Doe');
});
```
**Integration with Companion:**
```typescript
test('admin schedules race via companion integration', async ({ page }) => {
// Simulate companion IPC call
await simulateCompanionScheduleRace({
leagueId: 'league-123',
sessionConfig: { /* ... */ },
});
// Verify web app displays scheduled race
await page.goto('/leagues/league-123/schedule');
await expect(page.locator('.race-card')).toContainText('Monza GP');
});
```
---
## Success Metrics
### Technical Validation
**Architecture Proof:**
- [ ] All use cases testable in isolation
- [ ] Domain logic has zero infrastructure dependencies
- [ ] In-memory adapters swap cleanly with production stubs
- [ ] No business logic in presentation layer
**Feature Completeness:**
- [ ] Driver can create profile
- [ ] Driver can join single league
- [ ] Admin can create league
- [ ] Schedule displays companion-scheduled races
- [ ] Results import updates standings
- [ ] Standings calculate correctly
### User Experience Validation
**Admin Workflow:**
- [ ] Admin creates league in <2 minutes
- [ ] Admin imports results in <30 seconds
- [ ] Admin views standings with zero manual calculation
**Driver Workflow:**
- [ ] Driver creates profile in <1 minute
- [ ] Driver finds and joins league in <2 minutes
- [ ] Driver views schedule without asking admin
- [ ] Driver sees results immediately after import
### Value Proposition Validation
**Automation Integration:**
- [ ] Companion schedules race → web app displays schedule (proves connectivity)
- [ ] Web app schedule guides companion automation (proves unified workflow)
**Unified Management:**
- [ ] Single source of truth for league data
- [ ] No Discord/spreadsheet required for standings
- [ ] Driver profile persists across sessions (in-memory during session)
---
## Implementation Phases
### Phase 1: Foundation (Week 1-2)
**Deliverables:**
- [ ] Mode system: `pre-launch``alpha` transition
- [ ] Middleware: mode-gated routes
- [ ] Domain models: Driver, League, Race, RaceResult, Standing
- [ ] In-memory repositories: all entities
- [ ] Basic UI layout: dashboard, navigation
**Dependencies:**
- Clean Architecture patterns from companion
- Next.js 15 app router
- TypeScript strict mode
---
### Phase 2: Driver & League Core (Week 3-4)
**Deliverables:**
- [ ] Use case: CreateDriverProfile
- [ ] Use case: CreateLeague
- [ ] Use case: JoinLeague
- [ ] UI: `/profile/create`
- [ ] UI: `/leagues` (browse)
- [ ] UI: `/leagues/create`
- [ ] UI: `/leagues/:id` (detail)
- [ ] Unit tests: domain validation
- [ ] Integration tests: repository operations
**Dependencies:**
- Phase 1 complete
- Driver/League value objects
---
### Phase 3: Scheduling & Companion Integration (Week 5-6)
**Deliverables:**
- [ ] Use case: ScheduleRace (from companion)
- [ ] IPC stub: companion → web app communication
- [ ] Domain model: Race status transitions
- [ ] UI: `/leagues/:id/schedule`
- [ ] UI: Race detail cards
- [ ] Integration tests: companion scheduling flow
**Dependencies:**
- Phase 2 complete
- Companion POC running
- HostedSessionConfig understanding
---
### Phase 4: Results & Standings (Week 7-8)
**Deliverables:**
- [ ] Use case: ImportRaceResults
- [ ] Use case: CalculateStandings
- [ ] CSV parser: iRacing results format
- [ ] PointsPreset: F1 2024 preset
- [ ] UI: `/leagues/:id/races/:raceId/results/import`
- [ ] UI: `/leagues/:id/races/:raceId/results`
- [ ] UI: `/leagues/:id/standings`
- [ ] Unit tests: points calculation
- [ ] Integration tests: results → standings pipeline
**Dependencies:**
- Phase 3 complete
- iRacing CSV samples
---
### Phase 5: Polish & Testing (Week 9-10)
**Deliverables:**
- [ ] E2E tests: full user flows
- [ ] Error handling: all use cases
- [ ] Loading states: all async operations
- [ ] Mobile responsive: all views
- [ ] Theme compliance: THEME.md
- [ ] Voice compliance: VOICE.md
- [ ] Documentation: alpha usage guide
**Dependencies:**
- Phase 4 complete
- Design system from landing page
---
## Known Limitations (Alpha Scope)
### Explicitly Deferred Features
**Authentication & Authorization:**
- No login/signup
- No user sessions
- No permission checks
- **Rationale:** In-memory data resets on restart; auth irrelevant for testing workflows
**Data Persistence:**
- No database
- No file storage
- Data clears on server restart
- **Rationale:** Alpha validates domain logic and UI flows, not persistence
**Multi-League Support:**
- Driver joins single league only
- No cross-league profile
- No multi-league standings
- **Rationale:** Simplifies alpha scope; multi-league in production
**Advanced Scoring:**
- No drop weeks
- No bonus points (fastest lap, etc.)
- Single points preset only
- **Rationale:** Complex scoring deferred to production
**Team Competition:**
- No team management
- No team standings
- No team profiles
- **Rationale:** Driver/admin flows first; teams in production
**Result Automation:**
- Manual CSV import only
- No iRacing API integration
- No live result fetch
- **Rationale:** Proves import pipeline; automation post-alpha
**Complaint & Penalty System:**
- Not implemented in alpha
- **Rationale:** Core competition flows first; penalties in production
**Multi-Sim Support:**
- iRacing only
- **Rationale:** Alpha validates single-sim workflows
**Rating System:**
- Not implemented in alpha
- **Rationale:** Requires production data volume
---
## Alpha Success Definition
**Alpha is successful if:**
1. **Technical:** In-memory adapters swap cleanly with production stubs (no domain/app changes)
2. **Functional:** All 5 core features work end-to-end
3. **Integration:** Companion schedules race, web app displays it
4. **Value:** Admin workload reduced by automation visibility
5. **Architecture:** Clean Architecture boundaries respected (testable via unit tests)
**Alpha is ready for production when:**
1. Use cases remain unchanged
2. Only infrastructure layer needs replacement
3. UI components are production-ready
4. Theme and voice guidelines followed
5. E2E tests pass consistently
---
## Post-Alpha Migration Path
### Infrastructure Swap
**Repositories:**
```typescript
// Alpha
const driverRepo = new InMemoryDriverRepository();
// Production
const driverRepo = new PostgresDriverRepository(pool);
```
**Authentication:**
```typescript
// Alpha: No-op adapter
class NoAuthService implements IAuthService {
async getCurrentUser() { return mockDriver; }
}
// Production
class NextAuthService implements IAuthService {
async getCurrentUser() { return getServerSession(); }
}
```
**API Integration:**
```typescript
// Alpha: IPC stub
class StubCompanionClient implements ICompanionClient {
async scheduleRace() { /* in-memory */ }
}
// Production
class HTTPCompanionClient implements ICompanionClient {
async scheduleRace() { return fetch('/api/races'); }
}
```
### Zero Domain Changes
- `Driver`, `League`, `Race` entities unchanged
- `CreateDriverProfileUseCase` unchanged
- `CalculateStandingsUseCase` unchanged
- UI components unchanged
**Only wiring changes:**
```typescript
// apps/website/app/providers.tsx
const mode = getAppMode();
const repositories = mode === 'alpha'
? createInMemoryRepositories()
: createProductionRepositories(db);
```
---
## Theme & Voice Compliance
### Visual Guidelines (THEME.md)
**Colors:**
- Deep Graphite `#0E0F11` (background)
- Primary Blue `#198CFF` (accents)
- Performance Green `#6FE37A` (success states)
**Typography:**
- Inter font family
- Semibold headings
- Light/regular body
**Animations:**
- 150-250ms transitions
- Subtle hover states
- Smooth scrolling
**Components:**
- 6-8px rounded cards
- Soft shadows (blur 20-28px)
- Hover glow on interactive elements
### Voice Guidelines (VOICE.md)
**Tone:**
- Calm, clear, direct
- No corporate jargon
- No startup hype
- No gamer slang
**Examples:**
✅ "Profile created."
❌ "Awesome! Your profile is ready to rock!"
✅ "League joined."
❌ "Congratulations! You're now part of an amazing community!"
✅ "Results imported."
❌ "Success! We've synced your race data!"
---
## Architectural Decisions
### ADR-001: In-Memory Adapters for Alpha
**Decision:** Use in-memory repositories, not database, for alpha.
**Rationale:**
- Validates domain logic without infrastructure complexity
- Faster iteration on business rules
- Clean Architecture principles: domain independent of persistence
- Production migration only touches infrastructure layer
**Trade-offs:**
- Data lost on restart (acceptable for alpha testing)
- No concurrent user support (single-user alpha)
- Limited data volume testing (acceptable for workflows)
---
### ADR-002: Mode System vs Feature Flags
**Decision:** Use environment-based mode system (`GRIDPILOT_MODE=alpha`), not feature flags.
**Rationale:**
- Clear separation: landing vs alpha vs production
- No flag management complexity
- Environment-appropriate defaults
- Aligns with existing `pre-launch`/`post-launch` pattern
**Trade-offs:**
- Requires redeployment to switch modes (acceptable for alpha)
- No gradual rollout (not needed for internal alpha)
---
### ADR-003: Manual Result Import for Alpha
**Decision:** Admin uploads CSV, no iRacing API integration in alpha.
**Rationale:**
- Validates result processing pipeline
- Avoids iRacing API rate limits during testing
- Simpler alpha scope
- CSV upload pattern works for production fallback
**Trade-offs:**
- Manual admin step (acceptable for alpha)
- No live result sync (deferred to production)
---
### ADR-004: Single League Per Driver for Alpha
**Decision:** Drivers join one league maximum in alpha.
**Rationale:**
- Simplifies UI flows
- Reduces data model complexity
- Validates core competition workflow
- Multi-league requires persistent identity (production concern)
**Trade-offs:**
- Cannot test cross-league scenarios (acceptable for alpha)
- Multi-league profile in production
---
### ADR-005: No Authentication in Alpha
**Decision:** No login, no sessions, no permissions.
**Rationale:**
- In-memory data has no persistent identity
- Auth complexity distracts from workflow testing
- Production auth requires database anyway
- Alpha tests business logic, not security
**Trade-offs:**
- Cannot test multi-user scenarios (acceptable for alpha)
- Auth added in production with database
---
## Summary
**Alpha proves:** Automation + unified management reduces admin workload and consolidates fragmented tools.
**Technical approach:** Production domain/application logic with in-memory infrastructure.
**Core features:** Driver profiles, league management, race scheduling, result import, standings calculation.
**Success criteria:** Clean Architecture validated, workflows functional, companion integration working.
**Next step:** Production migration swaps only infrastructure adapters; domain and UI unchanged.
**Timeline:** 10 weeks to functional alpha demonstrating value proposition.