1063 lines
24 KiB
Markdown
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. |