working companion prototype
This commit is contained in:
@@ -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*
|
||||
181
docs/CONCEPT.md
181
docs/CONCEPT.md
@@ -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*
|
||||
835
docs/MOCK_FIXTURES_DESIGN.md
Normal file
835
docs/MOCK_FIXTURES_DESIGN.md
Normal 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
|
||||
@@ -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)
|
||||
22
docs/TECH.md
22
docs/TECH.md
@@ -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*
|
||||
Reference in New Issue
Block a user