working companion prototype

This commit is contained in:
2025-11-24 23:32:36 +01:00
parent e7978024d7
commit e2bea9a126
175 changed files with 23227 additions and 3519 deletions

View File

@@ -4,6 +4,44 @@ This document provides a technical deep-dive into GridPilot's Clean Architecture
---
## iRacing Automation Strategy
**IMPORTANT**: Understanding the distinction between iRacing's interfaces is critical for our automation approach.
### Two iRacing Interfaces
1. **iRacing Website (members.iracing.com)**: Standard HTML/DOM web application accessible at `https://members-ng.iracing.com/`. This is where hosted session management lives. Being a standard web application, it can be automated with browser automation tools like **Playwright** or **Puppeteer**. This is 100% legal and our preferred approach.
2. **iRacing Desktop App (Electron)**: The iRacing desktop application is a sandboxed Electron app. Its DOM is inaccessible, and any modification violates iRacing's Terms of Service. This is why tools like iRefined were shut down.
### Automation Rules
**Allowed Approaches:**
- ✅ Browser automation of the iRacing website using Playwright/Puppeteer
- ✅ Standard DOM manipulation and interaction via browser automation APIs
**Forbidden Approaches:**
- ❌ DOM automation inside the iRacing Electron desktop app
- ❌ Script injection into the desktop client
- ❌ Any client modification (similar to what got iRefined shut down)
### Technology Stack
- **Primary**: Playwright for browser automation of members.iracing.com
- **Alternative**: Puppeteer (if Playwright isn't suitable for specific use cases)
### Development vs Production Mode
- **Development Mode**: Launches a Playwright-controlled browser to automate the real iRacing website
- **Production Mode**: Same as development - browser automation targeting members.iracing.com
- **Test Mode**: Uses mocked browser automation (no real browser interaction)
### HTML Fixtures (resources/iracing-hosted-sessions/)
The HTML files in `resources/iracing-hosted-sessions/` are **static snapshots for reference and testing**. They help developers understand the iRacing UI structure and serve as fixtures for E2E tests. Production automation always targets the REAL iRacing website at members-ng.iracing.com.
---
## 1. Overview
### Clean Architecture Principles
@@ -151,7 +189,7 @@ GridPilot's monorepo structure enforces layer boundaries through directory organ
└── /companion # Electron desktop app
├── /main # Electron main process
├── /renderer # Electron renderer (React)
└── /automation # Nut.js browser automation scripts
└── /automation # Playwright browser automation scripts
```
### Import Rules (Enforced via ESLint)
@@ -639,7 +677,7 @@ function useCreateLeague() {
**Main Process (Node.js)**
- Handles IPC from renderer process
- Invokes use cases (directly calls application layer, not via HTTP)
- Manages Nut.js automation workflows
- Manages Playwright browser automation workflows
```typescript
// Electron IPC handler
@@ -648,7 +686,7 @@ ipcMain.handle('create-iracing-session', async (event, sessionData) => {
const result = await useCase.execute(sessionData);
if (result.requiresAutomation) {
// Trigger Nut.js automation
// Trigger Playwright browser automation
await automationService.createSessionInBrowser(result.sessionDetails);
}
@@ -660,22 +698,38 @@ ipcMain.handle('create-iracing-session', async (event, sessionData) => {
- UI for session creation, result monitoring
- IPC communication with main process
**Nut.js Automation** ([`AutomationService`](../src/apps/companion/automation/AutomationService.ts))
**Playwright Browser Automation** ([`PlaywrightAutomationAdapter`](../packages/infrastructure/adapters/automation/PlaywrightAutomationAdapter.ts))
GridPilot uses Playwright for all automation tasks. This is the only automation approach—there is no OS-level automation or fallback.
```typescript
export class AutomationService {
import { chromium, Browser, Page } from 'playwright';
export class PlaywrightAutomationAdapter {
private browser: Browser | null = null;
private page: Page | null = null;
async createSessionInBrowser(details: SessionDetails): Promise<void> {
// 1. Launch browser via Nut.js
await mouse.click(/* iRacing icon position */);
// 1. Launch browser via Playwright
this.browser = await chromium.launch({ headless: false });
this.page = await this.browser.newPage();
// 2. Navigate to session creation page
await keyboard.type('https://members.iracing.com/membersite/CreateSession');
// 2. Navigate to iRacing session creation page
await this.page.goto('https://members-ng.iracing.com/web/racing/hosted/create');
// 3. Fill form fields
await this.fillField('session-name', details.name);
await this.fillField('track', details.track);
// 3. Fill form fields using DOM selectors
await this.page.fill('[data-testid="session-name"]', details.name);
await this.page.selectOption('[data-testid="track-select"]', details.track);
// 4. Submit form
await this.clickButton('create-session');
await this.page.click('[data-testid="create-session-button"]');
// 5. Wait for confirmation
await this.page.waitForSelector('[data-testid="session-created-confirmation"]');
}
async cleanup(): Promise<void> {
await this.browser?.close();
}
}
```
@@ -1045,4 +1099,4 @@ test('User creates league via Web Client', async ({ page }) => {
*This architecture documentation will evolve as GridPilot matures. All changes must maintain Clean Architecture principles and the dependency rule.*
*Last Updated: 2025-11-21*
*Last Updated: 2025-11-23*

View File

@@ -94,10 +94,10 @@ Based on feedback from Reddit and Discord communities, league organizers are ove
- Configure team vs solo racing formats
**Authentication & Security**
- iRacing OAuth integration
- Verify driver identities automatically
- Zero-knowledge login: GridPilot never sees or stores your password
- User performs login in visible browser window
- Persistent sessions: login once, stay logged in
- Secure access control for league admin functions
- No separate account creation needed
### Migration Support
@@ -186,6 +186,177 @@ GridPilot acts as an admin assistant, not a bot:
GridPilot automates league management workflows - creating sessions, processing results, managing registrations. We never touch actual racing gameplay, driver behavior, or in-race activities. This is administrative automation to free organizers from manual work.
### iRacing Automation Rules
Understanding the distinction between iRacing's interfaces is critical:
**Two iRacing Interfaces:**
1. **iRacing Website (members.iracing.com)**: This is a standard HTML/DOM web application where hosted session management lives. Being a standard web application, it can be automated with browser automation tools like Playwright. This is 100% legal.
2. **iRacing Desktop App (Electron)**: The racing simulation itself runs in a sandboxed Electron application. Its DOM is inaccessible, and any modification violates iRacing's Terms of Service. This is why tools like iRefined were shut down.
**Our Approach:**
-**Browser automation of the website** - Playwright automates members.iracing.com for session creation, form filling, and data extraction
-**Never modify the desktop client** - No DOM injection, no script injection, no client modification
**Why This Matters:**
- iRacing explicitly allows third-party tools that interact with their website
- Client modification (like iRefined did) violates TOS and risks account bans
- Browser automation is reliable, testable, and fully compliant
## Security & Authentication
### Zero-Knowledge Login Design
GridPilot requires access to your iRacing account to automate session creation, but **we never see, store, or transmit your password**. This section explains how we achieve secure authentication while maintaining complete transparency.
### The Trust Problem
When an app asks for your credentials, you're trusting that app with your identity. Many apps:
- Store passwords in databases (risk of breach)
- Transmit credentials through their servers (man-in-the-middle risk)
- Have access to your login data internally (insider risk)
GridPilot takes a fundamentally different approach: **we never have access to your credentials at all**.
### How GridPilot Authentication Works
```
┌─────────────────────────────────────────────────────────────────┐
│ USER'S COMPUTER │
│ ┌───────────────┐ ┌─────────────────────────────────┐ │
│ │ GridPilot │ │ Playwright Browser Window │ │
│ │ Companion │────────▶│ (Visible to User) │ │
│ │ App │ │ │ │
│ │ │ │ ┌─────────────────────────┐ │ │
│ │ Cannot read │ │ │ iRacing Login Page │ │ │
│ │ form inputs │ │ │ members.iracing.com │ │ │
│ │ │ │ │ │ │ │
│ │ Only detects │ │ │ [Email: *********** ] │ │ │
│ │ URL changes │ │ │ [Password: ******** ] │ │ │
│ │ │ │ │ [ Sign In ] │ │ │
│ └───────────────┘ │ └─────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ HTTPS
┌─────────────────────┐
│ iRacing Servers │
│ (Direct connection │
│ from browser) │
└─────────────────────┘
```
**Step-by-Step Flow:**
1. **User clicks "Connect to iRacing"** in GridPilot
2. **A real browser window opens** showing the iRacing login page
3. **User verifies the URL** - it's the real `members.iracing.com`
4. **User enters credentials directly** - GridPilot cannot read input fields
5. **iRacing authenticates the user** - credentials go directly to iRacing
6. **GridPilot detects success** - only by observing the URL change
7. **Browser session is saved locally** - for future automation runs
### What GridPilot CAN and CANNOT Do
| Capability | GridPilot Access |
|------------|------------------|
| Read your password | ❌ **Never** |
| See what you type | ❌ **Never** |
| Intercept credentials | ❌ **Never** |
| Know if login succeeded | ✅ By URL change only |
| Use authenticated session | ✅ For automation only |
| Clear your session | ✅ User-initiated logout |
### Session Persistence
After your first login, GridPilot saves the browser session locally on your computer:
- **Location**: Your user data folder (not transmitted anywhere)
- **Contents**: Browser cookies and state (encrypted by OS)
- **Benefit**: You don't need to login every time you use GridPilot
- **Control**: You can sign out anytime to clear the saved session
**Session Lifecycle:**
```
First Use Return Visit Session Expired
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Login │ │ Check │ │ Detect │
│ Required│ │ Session │ │ Expiry │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Browser │ │ Valid? │───Yes────▶ │ Ready │
│ Login │ └────┬────┘ │ to Use │
└────┬────┘ │ No └─────────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Save │ │ Re-auth │ │ Pause │
│ Session │ │ Prompt │ │ & Prompt│
└────┬────┘ └─────────┘ └─────────┘
┌─────────┐
│ Ready │
│ to Use │
└─────────┘
```
### User Verification Points
At every step, you can verify GridPilot's behavior:
1. **Browser Window**: You see the real browser - not an embedded frame
2. **URL Bar**: Verify you're on `members.iracing.com` (look for HTTPS lock)
3. **Network Traffic**: Your credentials go directly to iRacing's servers
4. **Input Fields**: GridPilot cannot inject JavaScript to read form values
5. **Logout Control**: You can sign out from GridPilot at any time
### Session Expiry During Automation
If your iRacing session expires while automation is running:
1. **Automation pauses** - no data loss
2. **You're prompted to re-authenticate** - same secure flow
3. **Automation resumes** - from where it left off
GridPilot never stores credentials to "auto-retry" - you always maintain control.
### Security Guarantees
| Guarantee | How We Ensure It |
|-----------|------------------|
| Zero-knowledge | Playwright browser is sandboxed; we cannot inject credential-reading code |
| No transmission | Your login happens directly with iRacing; GridPilot servers never see traffic |
| Local storage only | Session data stays on your computer, encrypted by your OS |
| User control | You can logout, clear sessions, or revoke access anytime |
| Transparency | Browser window is visible; you see exactly what's happening |
### Comparison to Other Approaches
| Approach | Password Exposure | Risk Level |
|----------|-------------------|------------|
| **GridPilot (Zero-Knowledge)** | Never exposed | ✅ Minimal |
| OAuth (if iRacing offered it) | Never exposed | ✅ Minimal |
| Password stored in app | App has access | ⚠️ Moderate |
| Password in config file | File has plaintext | ❌ High |
### Technical Implementation
For developers interested in the implementation details, see [ARCHITECTURE.md](./ARCHITECTURE.md). Key points:
- **Playwright BrowserContext**: Manages browser state including cookies
- **Persistent Context**: Saved to `app.getPath('userData')` in Electron
- **Session Validation**: Navigates to protected page; detects login redirects
- **No Credential Ports**: Application layer has no interfaces for password handling
## Future Vision
### Monetization Approach
@@ -226,4 +397,6 @@ But first: nail the iRacing league management experience.
---
GridPilot exists to make league racing accessible and professional for organizers of all sizes, eliminating manual work so communities can focus on what matters: great racing and strong communities.
GridPilot exists to make league racing accessible and professional for organizers of all sizes, eliminating manual work so communities can focus on what matters: great racing and strong communities.
*Last Updated: 2025-11-23*

View File

@@ -0,0 +1,835 @@
# Mock HTML Fixtures Design Document
## Overview
This document specifies simplified mock HTML fixtures with explicit test attributes for browser automation testing. These fixtures replace the current full-page iRacing dumps with lightweight, testable HTML pages that simulate the iRacing hosted session wizard.
## Purpose
**Test fixtures for E2E testing** - Simplified HTML pages served by FixtureServer that simulate iRacing's wizard for testing the PlaywrightAutomationAdapter in isolation, without needing access to the real iRacing website.
## Problem Statement
Current fixtures in `resources/iracing-hosted-sessions/`:
- Full page dumps (~2.4M tokens per file)
- React/Chakra UI with obfuscated CSS classes (`css-451i2c`, etc.)
- No stable `data-testid` or `data-automation` attributes
- Unsuitable for reliable CSS selector-based automation
## Solution: Simplified Mock Fixtures
### Design Principles
1. **Explicit Test Attributes**: Every interactive element has stable `data-*` attributes
2. **Minimal HTML**: Only essential structure, no framework artifacts
3. **Self-Contained**: Each fixture includes all CSS needed for visual verification
4. **Navigation-Aware**: Buttons link to appropriate next/previous fixtures
5. **Form Fields Match Domain**: Field names align with `HostedSessionConfig` entity
---
## Attribute Schema
### Core Attributes
| Attribute | Purpose | Example Values |
|-----------|---------|----------------|
| `data-step` | Step identification | `2` through `18` |
| `data-action` | Navigation/action buttons | `next`, `back`, `confirm`, `cancel`, `create`, `add`, `select` |
| `data-field` | Form input fields | `sessionName`, `password`, `description`, `region`, etc. |
| `data-modal` | Modal container flag | `true` |
| `data-modal-trigger` | Button that opens a modal | `admin`, `car`, `track` |
| `data-list` | List container | `admins`, `cars`, `tracks` |
| `data-item` | Selectable list item | Car/track/admin ID |
| `data-toggle` | Toggle/checkbox element | `startNow`, `teamDriving`, `rollingStart` |
| `data-dropdown` | Dropdown select | `region`, `weather`, `trackState`, `carClass` |
| `data-slider` | Slider input | `time`, `temperature`, `practice`, `qualify`, `race` |
| `data-indicator` | Step indicator | `race-info`, `server-details`, etc. |
### Navigation Attribute Values
| Value | Description | Usage |
|-------|-------------|-------|
| `next` | Proceed to next step | All non-final steps |
| `back` | Return to previous step | Steps 3-18 |
| `confirm` | Confirm modal action | Modal steps (6, 9, 12) |
| `cancel` | Cancel/close modal | Modal steps |
| `create` | Create new race | Step 2 |
| `add` | Open add modal | Steps 5, 8, 11 |
| `select` | Select item from list | Modal list items |
---
## Step-by-Step Fixture Specifications
### Step 1: Login - Handled Externally
> Note: Login is handled externally. No fixture needed.
### Step 2: Hosted Racing - Main Page
**Purpose**: Landing page with Create a Race button
**Elements**:
```
data-step="2"
data-indicator="hosted-racing"
data-action="create" → Button: Create a Race
```
**Fields**: None
**Navigation**:
- `[data-action="create"]` → Step 3
---
### Step 3: Race Information
**Purpose**: Basic session configuration
**Elements**:
```
data-step="3"
data-indicator="race-information"
data-field="sessionName" → Input: Session name - required
data-field="password" → Input: Session password - optional
data-field="description" → Textarea: Session description
data-action="next" → Button: Next
data-action="back" → Button: Back
```
**Fields**:
| Field | Type | Required | Domain Property |
|-------|------|----------|-----------------|
| `sessionName` | text | Yes | `config.sessionName` |
| `password` | password | No | `config.password` |
| `description` | textarea | No | `config.description` |
---
### Step 4: Server Details
**Purpose**: Server region and timing configuration
**Elements**:
```
data-step="4"
data-indicator="server-details"
data-dropdown="region" → Select: Server region
data-toggle="startNow" → Checkbox: Start immediately
data-action="next"
data-action="back"
```
**Fields**:
| Field | Type | Options |
|-------|------|---------|
| `region` | dropdown | `us-east`, `us-west`, `eu-central`, `eu-west`, `asia`, `oceania` |
| `startNow` | toggle | Boolean |
---
### Step 5: Set Admins
**Purpose**: Admin list management
**Elements**:
```
data-step="5"
data-indicator="set-admins"
data-list="admins" → Container: Admin list
data-modal-trigger="admin" → Button: Add Admin
data-action="next"
data-action="back"
```
---
### Step 6: Add an Admin - Modal
**Purpose**: Search and select admin to add
**Elements**:
```
data-step="6"
data-modal="true"
data-indicator="add-admin"
data-field="adminSearch" → Input: Search admins
data-list="adminResults" → Container: Search results
data-item="{adminId}" → Each result item
data-action="select" → Button: Select admin
data-action="confirm" → Button: Add Selected
data-action="cancel" → Button: Cancel
```
**Fields**:
| Field | Type | Purpose |
|-------|------|---------|
| `adminSearch` | text | Filter admin list |
---
### Step 7: Time Limits
**Purpose**: Practice, qualify, and race duration settings
**Elements**:
```
data-step="7"
data-indicator="time-limits"
data-slider="practice" → Range: Practice length in minutes
data-slider="qualify" → Range: Qualify length in minutes
data-slider="race" → Range: Race length in laps or minutes
data-toggle="unlimitedTime" → Checkbox: Unlimited time
data-action="next"
data-action="back"
```
**Fields**:
| Field | Type | Range | Default |
|-------|------|-------|---------|
| `practice` | slider | 0-120 min | 15 |
| `qualify` | slider | 0-60 min | 10 |
| `race` | slider | 1-500 laps | 20 |
---
### Step 8: Set Cars
**Purpose**: Car list management
**Elements**:
```
data-step="8"
data-indicator="set-cars"
data-list="cars" → Container: Selected cars
data-modal-trigger="car" → Button: Add Car
data-action="next"
data-action="back"
```
---
### Step 9: Add a Car - Modal
**Purpose**: Search and select cars
**Elements**:
```
data-step="9"
data-modal="true"
data-indicator="add-car"
data-field="carSearch" → Input: Search cars
data-list="carResults" → Container: Car grid
data-item="{carId}" → Each car tile
data-action="select" → Select car
data-action="confirm" → Button: Add Selected
data-action="cancel" → Button: Cancel
```
---
### Step 10: Set Car Classes
**Purpose**: Multi-class race configuration
**Elements**:
```
data-step="10"
data-indicator="car-classes"
data-dropdown="carClass" → Select: Car class assignment
data-list="classAssignments" → Container: Class assignments
data-action="next"
data-action="back"
```
---
### Step 11: Set Track
**Purpose**: Track selection
**Elements**:
```
data-step="11"
data-indicator="set-track"
data-field="selectedTrack" → Display: Currently selected track
data-modal-trigger="track" → Button: Select Track
data-action="next"
data-action="back"
```
---
### Step 12: Add a Track - Modal
**Purpose**: Search and select track
**Elements**:
```
data-step="12"
data-modal="true"
data-indicator="add-track"
data-field="trackSearch" → Input: Search tracks
data-list="trackResults" → Container: Track grid
data-item="{trackId}" → Each track tile
data-action="select" → Select track
data-action="confirm" → Button: Select
data-action="cancel" → Button: Cancel
```
---
### Step 13: Track Options
**Purpose**: Track configuration selection
**Elements**:
```
data-step="13"
data-indicator="track-options"
data-dropdown="trackConfig" → Select: Track configuration
data-toggle="dynamicTrack" → Checkbox: Dynamic track
data-action="next"
data-action="back"
```
---
### Step 14: Time of Day
**Purpose**: Race start time configuration
**Elements**:
```
data-step="14"
data-indicator="time-of-day"
data-slider="timeOfDay" → Range: Time of day 0-24
data-field="raceDate" → Date picker: Race date
data-toggle="simulatedTime" → Checkbox: Simulated time progression
data-action="next"
data-action="back"
```
---
### Step 15: Weather
**Purpose**: Weather conditions
**Elements**:
```
data-step="15"
data-indicator="weather"
data-dropdown="weatherType" → Select: Weather type
data-slider="temperature" → Range: Temperature
data-slider="humidity" → Range: Humidity
data-toggle="dynamicWeather" → Checkbox: Dynamic weather
data-action="next"
data-action="back"
```
**Weather Types**: `clear`, `partly-cloudy`, `mostly-cloudy`, `overcast`
---
### Step 16: Race Options
**Purpose**: Race rules and settings
**Elements**:
```
data-step="16"
data-indicator="race-options"
data-field="maxDrivers" → Input: Maximum drivers
data-toggle="rollingStart" → Checkbox: Rolling start
data-toggle="fullCourseCautions" → Checkbox: Full course cautions
data-toggle="fastRepairs" → Checkbox: Fast repairs
data-action="next"
data-action="back"
```
---
### Step 17: Team Driving
**Purpose**: Team race configuration
**Elements**:
```
data-step="17"
data-indicator="team-driving"
data-toggle="teamDriving" → Checkbox: Enable team driving
data-field="minDrivers" → Input: Min drivers per team
data-field="maxDrivers" → Input: Max drivers per team
data-action="next"
data-action="back"
```
---
### Step 18: Track Conditions - Final Step
**Purpose**: Track state configuration
**Elements**:
```
data-step="18"
data-indicator="track-conditions"
data-dropdown="trackState" → Select: Track state
data-toggle="marbles" → Checkbox: Marbles simulation
data-slider="rubberLevel" → Range: Rubber buildup
data-action="back" → Button: Back
```
**Track States**: `auto-generated`, `clean`, `low-rubber`, `medium-rubber`, `high-rubber`
> **Note**: No Submit button on Step 18. Automation intentionally stops here for user review.
---
## Navigation Flow Diagram
```mermaid
flowchart TD
S2[Step 2: Hosted Racing] -->|Create Race| S3[Step 3: Race Information]
S3 -->|Next| S4[Step 4: Server Details]
S4 -->|Next| S5[Step 5: Set Admins]
S5 -->|Add Admin| S6[Step 6: Add Admin Modal]
S6 -->|Confirm/Cancel| S5
S5 -->|Next| S7[Step 7: Time Limits]
S7 -->|Next| S8[Step 8: Set Cars]
S8 -->|Add Car| S9[Step 9: Add Car Modal]
S9 -->|Confirm/Cancel| S8
S8 -->|Next| S10[Step 10: Car Classes]
S10 -->|Next| S11[Step 11: Set Track]
S11 -->|Select Track| S12[Step 12: Add Track Modal]
S12 -->|Confirm/Cancel| S11
S11 -->|Next| S13[Step 13: Track Options]
S13 -->|Next| S14[Step 14: Time of Day]
S14 -->|Next| S15[Step 15: Weather]
S15 -->|Next| S16[Step 16: Race Options]
S16 -->|Next| S17[Step 17: Team Driving]
S17 -->|Next| S18[Step 18: Track Conditions]
S18 -->|STOP| REVIEW[Manual Review Required]
S3 -.->|Back| S2
S4 -.->|Back| S3
S5 -.->|Back| S4
S7 -.->|Back| S5
S8 -.->|Back| S7
S10 -.->|Back| S8
S11 -.->|Back| S10
S13 -.->|Back| S11
S14 -.->|Back| S13
S15 -.->|Back| S14
S16 -.->|Back| S15
S17 -.->|Back| S16
S18 -.->|Back| S17
```
---
## Example Fixture: Step 3 - Race Information
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iRacing - Race Information</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #16213e;
padding: 16px 24px;
border-bottom: 1px solid #0f3460;
}
.step-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #888;
}
.step-indicator .current {
color: #e94560;
font-weight: bold;
}
.main {
flex: 1;
padding: 32px 24px;
max-width: 600px;
margin: 0 auto;
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
color: #aaa;
margin-bottom: 6px;
}
.form-label.required::after {
content: " *";
color: #e94560;
}
.form-input {
width: 100%;
padding: 12px 16px;
background: #16213e;
border: 1px solid #0f3460;
border-radius: 4px;
color: #eee;
font-size: 16px;
}
.form-input:focus {
outline: none;
border-color: #e94560;
}
textarea.form-input {
min-height: 100px;
resize: vertical;
}
.footer {
background: #16213e;
padding: 16px 24px;
border-top: 1px solid #0f3460;
display: flex;
justify-content: space-between;
}
.btn {
padding: 12px 24px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn-primary {
background: #e94560;
color: white;
}
.btn-primary:hover {
background: #ff6b6b;
}
.btn-secondary {
background: transparent;
color: #aaa;
border: 1px solid #0f3460;
}
.btn-secondary:hover {
background: #0f3460;
color: #eee;
}
</style>
</head>
<body data-step="3">
<header class="header">
<div class="step-indicator" data-indicator="race-information">
<span>Step</span>
<span class="current">3</span>
<span>of 18</span>
<span></span>
<span>Race Information</span>
</div>
</header>
<main class="main">
<h1 class="page-title">Race Information</h1>
<form id="race-info-form">
<div class="form-group">
<label class="form-label required" for="sessionName">Session Name</label>
<input
type="text"
id="sessionName"
class="form-input"
data-field="sessionName"
placeholder="Enter session name"
required
/>
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input
type="password"
id="password"
class="form-input"
data-field="password"
placeholder="Optional password"
/>
</div>
<div class="form-group">
<label class="form-label" for="description">Description</label>
<textarea
id="description"
class="form-input"
data-field="description"
placeholder="Optional session description"
></textarea>
</div>
</form>
</main>
<footer class="footer">
<button
type="button"
class="btn btn-secondary"
data-action="back"
onclick="window.location.href='step-02-hosted-racing.html'"
>
Back
</button>
<button
type="button"
class="btn btn-primary"
data-action="next"
onclick="window.location.href='step-04-server-details.html'"
>
Next
</button>
</footer>
</body>
</html>
```
---
## Selector Strategy for PlaywrightAutomationAdapter
### Primary Selector Pattern
Use **data-* attribute selectors** as the primary strategy:
```typescript
// Selector constants
const SELECTORS = {
// Step identification
stepContainer: (step: number) => `[data-step="${step}"]`,
stepIndicator: (name: string) => `[data-indicator="${name}"]`,
// Navigation
nextButton: '[data-action="next"]',
backButton: '[data-action="back"]',
confirmButton: '[data-action="confirm"]',
cancelButton: '[data-action="cancel"]',
createButton: '[data-action="create"]',
addButton: '[data-action="add"]',
selectButton: '[data-action="select"]',
// Form fields
field: (name: string) => `[data-field="${name}"]`,
dropdown: (name: string) => `[data-dropdown="${name}"]`,
toggle: (name: string) => `[data-toggle="${name}"]`,
slider: (name: string) => `[data-slider="${name}"]`,
// Modals
modal: '[data-modal="true"]',
modalTrigger: (type: string) => `[data-modal-trigger="${type}"]`,
// Lists and items
list: (name: string) => `[data-list="${name}"]`,
listItem: (id: string) => `[data-item="${id}"]`,
};
```
### PlaywrightAutomationAdapter Integration
```typescript
import { Page } from 'playwright';
export class PlaywrightAutomationAdapter implements IScreenAutomation {
private page: Page;
async waitForStep(stepNumber: number): Promise<void> {
await this.page.waitForSelector(`[data-step="${stepNumber}"]`, {
state: 'visible',
timeout: 10000,
});
}
async clickAction(action: string): Promise<ClickResult> {
const selector = `[data-action="${action}"]`;
await this.page.click(selector);
return { success: true };
}
async fillField(fieldName: string, value: string): Promise<FormFillResult> {
const selector = `[data-field="${fieldName}"]`;
await this.page.fill(selector, value);
return { success: true, fieldName, value };
}
async selectDropdown(name: string, value: string): Promise<void> {
const selector = `[data-dropdown="${name}"]`;
await this.page.selectOption(selector, value);
}
async setToggle(name: string, checked: boolean): Promise<void> {
const selector = `[data-toggle="${name}"]`;
const isChecked = await this.page.isChecked(selector);
if (isChecked !== checked) {
await this.page.click(selector);
}
}
async setSlider(name: string, value: number): Promise<void> {
const selector = `[data-slider="${name}"]`;
await this.page.fill(selector, String(value));
}
async waitForModal(): Promise<void> {
await this.page.waitForSelector('[data-modal="true"]', {
state: 'visible',
});
}
async selectListItem(itemId: string): Promise<void> {
const selector = `[data-item="${itemId}"]`;
await this.page.click(selector);
}
async executeStep(stepId: StepId, config: SessionConfig): Promise<AutomationResult> {
const step = stepId.value;
await this.waitForStep(step);
switch (step) {
case 2:
await this.clickAction('create');
break;
case 3:
await this.fillField('sessionName', config.sessionName);
if (config.password) {
await this.fillField('password', config.password);
}
if (config.description) {
await this.fillField('description', config.description);
}
await this.clickAction('next');
break;
// Additional steps follow same pattern...
}
return { success: true };
}
}
```
### Selector Priority Order
1. **`data-action`** - For all clickable navigation elements
2. **`data-field`** - For all form inputs
3. **`data-step`** - For step identification/verification
4. **`data-modal`** - For modal detection
5. **`data-item`** - For list item selection
### Benefits of This Strategy
1. **Stability**: Selectors will not break when CSS/styling changes
2. **Clarity**: Self-documenting selectors indicate purpose
3. **Consistency**: Same pattern across all steps
4. **Testability**: Easy to verify correct element targeting
5. **Maintenance**: Simple to update when workflow changes
---
## File Structure
```
resources/
└── mock-fixtures/ # NEW: Simplified test fixtures
├── step-02-hosted-racing.html
├── step-03-race-information.html
├── step-04-server-details.html
├── step-05-set-admins.html
├── step-06-add-admin.html
├── step-07-time-limits.html
├── step-08-set-cars.html
├── step-09-add-car.html
├── step-10-car-classes.html
├── step-11-set-track.html
├── step-12-add-track.html
├── step-13-track-options.html
├── step-14-time-of-day.html
├── step-15-weather.html
├── step-16-race-options.html
├── step-17-team-driving.html
├── step-18-track-conditions.html
└── shared.css # Optional: Shared styles
```
---
## FixtureServer Updates
Update `STEP_TO_FIXTURE` mapping in [`FixtureServer.ts`](../packages/infrastructure/adapters/automation/FixtureServer.ts:16):
```typescript
const STEP_TO_FIXTURE: Record<number, string> = {
2: 'step-02-hosted-racing.html',
3: 'step-03-race-information.html',
4: 'step-04-server-details.html',
5: 'step-05-set-admins.html',
6: 'step-06-add-admin.html',
7: 'step-07-time-limits.html',
8: 'step-08-set-cars.html',
9: 'step-09-add-car.html',
10: 'step-10-car-classes.html',
11: 'step-11-set-track.html',
12: 'step-12-add-track.html',
13: 'step-13-track-options.html',
14: 'step-14-time-of-day.html',
15: 'step-15-weather.html',
16: 'step-16-race-options.html',
17: 'step-17-team-driving.html',
18: 'step-18-track-conditions.html',
};
```
---
## Implementation Tasks for Code Mode
1. Create `resources/mock-fixtures/` directory
2. Create 17 HTML fixture files for steps 2-18
3. Update [`FixtureServer.ts`](../packages/infrastructure/adapters/automation/FixtureServer.ts:42) constructor to use new fixtures path
4. Create `PlaywrightAutomationAdapter` implementing selector strategy
5. Update E2E tests to use PlaywrightAutomationAdapter with FixtureServer
---
## Testing Verification Checklist
For each fixture, verify:
- [ ] `data-step` attribute present on body
- [ ] `data-indicator` present for step identification
- [ ] All navigation buttons have `data-action`
- [ ] All form fields have `data-field`, `data-dropdown`, `data-toggle`, or `data-slider`
- [ ] Modal fixtures have `data-modal="true"`
- [ ] Navigation links point to correct next/previous fixtures
- [ ] Visual rendering is acceptable in browser

View File

@@ -39,12 +39,13 @@ This roadmap provides a phased implementation plan for GridPilot, an automated l
- [ ] Document development setup in README.md
### Automation Validation
- [ ] Install and configure Nut.js for browser automation
- [ ] Test iRacing session creation page detection
- [ ] Install and configure Playwright for browser automation
- [ ] Test iRacing website navigation and authentication flow
- [ ] Test session creation page detection on members.iracing.com
- [ ] Test session ID extraction from URL or page elements
- [ ] Validate server-side result polling from iRacing API
- [ ] Create proof-of-concept automation script
- [ ] Document automation approach and limitations
- [ ] Create proof-of-concept automation script using Playwright
- [ ] Document browser automation approach and iRacing automation rules
- [ ] Identify automation failure modes and mitigation strategies
### Testing Foundation
@@ -187,7 +188,7 @@ This roadmap provides a phased implementation plan for GridPilot, an automated l
### Companion App Foundation
- [ ] Set up Electron application structure
- [ ] Implement Nut.js browser automation framework
- [ ] Implement Playwright browser automation framework
- [ ] Create IPC bridge for backend communication
- [ ] Build auto-updater mechanism
- [ ] Set up application signing and packaging
@@ -460,6 +461,6 @@ This roadmap is a living document and will be updated as the project evolves. Ke
---
**Last Updated:** 2025-11-21
**Current Phase:** Phase 0 (Foundation)
**Overall Progress:** 0% (not started)
**Last Updated:** 2025-11-23
**Current Phase:** Phase 0 (Foundation)
**Overall Progress:** In Progress (browser automation implemented)

View File

@@ -123,10 +123,19 @@ Two candidates meet accessibility and performance requirements:
- **Rationale:** Cross-platform desktop framework (Windows, macOS, Linux). Native OS integration (system tray, notifications, auto-start).
- **Security:** Isolated renderer processes, context bridge for IPC
### Nut.js
- **Purpose:** Keyboard/mouse control for browser automation
- **Rationale:** Simulates human interaction with iRacing web UI when official API unavailable. Not gameplay automation—assistant for data entry tasks.
- **Constraints:** Windows-only initially (iRacing primary platform)
### Playwright (Browser Automation)
- **Purpose:** Automate the iRacing website (members.iracing.com) via DOM manipulation
- **Rationale:** Standard browser automation for the iRacing web interface. The iRacing website is a standard HTML/DOM application that can be automated with Playwright. This is 100% legal and the only approach GridPilot uses.
- **Key Features:**
- Cross-browser support (Chromium, Firefox, WebKit)
- Auto-wait for elements (eliminates flaky tests)
- Network interception for testing
- Screenshot/video capture for debugging
### Important: iRacing Automation Rules
- **✅ Allowed:** Browser automation of members.iracing.com (standard web application)
- **❌ Forbidden:** DOM automation inside the iRacing Electron desktop app (TOS violation)
- **❌ Forbidden:** Script injection or client modification (like iRefined)
### Electron IPC
- **Main ↔ Renderer:** Type-safe message passing via preload scripts
@@ -140,6 +149,7 @@ Two candidates meet accessibility and performance requirements:
- Assistant-style automation (user-initiated), not gameplay bots
- Complements web app (handles tasks iRacing API doesn't expose)
- Desktop integration (notifications for upcoming races, quick access via system tray)
- Browser automation is reliable and testable
## 7. Testing Tools
@@ -243,7 +253,7 @@ Two candidates meet accessibility and performance requirements:
- Runtime: Node.js 20+
- Database: PostgreSQL 15+
- Auth: iRacing OAuth + JWT
- Companion: Electron + Nut.js
- Companion: Electron + Playwright (browser automation only)
- Testing: Vitest + Playwright + Test Containers
- Infra: Docker + Redis + S3/MinIO
- Monorepo: npm workspaces
@@ -271,4 +281,4 @@ Two candidates meet accessibility and performance requirements:
---
*Last Updated: 2025-11-21*
*Last Updated: 2025-11-23*