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

24 KiB

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:

// 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:

// 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:

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:

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:

class Race {
  id: RaceId
  leagueId: LeagueId
  trackId: string
  carIds: string[]
  scheduledAt: Date
  sessionName: string
  status: 'scheduled' | 'completed' | 'cancelled'
}

Integration:

// 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:

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:

class Standing {
  leagueId: LeagueId
  driverId: DriverId
  position: number
  points: number
  racesCompleted: number
}

class PointsPreset {
  name: string
  pointsMap: Map<number, number> // position → points
}

Calculation:

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

// 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

// 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:

// 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:

// 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):

// 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:

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:

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:

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:

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:

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:

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-launchalpha 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:

// Alpha
const driverRepo = new InMemoryDriverRepository();

// Production
const driverRepo = new PostgresDriverRepository(pool);

Authentication:

// Alpha: No-op adapter
class NoAuthService implements IAuthService {
  async getCurrentUser() { return mockDriver; }
}

// Production
class NextAuthService implements IAuthService {
  async getCurrentUser() { return getServerSession(); }
}

API Integration:

// 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:

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