# 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 // 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 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 { // 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 { // 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.