1080 lines
65 KiB
Markdown
1080 lines
65 KiB
Markdown
# GridPilot Architecture
|
||
|
||
This document describes how the current GridPilot repository fits into the broader **competition platform** vision described in the concept docs, with a primary focus on the **GridPilot Web Platform (website + core platform services)** and a supporting focus on the **Automation & Companion subsystem**.
|
||
|
||
- Product vision and competition layer: see [`CONCEPT.md`](docs/concept/CONCEPT.md), [`ADMINS.md`](docs/concept/ADMINS.md), [`DRIVERS.md`](docs/concept/DRIVERS.md), [`COMPETITION.md`](docs/concept/COMPETITION.md), [`RACING.md`](docs/concept/RACING.md), [`STATS.md`](docs/concept/STATS.md), [`RATING.md`](docs/concept/RATING.md), [`SOCIAL.md`](docs/concept/SOCIAL.md), [`TEAMS.md`](docs/concept/TEAMS.md).
|
||
- Technology overview and tooling: see [`TECH.md`](docs/TECH.md).
|
||
- Testing strategy: see [`TESTS.md`](docs/TESTS.md).
|
||
|
||
The architecture is organized into two main parts:
|
||
|
||
1. **GridPilot Web Platform Architecture** (primary; website + core platform services).
|
||
2. **Automation & Companion Architecture** (supporting subsystem that executes hosted sessions for the platform and admins).
|
||
|
||
A short system context comes first, then the web platform, then the automation/companion subsystem, then evolution and integration.
|
||
|
||
---
|
||
|
||
## 1. System Context and Boundaries
|
||
|
||
The concept docs define GridPilot as a full **competition layer** for iRacing leagues:
|
||
|
||
- League identity, seasons, signups, teams, results, penalties, stats, social, discovery.
|
||
|
||
The **current codebase in this repo** implements only a **narrow, but critical slice** of that vision:
|
||
|
||
- A **desktop companion app** that runs on the admin’s machine.
|
||
- A **hosted-session automation engine** that drives iRacing’s web UI (members.iracing.com) via Playwright.
|
||
- Supporting domain models for sessions, steps, page validation, checkout safety and overlays.
|
||
|
||
Everything else from the concept docs (league/season management, result aggregation, team stats, complaints, social features) is **future or external** to this repo and should be treated as an integrating platform that will later consume this automation layer.
|
||
|
||
### 1.1 Current Scope (this repo)
|
||
|
||
- Electron companion app:
|
||
- [`index`](apps/companion/main/index.ts)
|
||
- [`di-container`](apps/companion/main/di-container.ts)
|
||
- [`ipc-handlers`](apps/companion/main/ipc-handlers.ts)
|
||
- React renderer: [`App`](apps/companion/renderer/App.tsx), UI components under [`components`](apps/companion/renderer/components).
|
||
|
||
- Automation & browser integration:
|
||
- Domain models around hosted sessions and steps, e.g. [`AutomationSession`](core/domain/entities/AutomationSession.ts:1), [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1).
|
||
- Application layer ports and use-cases, e.g. [`IAutomationEngine`](core/application/ports/IAutomationEngine.ts:1), [`ISessionRepository`](core/application/ports/ISessionRepository.ts:1), [`StartAutomationSessionUseCase`](core/application/use-cases/StartAutomationSessionUseCase.ts:1).
|
||
- Playwright-based automation adapter and helpers, e.g. [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1), [`CheckoutPriceExtractor`](core/infrastructure/adapters/automation/CheckoutPriceExtractor.ts:1).
|
||
- Logging, config and in-memory repository implementations, e.g. [`InMemorySessionRepository`](core/infrastructure/repositories/InMemorySessionRepository.ts:1), [`AutomationConfig`](core/infrastructure/config/AutomationConfig.ts:1), [`LoggingConfig`](core/infrastructure/config/LoggingConfig.ts:1), [`PinoLogAdapter`](core/infrastructure/adapters/logging/PinoLogAdapter.ts:1).
|
||
|
||
- Testing:
|
||
- Unit tests for domain, application and infrastructure.
|
||
- Integration tests across automation, Electron, and renderer.
|
||
- E2E workflows that drive the hosted-session steps using Playwright and fixtures.
|
||
- See [`TESTS.md`](docs/TESTS.md) and the `tests/*` tree.
|
||
|
||
### 1.2 Future / External Scope (concept platform)
|
||
|
||
The following are **product/domain concerns** from the concept docs that are NOT implemented in this repo and should be treated as external or future services:
|
||
|
||
- League/season/series modeling and identity (`League`, `Season`, `Race`, etc.).
|
||
- Driver and team accounts, cross-league profiles, stats and history.
|
||
- Standing/points systems, drop weeks, team scoring, analytics.
|
||
- Complaint & penalty workflow, protest queues, visibility and audit trails.
|
||
- Social graph, discovery, messaging, notifications.
|
||
|
||
In a future state, these would live in separate services or applications (e.g. “GridPilot Core Platform”) and integrate with this automation layer via API or IPC boundaries. The automation layer is deliberately kept small, testable and ToS-safe to serve as a **reliable hosted-session engine** for those higher-level features.
|
||
|
||
---
|
||
## 2. GridPilot Web Platform Architecture
|
||
|
||
This section describes the **GridPilot Web Platform** (website + core platform services) as the primary system. The automation and companion slice in this repo exist to serve this platform and the admins/drivers described in the concept docs.
|
||
|
||
### 2.1 Web Platform Scope
|
||
|
||
The web platform is responsible for the competition-layer features described in the concept docs, presented through the website and backed by platform services:
|
||
|
||
- **League and season management**
|
||
- League identity, branding and public presence as described in [`CONCEPT.md`](docs/concept/CONCEPT.md) and [`ADMINS.md`](docs/concept/ADMINS.md).
|
||
- Season calendars, formats, rules and information pages.
|
||
- Configuration of point systems, drop weeks, solo vs team modes.
|
||
|
||
- **Driver and team system**
|
||
- Driver registration, league join flows and rosters as described in [`DRIVERS.md`](docs/concept/DRIVERS.md) and [`TEAMS.md`](docs/concept/TEAMS.md).
|
||
- Team creation and management, constructors-style championships and history.
|
||
|
||
- **Results, standings, stats and rating**
|
||
- Import and storage of race results (either via manual upload or result automation) and computation of standings.
|
||
- Season and all-time statistics per [`STATS.md`](docs/concept/STATS.md).
|
||
- Rating computation and presentation per [`RATING.md`](docs/concept/RATING.md).
|
||
|
||
- **Complaints, penalties and classification**
|
||
- Complaint intake, review workflows and penalty assignment as described in [`COMPETITION.md`](docs/concept/COMPETITION.md) and [`ADMINS.md`](docs/concept/ADMINS.md).
|
||
- Penalty-aware classification and standings adjustment.
|
||
|
||
- **Social and discovery**
|
||
- League landing and discovery surfaces per [`SOCIAL.md`](docs/concept/SOCIAL.md) and [`LANDING.md`](docs/concept/LANDING.md).
|
||
- Links to Discord, streaming, content, and lightweight social graph features.
|
||
|
||
Architecturally:
|
||
|
||
- The **website frontend** lives in this monorepo under [`apps/website`](apps/website:1) as the primary web presentation surface (even if only partially implemented today).
|
||
- The **core platform / backend services** form a separate layer of APIs, databases and background workers that implement the competition, stats and rating logic; they may live in this monorepo or in separate services, but are conceptually distinct from the automation slice.
|
||
- The **automation & companion subsystem** in this repo acts as a hosted-session execution backend that the platform can call when it needs to create or manage iRacing hosted sessions.
|
||
|
||
### 2.2 Layering for the Web Platform
|
||
|
||
The web platform itself is designed using a Clean / Hexagonal style, conceptually mirroring the layering used for the automation slice but with a competition-first domain:
|
||
|
||
- **Presentation (Web)**
|
||
- Website SPA/SSR app under [`apps/website`](apps/website:1).
|
||
- Routes for league pages, season views, schedules, standings, driver profiles, team pages, stats dashboards, rating insights and social/discovery pages.
|
||
- UI components for admin configuration flows (league setup, season configuration, team/driver management) and for driver-facing views (signups, results, standings, stats, complaints).
|
||
|
||
- **Application (Platform use-cases)**
|
||
- Use-cases that implement the flows described in the concept docs, for example:
|
||
- CreateLeague, ConfigureSeason, OpenRegistration, CloseRegistration.
|
||
- ImportResults, RecalculateStandings, ApplyPenalty, RebuildClassification.
|
||
- SubmitComplaint, ReviewComplaint, AssignPenalty.
|
||
- ComputeStatsSnapshot, ComputeRatingUpdate.
|
||
- These use-cases orchestrate domain models, repositories and external services but stay independent of HTTP frameworks and database details.
|
||
|
||
- **Domain (Competition models)**
|
||
- Conceptual aggregates such as League, Season, Event, Race, Driver, Team, Complaint, Penalty, Standing, SeasonStats, DriverStats, TeamStats, RatingSnapshot.
|
||
- Invariants around eligibility, registration windows, scoring rules, penalty impacts and rating updates.
|
||
- These models are **conceptual** in this architecture document; they are not yet implemented as concrete TypeScript files in this repo and should be treated as the target design for future platform work.
|
||
|
||
- **Infrastructure (Platform adapters)**
|
||
- Persistence adapters for storing leagues, seasons, events, results, stats, rating snapshots, complaints and penalties in one or more databases.
|
||
- Messaging/queue adapters for background processing (e.g. delayed stats/rating recomputation after penalties).
|
||
- Integration adapters for authentication, payment/billing (if added later), email/notifications and external identity providers.
|
||
- HTTP/GraphQL controllers that expose the application use-cases to the website, companion and other consumers.
|
||
|
||
The same dependency rule applies as in the automation slice:
|
||
|
||
- Presentation and infrastructure depend **inwards** on application and domain.
|
||
- Domain and application stay free of framework and transport details so that the platform can support multiple frontends (web, admin consoles, APIs) over time.
|
||
|
||
### 2.3 Website Frontend Architecture
|
||
|
||
The website under [`apps/website`](apps/website:1) is the primary presentation surface for admins, drivers and teams:
|
||
|
||
- **Application shell and routing**
|
||
- A SPA/SSR architecture (for example, React/Next-style) with route groups for:
|
||
- League landing and overview pages.
|
||
- Season calendars, event detail and schedule pages.
|
||
- Standings and championship tables (driver and team).
|
||
- Driver profiles and team pages.
|
||
- Stats dashboards and rating views as described in [`STATS.md`](docs/concept/STATS.md) and [`RATING.md`](docs/concept/RATING.md).
|
||
- Complaint and penalty views, including driver- and admin-facing lists.
|
||
|
||
- **State and data management**
|
||
- A query-driven data access pattern (e.g. query client plus local UI store) to fetch and cache platform data from backend APIs.
|
||
- UI state for filters, table sorting, visualization settings and admin workflows kept distinct from server data.
|
||
- Optimistic updates used carefully for admin actions where eventual consistency is acceptable.
|
||
|
||
- **Presentation of competition concepts**
|
||
- League homepages present branding, schedule, standings, rosters and links as described in [`ADMINS.md`](docs/concept/ADMINS.md) and [`CONCEPT.md`](docs/concept/CONCEPT.md).
|
||
- Season pages combine calendar, results, standings and stats into a coherent narrative.
|
||
- Driver profiles show season and all-time stats, rating progression, team history and complaint/penalty context consistent with [`DRIVERS.md`](docs/concept/DRIVERS.md) and [`STATS.md`](docs/concept/STATS.md).
|
||
- Team pages present constructors-style standings, driver contribution and historical performance in line with [`TEAMS.md`](docs/concept/TEAMS.md).
|
||
- Rating views explain changes over time and per-race contributions in a way that matches the principles described in [`RATING.md`](docs/concept/RATING.md).
|
||
|
||
- **APIs consumed by the website**
|
||
- The website calls platform APIs (HTTP/REST or GraphQL) to:
|
||
- Read league, season, event, driver and team data.
|
||
- Read standings, stats snapshots and rating values.
|
||
- Submit registrations, complaints and admin actions.
|
||
- Trigger or schedule result imports and (where allowed) automation runs.
|
||
- These APIs are implemented by the platform application layer and are conceptually separate from the automation-specific ports in `core/application`.
|
||
|
||
### 2.4 Platform Integration with Automation
|
||
|
||
The web platform uses the automation & companion subsystem as a hosted-session engine, without coupling to Playwright or Electron details:
|
||
|
||
- **From league/season data to automation intent**
|
||
- The platform aggregates league configuration (series settings, car/track choices, schedule, server settings) and derives a hosted-session configuration structure conceptually equivalent to [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1).
|
||
- This configuration captures the intent of a single hosted session (or a batch of sessions across a season) in a platform-neutral format.
|
||
|
||
- **Hand-off to automation**
|
||
- The platform stores these hosted-session configurations and exposes them via APIs to operator-facing tools:
|
||
- The companion app retrieves configurations and invokes automation use-cases using the shared application ports.
|
||
- A future headless orchestrator service could call the same ports directly, as long as it respects iRacing ToS and keeps an admin in the loop.
|
||
- The automation subsystem executes the configurations against iRacing’s web UI and reports back status and results.
|
||
|
||
- **Ingesting results back into the platform**
|
||
- Once automation completes, race results and any derived classification (including incident counts and penalties applied later) are ingested by platform services.
|
||
- The stats and scoring engine described in [`STATS.md`](docs/concept/STATS.md) processes these inputs to update standings and rich analytics.
|
||
- The rating engine described in [`RATING.md`](docs/concept/RATING.md) uses the same inputs (plus broader season context) to update driver ratings and team evaluations.
|
||
|
||
- **Separation of concerns**
|
||
- Automation is explicitly a **helper** for admins and the platform: it creates predictable, repeatable hosted sessions and reduces workload, but it does not own competition identity, standings, stats or rating.
|
||
- The platform retains ownership of all long-term competition data and presents it through the website under [`apps/website`](apps/website:1).
|
||
|
||
---
|
||
|
||
## 2. System Context and Boundaries
|
||
|
||
This section describes how the automation engine and companion in this repo sit alongside the GridPilot Web App / Website, future core platform services, and iRacing itself.
|
||
|
||
### 2.1 Main components
|
||
|
||
- **GridPilot Web App / Website**
|
||
- Browser-based UI for admins, drivers and teams.
|
||
- Focused on league management, stats, rating, discovery and social features, as described in the concept docs.
|
||
- Lives in a separate codebase as a **presentation & API consumer** that talks to core platform services over HTTP or GraphQL.
|
||
|
||
- **GridPilot Companion App (Electron, this repo)**
|
||
- Desktop operator console running on the admin machine.
|
||
- Provides the UI for configuring and supervising hosted-session automation.
|
||
- Integrates directly with the automation engine in this repo and owns the relationship with Playwright and the local browser.
|
||
|
||
- **Automation Engine & Domain (this repo)**
|
||
- Clean Architecture core for iRacing hosted-session automation:
|
||
- Domain and value objects under [`core/domain`](core/domain/entities/AutomationSession.ts:1).
|
||
- Application use-cases and ports under [`core/application`](core/application/use-cases/StartAutomationSessionUseCase.ts:1).
|
||
- Infrastructure adapters under [`core/infrastructure`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1).
|
||
- Exposes automation capabilities via application ports that can be used both by the companion and by external orchestrators.
|
||
|
||
- **iRacing Web (members.iracing.com)**
|
||
- External system whose hosted-session wizard is automated via Playwright running in a standard browser on the admin machine.
|
||
- Treated strictly as a browser-automation target to remain within iRacing Terms of Service.
|
||
|
||
- **Core Platform / Backend Services (future/external)**
|
||
- Own competition domain concerns such as leagues, schedules, results, stats, rating and social graph as described in [`STATS.md`](docs/concept/STATS.md) and [`RATING.md`](docs/concept/RATING.md).
|
||
- Consume the automation engine as a hosted-session execution backend.
|
||
|
||
### 2.2 Side-by-side presentation layers
|
||
|
||
- The **GridPilot Web App / Website** and the **GridPilot Companion App** are sibling **presentation layers**:
|
||
- Both serve admins and drivers, but with different roles.
|
||
- Both live outside the automation core and depend **inward** on shared domain and application concepts.
|
||
|
||
- Shared concept space:
|
||
- Both surfaces operate on the competition concepts described in [`CONCEPT.md`](docs/concept/CONCEPT.md), [`ADMINS.md`](docs/concept/ADMINS.md), [`DRIVERS.md`](docs/concept/DRIVERS.md), [`COMPETITION.md`](docs/concept/COMPETITION.md), [`RACING.md`](docs/concept/RACING.md), [`SOCIAL.md`](docs/concept/SOCIAL.md), [`TEAMS.md`](docs/concept/TEAMS.md).
|
||
- The automation engine uses [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1) as the canonical representation of a hosted-session setup derived from those higher-level concepts.
|
||
|
||
- Division of responsibilities:
|
||
- The **website** is the primary UI for league management, discovery, stats and rating (core platform concerns).
|
||
- The **companion** is the trusted operator console for ToS-safe automation on the admin’s machine.
|
||
|
||
### 2.3 Communication model
|
||
|
||
At a high level, communication flows are designed around the automation engine as a reusable core:
|
||
|
||
- Typical platform-driven flow:
|
||
- Website UI → Core Platform APIs → Automation Orchestrator → Automation Engine (this repo) → Companion App (operator and overlay) → iRacing Web.
|
||
|
||
- Typical companion-driven flow:
|
||
- Companion App → Application Use-cases → Automation Engine → Playwright Adapter → iRacing Web.
|
||
|
||
- Hand-off of automation intent:
|
||
- Core platform services or the web app can derive a [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1) from league data and store it server-side.
|
||
- The companion app or a headless orchestrator retrieves these configs via platform APIs and invokes the automation engine using the same application ports used in this repo.
|
||
|
||
### 2.4 System context diagram
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Website[GridPilot Web App]
|
||
Platform[Core Platform Services]
|
||
Companion[GridPilot Companion App]
|
||
Engine[Automation Engine and Domain this repo]
|
||
IRacing[iRacing Web members.iracing com]
|
||
|
||
Website -- HTTPS APIs --> Platform
|
||
Platform -- Automation requests --> Engine
|
||
Companion -- Local IPC and function calls --> Engine
|
||
Engine -- Browser automation via Playwright --> IRacing
|
||
Platform -- Results events and stats inputs --> Platform
|
||
```
|
||
|
||
---
|
||
|
||
## 3. iRacing Automation Constraints
|
||
|
||
The automation strategy is shaped by iRacing’s platform boundaries and Terms of Service.
|
||
|
||
### 2.1 Two iRacing surfaces
|
||
|
||
1. **Web site (members.iracing.com)**
|
||
- Standard HTML/DOM web app at `https://members-ng.iracing.com/`.
|
||
- Hosted session creation wizard lives here.
|
||
- This repo automates this surface using Playwright, treating it like any other web UI.
|
||
- All automation happens **inside a normal browser**, launched on the admin’s machine.
|
||
|
||
2. **Desktop app (iRacing launcher / sim)**
|
||
- Electron-based desktop launcher and the racing sim itself.
|
||
- DOM and process are **off-limits**; modifying or injecting into this client is ToS-violating.
|
||
- This repo does **not** automate or modify the desktop app or the sim.
|
||
|
||
### 2.2 Allowed vs forbidden approaches
|
||
|
||
**Allowed (and used here)**
|
||
|
||
- Browser automation of the iRacing website via Playwright.
|
||
- Navigating, filling forms, clicking DOM elements in members.iracing.com.
|
||
- Using local HTML/JSON fixtures to test selectors and flows.
|
||
- Running everything on the admin’s machine, under their control.
|
||
|
||
**Forbidden (and explicitly avoided)**
|
||
|
||
- Injecting scripts into the official iRacing desktop client.
|
||
- Reading or manipulating the Electron app’s internal DOM.
|
||
- Hooking the sim process or game memory.
|
||
- Running any automation that is not clearly aligned with “browser automation of a public web UI”.
|
||
|
||
The [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1) contains additional safeguards, such as:
|
||
|
||
- Guardrails around checkout/payment-like buttons using iRacing-specific selector maps.
|
||
- Explicit “blocked selector” detection before clicks.
|
||
- Separate mock/fixture mode for tests vs “real” mode for live sessions.
|
||
|
||
This aligns with the safety and trust constraints in [`CONCEPT.md`](docs/concept/CONCEPT.md) and [`RACING.md`](docs/concept/RACING.md): automation is a **helper**, not a cheat, and always runs transparently on the admin’s machine.
|
||
|
||
---
|
||
|
||
## 4. Clean Architecture Overview (Automation Slice)
|
||
|
||
The automation & companion code follows a layered, Clean Architecture style where **core domain and application logic are shared across multiple presentation and infrastructure options**.
|
||
|
||
### 4.1 Conceptual layering
|
||
|
||
```text
|
||
Outer layers:
|
||
Presentation (Web App, Companion, APIs)
|
||
Infrastructure (Playwright, logging, persistence, IPC)
|
||
|
||
Inner layers:
|
||
Application (Use-cases, Ports)
|
||
Domain (Entities, Value Objects, Validators)
|
||
```
|
||
|
||
**Dependency rule**
|
||
|
||
- Code may only depend **inwards**:
|
||
- Presentation → Application → Domain.
|
||
- Infrastructure → Application → Domain.
|
||
- Inner layers have **no knowledge** of specific UIs, frameworks or transport.
|
||
|
||
**Layer responsibilities**
|
||
|
||
- **Domain**
|
||
- Competition-neutral automation concepts such as [`AutomationSession`](core/domain/entities/AutomationSession.ts:1), [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1), session and step invariants and validators.
|
||
- Independent of Electron, Playwright, HTTP or persistence.
|
||
|
||
- **Application**
|
||
- Use-cases that express admin intent (start automation, verify auth, confirm checkout) and orchestrate domain entities via ports.
|
||
- Defines the automation API that both the companion and any future platform orchestrators use.
|
||
|
||
- **Infrastructure**
|
||
- Adapters that implement ports using concrete technologies such as Playwright, Pino, IPC and in-memory repositories.
|
||
- Contains browser automation, DOM helpers, logging and configuration.
|
||
|
||
- **Presentation**
|
||
- UIs and process hosts that call application use-cases:
|
||
- Today: the Electron-based companion app under [`apps/companion`](apps/companion/main/index.ts:1).
|
||
- Future: a web app or backend services that invoke the same use-cases via HTTP, IPC or other transports.
|
||
|
||
**Key rules in this repo**
|
||
|
||
- `core/domain/*` has no dependencies on application, infrastructure, or Electron.
|
||
- `core/application/*` depends only on `core/domain/*` (plus shared types).
|
||
- `core/infrastructure/*` depends on domain and application ports, and on external libraries (Playwright, Pino, etc.).
|
||
- `apps/companion/*` depends on inner layers and glues application use-cases to Electron UI and IPC.
|
||
|
||
This keeps the **automation engine** stable and reusable across presentation surfaces (companion today, web or backend orchestrators later), while allowing infrastructure and UI details to evolve without rewriting business logic.
|
||
|
||
### 4.2 Critical Clean Architecture Principle: Use Cases Do NOT Call Presenters
|
||
|
||
**The most important rule in Clean Architecture is that use cases must remain completely independent of presentation concerns.**
|
||
|
||
#### ❌ WRONG PATTERN (What NOT to do)
|
||
|
||
```typescript
|
||
// ❌ VIOLATES CLEAN ARCHITECTURE
|
||
class GetRaceDetailUseCase {
|
||
constructor(
|
||
private repositories: any,
|
||
private output: UseCaseOutputPort<GetRaceDetailResult>
|
||
) {}
|
||
|
||
async execute(input: GetRaceDetailInput): Promise<Result<void, ApplicationError>> {
|
||
const race = await this.raceRepository.findById(input.raceId);
|
||
|
||
if (!race) {
|
||
// WRONG: Use case calling presenter
|
||
const result = Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
|
||
this.output.present(result); // ❌ DON'T DO THIS
|
||
return result;
|
||
}
|
||
|
||
// WRONG: Use case calling presenter
|
||
const result = Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
|
||
this.output.present(result); // ❌ DON'T DO THIS
|
||
return result;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Why this violates Clean Architecture:**
|
||
- Use cases now **know about presenters** and how to call them
|
||
- Creates **tight coupling** between application logic and presentation
|
||
- Makes use cases **untestable** without mocking presenters
|
||
- Violates the **Dependency Rule** (inner layer depending on outer layer behavior)
|
||
|
||
#### ✅ CORRECT PATTERN (Clean Architecture)
|
||
|
||
```typescript
|
||
// ✅ CLEAN ARCHITECTURE - Use case returns data, period
|
||
class GetRaceDetailUseCase {
|
||
constructor(
|
||
private repositories: any,
|
||
private output: UseCaseOutputPort<GetRaceDetailResult>
|
||
) {}
|
||
|
||
async execute(input: GetRaceDetailInput): Promise<Result<GetRaceDetailResult, ApplicationError>> {
|
||
const race = await this.raceRepository.findById(input.raceId);
|
||
|
||
if (!race) {
|
||
return Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
|
||
// NO .present() call! Just returns the Result.
|
||
}
|
||
|
||
return Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
|
||
// NO .present() call! Just returns the Result.
|
||
}
|
||
}
|
||
```
|
||
|
||
**The Controller/Wiring Layer (Infrastructure/Presentation):**
|
||
|
||
```typescript
|
||
// ✅ Controller wires use case to presenter
|
||
class RaceController {
|
||
constructor(
|
||
private getRaceDetailUseCase: GetRaceDetailUseCase,
|
||
private raceDetailPresenter: RaceDetailPresenter
|
||
) {}
|
||
|
||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailDTO> {
|
||
// 1. Execute use case
|
||
const result = await this.getRaceDetailUseCase.execute(params);
|
||
|
||
// 2. Pass result to presenter (wiring happens here)
|
||
this.raceDetailPresenter.present(result);
|
||
|
||
// 3. Get ViewModel from presenter
|
||
return this.raceDetailPresenter.viewModel;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 The "Presenter Not Presented" Error Explained
|
||
|
||
Your current architecture has this error because:
|
||
|
||
1. **Use cases call `.present()`** (violating Clean Architecture)
|
||
2. **Controllers expect presenters to have `.viewModel`**
|
||
3. **But if use case returns early on error without calling `.present()`**, the presenter never gets data
|
||
4. **Controller tries to access `.viewModel`** → throws "Presenter not presented"
|
||
|
||
**The fix is NOT to add more `.present()` calls to use cases. The fix is to remove ALL `.present()` calls from use cases.**
|
||
|
||
### 4.4 Your Adapter Pattern is a Smokescreen
|
||
|
||
Your current code uses adapter classes like:
|
||
|
||
```typescript
|
||
class RaceDetailOutputAdapter implements UseCaseOutputPort<GetRaceDetailResult> {
|
||
constructor(private presenter: RaceDetailPresenter) {}
|
||
|
||
present(result: GetRaceDetailResult): void {
|
||
this.presenter.present(result);
|
||
}
|
||
}
|
||
```
|
||
|
||
**This is just hiding the crime.** The adapter still couples the use case to the presenter concept. The real Clean Architecture approach eliminates these adapters entirely and has controllers do the wiring.
|
||
|
||
### 4.5 The Real Clean Architecture Flow
|
||
|
||
```
|
||
1. Controller receives HTTP request
|
||
2. Controller calls UseCase.execute()
|
||
3. UseCase returns Result<T, E> (no presenter knowledge)
|
||
4. Controller passes Result to Presenter
|
||
5. Presenter transforms Result → ViewModel
|
||
6. Controller returns ViewModel to HTTP layer
|
||
```
|
||
|
||
**Key insight:** The use case's `output` port should be **the Result itself**, not a presenter. The controller is responsible for taking that Result and passing it to the appropriate presenter.
|
||
|
||
### 4.6 What This Means for Your Codebase
|
||
|
||
**To achieve 100% Clean Architecture, you must:**
|
||
|
||
1. **Remove all `.present()` calls from use cases** - they should only return Results
|
||
2. **Remove all adapter classes** - they're unnecessary coupling
|
||
3. **Make controllers wire use cases to presenters** - this is where the "glue" belongs
|
||
4. **Use cases return Results, period** - they don't know about presenters, viewmodels, or HTTP
|
||
|
||
**This is the ONLY way to achieve true Clean Architecture.** Any pattern where use cases call presenters is **not Clean Architecture**, regardless of how many adapter layers you add.
|
||
|
||
The "presenter not presented" error is a **symptom** of this architectural violation, not the root problem.
|
||
|
||
---
|
||
|
||
## 4. Layer-by-Layer Mapping
|
||
|
||
### 4.1 Domain Layer (`core/domain/*`)
|
||
|
||
The domain models what an “automation session” is, how it progresses, and which invariants must hold regardless of UI or tooling.
|
||
|
||
**Core entities**
|
||
|
||
- [`AutomationSession`](core/domain/entities/AutomationSession.ts:1)
|
||
- Represents a single hosted-session automation run.
|
||
- Tracks:
|
||
- `id`
|
||
- `currentStep` (wizard step, via [`StepId`](core/domain/value-objects/StepId.ts:1))
|
||
- `state` (via [`SessionState`](core/domain/value-objects/SessionState.ts:1))
|
||
- immutable [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1)
|
||
- timestamps (`startedAt`, `completedAt`)
|
||
- failure information (`errorMessage`).
|
||
- Enforces invariants:
|
||
- Cannot start unless config is valid and state is pending.
|
||
- Steps can only move forward and must progress sequentially (`n → n+1`, no skipping or going backwards).
|
||
- Terminal states (e.g. `STOPPED_AT_STEP_18`, `FAILED`) cannot be mutated further.
|
||
|
||
- [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1)
|
||
- Describes the admin’s intended hosted session:
|
||
- Required: `sessionName`, `trackId`, `carIds`.
|
||
- Optional QoL fields: `serverName`, `password`, `adminPassword`, `maxDrivers`, weather/time-of-day, lengths, damage model, track state, etc.
|
||
- Acts as the **canonical session description** that higher layers use to map into iRacing’s wizard fields.
|
||
|
||
- [`StepExecution`](core/domain/entities/StepExecution.ts:1)
|
||
- Encapsulates the result of executing a single wizard step, including timing and error data.
|
||
|
||
**Value objects**
|
||
|
||
- Session lifecycle and step identity:
|
||
- [`SessionState`](core/domain/value-objects/SessionState.ts:1)
|
||
- [`SessionLifetime`](core/domain/value-objects/SessionLifetime.ts:1)
|
||
- [`StepId`](core/domain/value-objects/StepId.ts:1)
|
||
|
||
- Authentication and browser state:
|
||
- [`AuthenticationState`](core/domain/value-objects/AuthenticationState.ts:1)
|
||
- [`BrowserAuthenticationState`](core/domain/value-objects/BrowserAuthenticationState.ts:1)
|
||
- [`CookieConfiguration`](core/domain/value-objects/CookieConfiguration.ts:1)
|
||
|
||
- Checkout and pricing safety:
|
||
- [`CheckoutPrice`](core/domain/value-objects/CheckoutPrice.ts:1)
|
||
- [`CheckoutState`](core/domain/value-objects/CheckoutState.ts:1)
|
||
- [`CheckoutConfirmation`](core/domain/value-objects/CheckoutConfirmation.ts:1)
|
||
|
||
- Overlay/visual integration:
|
||
- [`ScreenRegion`](core/domain/value-objects/ScreenRegion.ts:1)
|
||
- [`RaceCreationResult`](core/domain/value-objects/RaceCreationResult.ts:1)
|
||
|
||
**Domain services**
|
||
|
||
- [`PageStateValidator`](core/domain/services/PageStateValidator.ts:1)
|
||
- Declaratively describes what it means for the automation to be “on the right wizard page”.
|
||
- Evaluates combinations of required/forbidden selectors into a structured result.
|
||
- Used by [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1) to avoid driving the wrong UI state.
|
||
|
||
- [`StepTransitionValidator`](core/domain/services/StepTransitionValidator.ts:1)
|
||
- Enforces legal transitions between wizard steps and session states.
|
||
- Centralizes the rules for when it is safe to move to the next step or finalize.
|
||
|
||
**How this ties back to the concept docs**
|
||
|
||
- Admin workload reduction: the domain ensures that once configured, the automation moves deterministically through the iRacing wizard (no skipped or repeated steps).
|
||
- Safety and trust: session states and validators make failures explicit and traceable, aligning with the transparency goals in [`ADMINS.md`](docs/concept/ADMINS.md) and [`RACING.md`](docs/concept/RACING.md).
|
||
|
||
---
|
||
|
||
### 4.2 Application Layer (`core/application/*`)
|
||
|
||
The application layer orchestrates use-cases by coordinating domain entities and calling ports that are implemented in infrastructure.
|
||
|
||
**Key ports**
|
||
|
||
- [`IAutomationEngine`](core/application/ports/IAutomationEngine.ts:1)
|
||
- `validateConfiguration(config)` → checks whether the intended hosted session is structurally valid and automatable.
|
||
- `executeStep(stepId, config)` → drives a single wizard step using the underlying browser automation.
|
||
- `stopAutomation()` → halts the current run.
|
||
|
||
- [`ISessionRepository`](core/application/ports/ISessionRepository.ts:1)
|
||
- Persists [`AutomationSession`](core/domain/entities/AutomationSession.ts:1) instances for tracking and later inspection.
|
||
|
||
- Screen & browser integration:
|
||
- [`IScreenAutomation`](core/application/ports/IScreenAutomation.ts:1) (implemented by [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1)).
|
||
- [`IAuthenticationService`](core/application/ports/IAuthenticationService.ts:1) for login/session management.
|
||
- [`ICheckoutService`](core/application/ports/ICheckoutService.ts:1) and [`ICheckoutConfirmationPort`](core/application/ports/ICheckoutConfirmationPort.ts:1) for safe credits/checkout flows.
|
||
|
||
- Cross-cutting:
|
||
- [`Logger`](core/application/ports/Logger.ts:1)
|
||
- [`IOverlaySyncPort`](core/application/ports/IOverlaySyncPort.ts:1)
|
||
- [`IAutomationEventPublisher`](core/application/ports/IAutomationEventPublisher.ts:1)
|
||
- [`IUserConfirmationPort`](core/application/ports/IUserConfirmationPort.ts:1)
|
||
|
||
**Key services**
|
||
|
||
- [`OverlaySyncService`](core/application/services/OverlaySyncService.ts:1)
|
||
- Bridges the automation lifecycle (from infrastructure) to overlay/presentation consumers (companion renderer).
|
||
- Consumes an [`IAutomationLifecycleEmitter`](core/infrastructure/adapters/IAutomationLifecycleEmitter.ts:1) (implemented by the Playwright adapter) and publishes state to an overlay sync port.
|
||
|
||
**Key use-cases**
|
||
|
||
- [`StartAutomationSessionUseCase`](core/application/use-cases/StartAutomationSessionUseCase.ts:1)
|
||
- Creates a new [`AutomationSession`](core/domain/entities/AutomationSession.ts:1) from a [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1).
|
||
- Calls [`IAutomationEngine`](core/application/ports/IAutomationEngine.ts:1) to perform validation before any browser work starts.
|
||
- Persists the session via [`ISessionRepository`](core/application/ports/ISessionRepository.ts:1).
|
||
- Returns a DTO that the companion app tracks as progress state.
|
||
|
||
- Authentication:
|
||
- [`CheckAuthenticationUseCase`](core/application/use-cases/CheckAuthenticationUseCase.ts:1)
|
||
- [`InitiateLoginUseCase`](core/application/use-cases/InitiateLoginUseCase.ts:1)
|
||
- [`ClearSessionUseCase`](core/application/use-cases/ClearSessionUseCase.ts:1)
|
||
- These map the ToS-compliant login flow described in [`CONCEPT.md`](docs/concept/CONCEPT.md) and [`ADMINS.md`](docs/concept/ADMINS.md) into explicit use-cases (no password handling by GridPilot).
|
||
|
||
- Automation completion & checkout:
|
||
- [`CompleteRaceCreationUseCase`](core/application/use-cases/CompleteRaceCreationUseCase.ts:1)
|
||
- [`ConfirmCheckoutUseCase`](core/application/use-cases/ConfirmCheckoutUseCase.ts:1)
|
||
- Uses [`ICheckoutService`](core/application/ports/ICheckoutService.ts:1) from the Playwright layer to inspect price/state and defers to a UI-side confirmation port to comply with “never surprise-charge the admin”.
|
||
|
||
- Guarding against stale auth:
|
||
- [`VerifyAuthenticatedPageUseCase`](core/application/use-cases/VerifyAuthenticatedPageUseCase.ts:1) ensures the current page is still authenticated before continuing automation.
|
||
|
||
The application layer is where “admin intent” (start automation, confirm checkout, verify login) is encoded and exposed to the companion, without exposing Playwright or DOM specifics.
|
||
|
||
---
|
||
|
||
### 4.3 Infrastructure Layer (`core/infrastructure/*`)
|
||
|
||
The infrastructure layer implements the ports using concrete tools and services.
|
||
|
||
**Automation adapters**
|
||
|
||
- [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1)
|
||
- Implements:
|
||
- [`IScreenAutomation`](core/application/ports/IScreenAutomation.ts:1)
|
||
- [`IAuthenticationService`](core/application/ports/IAuthenticationService.ts:1)
|
||
- [`IAutomationLifecycleEmitter`](core/infrastructure/adapters/IAutomationLifecycleEmitter.ts:1) used by [`OverlaySyncService`](core/application/services/OverlaySyncService.ts:1).
|
||
- Responsibilities:
|
||
- Managing Playwright browser, context, and page lifecycle (headed/headless, fixture vs real mode).
|
||
- Navigating to the iRacing hosted-session wizard, login pages and fixtures.
|
||
- Driving wizard steps through an internal [`WizardStepOrchestrator`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1).
|
||
- Injecting and updating the on-page automation overlay.
|
||
- Enforcing click safety (blocked selectors, checkout detection).
|
||
- Handling authentication flows, cookies and persistent sessions via [`SessionCookieStore`](core/infrastructure/adapters/automation/auth/SessionCookieStore.ts:1) and [`PlaywrightAuthSessionService`](core/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService.ts:1).
|
||
- Validating page state using [`PageStateValidator`](core/domain/services/PageStateValidator.ts:1).
|
||
|
||
- [`MockBrowserAutomationAdapter`](core/infrastructure/adapters/automation/engine/MockBrowserAutomationAdapter.ts:1)
|
||
- Lightweight adapter used in tests to simulate automation without real browser interactions.
|
||
|
||
- [`FixtureServer`](core/infrastructure/adapters/automation/engine/FixtureServer.ts:1)
|
||
- Serves static HTML fixtures from `html-dumps-optimized/` to exercise the automation logic deterministically in tests and local development.
|
||
|
||
**DOM helpers**
|
||
|
||
- [`IRacingDomNavigator`](core/infrastructure/adapters/automation/dom/IRacingDomNavigator.ts:1)
|
||
- High-level navigation and waiting logic for wizard steps and key UI elements.
|
||
|
||
- [`IRacingDomInteractor`](core/infrastructure/adapters/automation/dom/IRacingDomInteractor.ts:1)
|
||
- Encapsulates filling fields, clicking elements, handling modals.
|
||
|
||
- [`SafeClickService`](core/infrastructure/adapters/automation/dom/SafeClickService.ts:1)
|
||
- A safer click abstraction that combines timeouts, modal-dismiss logic and safety checks.
|
||
|
||
- Selectors and constants:
|
||
- [`IRACING_SELECTORS`](core/infrastructure/adapters/automation/dom/IRacingSelectors.ts:1)
|
||
- Additional helper types under `core/infrastructure/adapters/automation/dom/*`.
|
||
|
||
**Checkout & pricing**
|
||
|
||
- [`CheckoutPriceExtractor`](core/infrastructure/adapters/automation/CheckoutPriceExtractor.ts:1)
|
||
- Reads DOM labels and buttons in the iRacing wizard to produce:
|
||
- [`CheckoutPrice`](core/domain/value-objects/CheckoutPrice.ts:1) (if parsable).
|
||
- [`CheckoutState`](core/domain/value-objects/CheckoutState.ts:1) (derived from button classes).
|
||
- Raw `buttonHtml` for debugging.
|
||
- Returns results wrapped in [`Result`](core/shared/result/Result.ts:1) to avoid throwing during automation.
|
||
|
||
**Repositories & configuration**
|
||
|
||
- [`InMemorySessionRepository`](core/infrastructure/repositories/InMemorySessionRepository.ts:1)
|
||
- Simple in-process implementation of [`ISessionRepository`](core/application/ports/ISessionRepository.ts:1), used in the companion app and tests.
|
||
|
||
- Config:
|
||
- [`AutomationConfig`](core/infrastructure/config/AutomationConfig.ts:1)
|
||
- Mode selection: `production`, `development`, `test`.
|
||
- Default timeouts and base URLs for fixtures vs live.
|
||
- [`BrowserModeConfig`](core/infrastructure/config/BrowserModeConfig.ts:1)
|
||
- Controls headed/headless behavior, with [`BrowserModeConfigLoader`](core/infrastructure/config/BrowserModeConfig.ts:1) used heavily in [`di-container`](apps/companion/main/di-container.ts:1).
|
||
- [`LoggingConfig`](core/infrastructure/config/LoggingConfig.ts:1)
|
||
|
||
- Logging:
|
||
- [`PinoLogAdapter`](core/infrastructure/adapters/logging/PinoLogAdapter.ts:1)
|
||
- [`NoOpLogAdapter`](core/infrastructure/adapters/logging/NoOpLogAdapter.ts:1) (used in tests to keep noise low).
|
||
|
||
**IPC / UI integration**
|
||
|
||
- [`ElectronCheckoutConfirmationAdapter`](core/infrastructure/adapters/ipc/ElectronCheckoutConfirmationAdapter.ts:1)
|
||
- Bridges application-level checkout confirmation flows to the companion renderer via IPC.
|
||
|
||
This layer is where the “messy reality” of Playwright, Electron, file paths and selectors is implemented, while honoring the constraints defined at the domain/application layers.
|
||
|
||
---
|
||
|
||
### 4.4 Presentation Layer (`apps/companion/*`)
|
||
|
||
The presentation layer in this repo is currently a **single Electron app** that provides UI and IPC for admins, but the architecture explicitly anticipates additional presentation surfaces (web app, backend APIs) that reuse the same application and domain layers.
|
||
|
||
**Electron main**
|
||
|
||
- [`index`](apps/companion/main/index.ts)
|
||
- Bootstraps the Electron app.
|
||
- Creates windows for the renderer.
|
||
- Wires process-level lifecycle events and shutdown behavior.
|
||
|
||
- [`di-container`](apps/companion/main/di-container.ts)
|
||
- Central wiring for:
|
||
- Logger ([`Logger`](core/application/ports/Logger.ts:1)) via `createLogger()` and [`LoggingConfig`](core/infrastructure/config/LoggingConfig.ts:1).
|
||
- Session repository ([`ISessionRepository`](core/application/ports/ISessionRepository.ts:1)) via [`InMemorySessionRepository`](core/infrastructure/repositories/InMemorySessionRepository.ts:1).
|
||
- Browser automation adapter via `createBrowserAutomationAdapter()` using [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1) or [`MockBrowserAutomationAdapter`](core/infrastructure/adapters/automation/engine/MockBrowserAutomationAdapter.ts:1) depending on mode.
|
||
- [`IAutomationEngine`](core/application/ports/IAutomationEngine.ts:1) via [`AutomationEngineAdapter`](core/infrastructure/adapters/automation/engine/AutomationEngineAdapter.ts:1) / [`MockAutomationEngineAdapter`](core/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter.ts:1).
|
||
- Application use-cases:
|
||
- [`StartAutomationSessionUseCase`](core/application/use-cases/StartAutomationSessionUseCase.ts:1)
|
||
- [`CheckAuthenticationUseCase`](core/application/use-cases/CheckAuthenticationUseCase.ts:1)
|
||
- [`InitiateLoginUseCase`](core/application/use-cases/InitiateLoginUseCase.ts:1)
|
||
- [`ClearSessionUseCase`](core/application/use-cases/ClearSessionUseCase.ts:1)
|
||
- [`ConfirmCheckoutUseCase`](core/application/use-cases/ConfirmCheckoutUseCase.ts:1)
|
||
- Overlay sync orchestration via [`OverlaySyncService`](core/application/services/OverlaySyncService.ts:1) and [`IAutomationLifecycleEmitter`](core/infrastructure/adapters/IAutomationLifecycleEmitter.ts:1).
|
||
- Exposes methods like `getStartAutomationUseCase()`, `initializeBrowserConnection()`, `getBrowserModeConfigLoader()`, and `refreshBrowserAutomation()` that are used by IPC handlers and tests.
|
||
|
||
- [`ipc-handlers`](apps/companion/main/ipc-handlers.ts)
|
||
- Translates renderer IPC calls into use-case invocations:
|
||
- `startAutomation` → [`StartAutomationSessionUseCase`](core/application/use-cases/StartAutomationSessionUseCase.ts:1).
|
||
- `checkAuth` / `initiateLogin` / `clearSession` → authentication use-cases.
|
||
- Browser connection initialization and overlay sync wiring.
|
||
|
||
- [`preload`](apps/companion/main/preload.ts)
|
||
- Exposes a typed `window.electronAPI` bridge to the renderer with functions like `startAutomation`, `checkAuth`, `initiateLogin`, `onSessionProgress`, `onCheckoutConfirmationRequest`.
|
||
|
||
**React renderer**
|
||
|
||
- Root:
|
||
- [`App`](apps/companion/renderer/App.tsx)
|
||
- Controls high-level UI state:
|
||
- Authentication state (`AuthState`) and login flow.
|
||
- Current [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1) passed from the session creation form.
|
||
- Session progress tracking (step, state, errors).
|
||
- Checkout confirmation dialog and race creation success views.
|
||
- Talks to Electron via `window.electronAPI`:
|
||
- `checkAuth()` and `initiateLogin()` for auth.
|
||
- `startAutomation()` and `stopAutomation()` for sessions.
|
||
- `onSessionProgress()` to update the right-hand progress monitor.
|
||
- `onCheckoutConfirmationRequest()` to show [`CheckoutConfirmationDialog`](apps/companion/renderer/components/CheckoutConfirmationDialog.tsx).
|
||
|
||
- Components (selection):
|
||
- [`SessionCreationForm`](apps/companion/renderer/components/SessionCreationForm.tsx)
|
||
- Collects the data that becomes [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1).
|
||
- [`SessionProgressMonitor`](apps/companion/renderer/components/SessionProgressMonitor.tsx)
|
||
- Visualizes the current step, completed steps, and error state of the [`AutomationSession`](core/domain/entities/AutomationSession.ts:1).
|
||
- [`LoginPrompt`](apps/companion/renderer/components/LoginPrompt.tsx)
|
||
- Renders different messages for `AUTHENTICATED`, `EXPIRED`, `LOGGED_OUT`, `UNKNOWN` states and wires `onLogin`/`onRetry`.
|
||
- [`BrowserModeToggle`](apps/companion/renderer/components/BrowserModeToggle.tsx)
|
||
- Allows switching between headed/headless behavior in development, talking to the [`BrowserModeConfigLoader`](core/infrastructure/config/BrowserModeConfig.ts:1) through IPC.
|
||
- [`CheckoutConfirmationDialog`](apps/companion/renderer/components/CheckoutConfirmationDialog.tsx)
|
||
- Presents a clear, time-bounded dialog where the admin must confirm or cancel a credit purchase-like action.
|
||
- [`RaceCreationSuccessScreen`](apps/companion/renderer/components/RaceCreationSuccessScreen.tsx)
|
||
- Displays the final result of a successful race creation as modeled by [`RaceCreationResult`](core/domain/value-objects/RaceCreationResult.ts:1).
|
||
|
||
The companion app is intentionally a **single presentation layer implemented in this repo**, but the application and domain layers are structured so that other presentation layers (web client, backend orchestration service) can reuse the same automation engine via the same ports.
|
||
|
||
---
|
||
|
||
### 4.5 Presentation Surfaces: Web App and Companion
|
||
|
||
This subsection summarizes how the GridPilot Web App / Website and the companion app relate to the automation engine.
|
||
|
||
**GridPilot Web App / Website (external presentation)**
|
||
|
||
- Primary UI for:
|
||
- League and season management, scheduling and calendars.
|
||
- Driver and team views, stats, rating, discovery and social features as described in the concept docs.
|
||
- Runs in browsers, talking to core platform services over HTTP or GraphQL.
|
||
- Produces configuration artifacts such as hosted-session definitions that can be mapped into [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1) on the platform side.
|
||
|
||
**GridPilot Companion App (desktop presentation, this repo)**
|
||
|
||
- Operator UI running on the admin PC.
|
||
- Owns:
|
||
- The relationship with Playwright and the local browser.
|
||
- Local configuration, browser mode, fixture usage and overlay rendering.
|
||
- Integrates with the automation engine via application use-cases and ports that are **shared** with other potential callers (for example, a core platform automation orchestrator).
|
||
|
||
**Communication pattern between website, platform and companion**
|
||
|
||
- A typical flow for a scheduled league race:
|
||
- The web app and core platform services use competition data (league, calendar, formats) to derive a [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1) or equivalent structure server-side.
|
||
- The core platform stores and schedules these configs and can expose them via APIs.
|
||
- The companion app fetches or receives the relevant configs, calls into the application layer (for example [`StartAutomationSessionUseCase`](core/application/use-cases/StartAutomationSessionUseCase.ts:1)) and executes them against iRacing through Playwright.
|
||
- Status and results flow back through overlays and events, and can be consumed both by the companion UI and by platform-side stats and rating services.
|
||
|
||
- This repo provides:
|
||
- The **domain**, **application**, and **infrastructure** for safe, repeatable hosted-session automation.
|
||
- A concrete **desktop presentation** in the form of the companion app.
|
||
|
||
- The web app and core platform are **separate outer layers** that integrate with this automation slice via APIs and must respect the same dependency direction (only depend inwards on application and domain, not vice versa).
|
||
|
||
---
|
||
|
||
## 5. Automation Flow: From Admin Click to iRacing Wizard
|
||
|
||
This section describes how a typical hosted-session automation run flows through the layers.
|
||
|
||
### 5.1 High-level sequence
|
||
|
||
1. **Admin configures a session**
|
||
|
||
- In the companion UI, the admin fills out [`SessionCreationForm`](apps/companion/renderer/components/SessionCreationForm.tsx).
|
||
- The form builds a [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1)-compatible object and calls `window.electronAPI.startAutomation(config)`.
|
||
|
||
2. **IPC → main process**
|
||
|
||
- The preload script forwards this call to an IPC handler in [`ipc-handlers`](apps/companion/main/ipc-handlers.ts).
|
||
- The handler resolves the singleton [`DIContainer`](apps/companion/main/di-container.ts:1) and obtains:
|
||
- [`StartAutomationSessionUseCase`](core/application/use-cases/StartAutomationSessionUseCase.ts:1).
|
||
- [`IAutomationEngine`](core/application/ports/IAutomationEngine.ts:1) and [`ISessionRepository`](core/application/ports/ISessionRepository.ts:1) behind the scenes.
|
||
|
||
3. **Use-case and domain**
|
||
|
||
- [`StartAutomationSessionUseCase`](core/application/use-cases/StartAutomationSessionUseCase.ts:1):
|
||
- Creates a new [`AutomationSession`](core/domain/entities/AutomationSession.ts:1) by calling `AutomationSession.create(config)`, which validates the core invariants (non-empty session name, track, cars).
|
||
- Calls `automationEngine.validateConfiguration(config)` to perform more detailed checks (e.g. compatibility with supported wizard flows).
|
||
- Persists the new session via [`ISessionRepository`](core/application/ports/ISessionRepository.ts:1) (implemented by [`InMemorySessionRepository`](core/infrastructure/repositories/InMemorySessionRepository.ts:1)).
|
||
- Returns a DTO to the IPC handler which is forwarded to the renderer.
|
||
|
||
4. **Browser and Playwright setup**
|
||
|
||
- The [`DIContainer`](apps/companion/main/di-container.ts:1) uses [`AutomationConfig`](core/infrastructure/config/AutomationConfig.ts:1) and [`BrowserModeConfig`](core/infrastructure/config/BrowserModeConfig.ts:1) to:
|
||
- Decide between `production`, `development`, `test` modes.
|
||
- Decide headed vs headless rendering.
|
||
- Potentially start a [`FixtureServer`](core/infrastructure/adapters/automation/engine/FixtureServer.ts:1) in test/fixture modes.
|
||
- [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1) connects to a browser and ensures the context is ready via `connect()` / `ensureBrowserContext()`.
|
||
|
||
5. **Authentication and login**
|
||
|
||
- For real iRacing usage:
|
||
- [`CheckAuthenticationUseCase`](core/application/use-cases/CheckAuthenticationUseCase.ts:1) and [`VerifyAuthenticatedPageUseCase`](core/application/use-cases/VerifyAuthenticatedPageUseCase.ts:1) verify whether the session is still valid.
|
||
- If not, the renderer triggers [`InitiateLoginUseCase`](core/application/use-cases/InitiateLoginUseCase.ts:1) via IPC.
|
||
- The Playwright adapter opens a headed browser window, navigates to the login page, and lets the admin log in manually.
|
||
- Once login is complete, [`PlaywrightAuthSessionService`](core/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService.ts:1) stores cookies in [`SessionCookieStore`](core/infrastructure/adapters/automation/auth/SessionCookieStore.ts:1), and future runs reuse them.
|
||
|
||
6. **Step-by-step wizard automation**
|
||
|
||
- The automation engine (implemented by [`AutomationEngineAdapter`](core/infrastructure/adapters/automation/engine/AutomationEngineAdapter.ts:1) and backed by [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1)) proceeds through steps:
|
||
|
||
- Navigate to hosted sessions.
|
||
- Open "Create a Race" and the hosted-session wizard.
|
||
- For each step (race information, server details, admins, cars, tracks, weather, race options, conditions):
|
||
- Ensure the correct page is active using [`PageStateValidator`](core/domain/services/PageStateValidator.ts:1) and selectors from [`IRACING_SELECTORS`](core/infrastructure/adapters/automation/dom/IRacingSelectors.ts:1).
|
||
- Fill fields and toggles using [`IRacingDomInteractor`](core/infrastructure/adapters/automation/dom/IRacingDomInteractor.ts:1).
|
||
- Click the correct "Next" / "Create Race" / "New Race" buttons, guarded by [`SafeClickService`](core/infrastructure/adapters/automation/dom/SafeClickService.ts:1) and blocked-selector logic.
|
||
|
||
- At each step, the Playwright adapter:
|
||
- Updates the overlay via `updateOverlay(step, message)`.
|
||
- Emits lifecycle events consumed by [`OverlaySyncService`](core/application/services/OverlaySyncService.ts:1), which the companion uses to update [`SessionProgressMonitor`](apps/companion/renderer/components/SessionProgressMonitor.tsx).
|
||
|
||
7. **Checkout and confirmation**
|
||
|
||
- For flows that involve iRacing credits or non-zero prices:
|
||
- [`CheckoutPriceExtractor`](core/infrastructure/adapters/automation/CheckoutPriceExtractor.ts:1) is used by the Playwright adapter to parse price and state from the wizard UI.
|
||
- [`ConfirmCheckoutUseCase`](core/application/use-cases/ConfirmCheckoutUseCase.ts:1) sends a confirmation request to the renderer via [`ElectronCheckoutConfirmationAdapter`](core/infrastructure/adapters/ipc/ElectronCheckoutConfirmationAdapter.ts:1).
|
||
- [`CheckoutConfirmationDialog`](apps/companion/renderer/components/CheckoutConfirmationDialog.tsx) shows the admin the price and state, with an explicit timeout and cancel path.
|
||
- Only after explicit confirmation does the adapter simulate the checkout sequence (and in real mode, additional safeguards ensure no ToS-violating clicks occur).
|
||
|
||
8. **Completion and result**
|
||
|
||
- Once the final step is reached, [`AutomationSession`](core/domain/entities/AutomationSession.ts:1) transitions into a terminal state (`STOPPED_AT_STEP_18`, `FAILED`, etc.).
|
||
- The companion renderer may present a [`RaceCreationResult`](core/domain/value-objects/RaceCreationResult.ts:1) via [`RaceCreationSuccessScreen`](apps/companion/renderer/components/RaceCreationSuccessScreen.tsx).
|
||
- The browser context is closed or re-used based on mode and configuration; debug artifacts may be written by the Playwright adapter for failed runs.
|
||
|
||
### 5.2 Clean Architecture Flow Example
|
||
|
||
**The correct Clean Architecture flow for use cases:**
|
||
|
||
```typescript
|
||
// ❌ WRONG - Current broken pattern
|
||
class GetRaceDetailUseCase {
|
||
async execute(input: GetRaceDetailInput): Promise<Result<void, ApplicationError>> {
|
||
const race = await this.raceRepository.findById(input.raceId);
|
||
|
||
if (!race) {
|
||
const result = Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
|
||
this.output.present(result); // ❌ Use case calling presenter
|
||
return result;
|
||
}
|
||
|
||
const result = Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
|
||
this.output.present(result); // ❌ Use case calling presenter
|
||
return result;
|
||
}
|
||
}
|
||
|
||
// ✅ CORRECT - Clean Architecture
|
||
class GetRaceDetailUseCase {
|
||
async execute(input: GetRaceDetailInput): Promise<Result<GetRaceDetailResult, ApplicationError>> {
|
||
const race = await this.raceRepository.findById(input.raceId);
|
||
|
||
if (!race) {
|
||
return Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
|
||
// ✅ No .present() call - just returns Result
|
||
}
|
||
|
||
return Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
|
||
// ✅ No .present() call - just returns Result
|
||
}
|
||
}
|
||
|
||
// Controller wiring (in infrastructure/presentation layer)
|
||
class RaceController {
|
||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailDTO> {
|
||
// 1. Call use case
|
||
const result = await this.getRaceDetailUseCase.execute(params);
|
||
|
||
// 2. Wire to presenter
|
||
this.raceDetailPresenter.present(result);
|
||
|
||
// 3. Return ViewModel
|
||
return this.raceDetailPresenter.viewModel;
|
||
}
|
||
}
|
||
```
|
||
|
||
**This is the ONLY pattern that respects Clean Architecture.** Your current architecture violates this fundamental principle, which is why you have the "presenter not presented" problem.
|
||
|
||
---
|
||
|
||
## 6. How This Serves Admins, Drivers, Teams
|
||
|
||
Although this repo focuses on automation and companion, it is deliberately shaped to support the value promised in the concept docs.
|
||
|
||
**For admins (see [`ADMINS.md`](docs/concept/ADMINS.md))**
|
||
|
||
- **Less repetitive work**:
|
||
- Hosted-session creation for a league calendar can be scripted once and reused.
|
||
- Validation catches inconsistent configs before iRacing errors do.
|
||
- **Reliability and safety**:
|
||
- Playwright runs in a controlled, tested way, with domain validators and guardrails.
|
||
- Checkout logic always goes through a confirmation dialog and blocked selectors.
|
||
- **Transparency**:
|
||
- Session state (pending, in progress, terminal) and step progress are visible in the companion UI.
|
||
- Debug artifacts for failures are generated in a controlled way, enabling root-cause analysis.
|
||
|
||
**For drivers and teams (via future platform integration)**
|
||
|
||
- The automation engine is designed so that a future league platform can:
|
||
|
||
- Generate [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1) from league and calendar data (cars, tracks, formats) defined in the core competition services.
|
||
- Trigger automation runs at scheduled times, with the admin supervising in the companion app.
|
||
- Use consistent, error-free sessions as the basis for reliable result capture and stats, supporting the narratives in [`DRIVERS.md`](docs/concept/DRIVERS.md), [`TEAMS.md`](docs/concept/TEAMS.md), [`RACING.md`](docs/concept/RACING.md) and [`STATS.md`](docs/concept/STATS.md).
|
||
|
||
The architecture is intentionally **narrow** but **solid**, so that when the broader competition platform arrives, it can treat this automation module as a trustworthy, ToS-compliant “session engine” for many leagues.
|
||
|
||
---
|
||
|
||
## 7. Testing and Validation
|
||
|
||
Testing is structured to mirror the Clean Architecture layering and the automation flow, without enumerating every individual test file.
|
||
|
||
- See [`TESTS.md`](docs/TESTS.md) for detailed structure and conventions; this section summarizes how tests align with the architecture.
|
||
|
||
**Unit tests**
|
||
|
||
- Domain:
|
||
- Focus on entities, value objects and validators such as [`AutomationSession`](tests/unit/domain/entities/AutomationSession.test.ts:1) and [`PageStateValidator`](tests/unit/domain/services/PageStateValidator.test.ts:1).
|
||
- Ensure that hosted-session invariants and page transition rules hold independently of any UI or automation adapter.
|
||
|
||
- Application:
|
||
- Exercise use-cases such as [`StartAutomationSessionUseCase`](tests/unit/application/use-cases/StartAutomationSession.test.ts:1), authentication flows and checkout confirmation coordination.
|
||
- Verify that ports are invoked correctly and that domain state transitions are orchestrated as intended.
|
||
|
||
- Infrastructure:
|
||
- Cover config, logging and key adapters such as [`AutomationConfig`](tests/unit/infrastructure/AutomationConfig.test.ts:1) and [`ElectronCheckoutConfirmationAdapter`](tests/unit/infrastructure/adapters/ElectronCheckoutConfirmationAdapter.test.ts:1).
|
||
- Validate Playwright auth/session helpers and DOM-level utilities in isolation.
|
||
|
||
**Integration tests**
|
||
|
||
- Automation flows:
|
||
- Validate that Playwright-based adapters, DOM helpers, overlay emitters and repositories work together across the iRacing wizard steps using fixtures.
|
||
|
||
- Repositories and browser modes:
|
||
- Ensure that session persistence and browser mode configuration behave consistently across environments.
|
||
|
||
- Interface-level flows:
|
||
- Exercise IPC wiring and renderer integration to ensure the companion presentation layer correctly drives use-cases and reacts to lifecycle events.
|
||
|
||
**E2E and smoke tests**
|
||
|
||
- E2E:
|
||
- Drive full hosted-session workflows end-to-end (fixtures and, when configured, real iRacing) through the companion UI and automation engine.
|
||
- Validate that the system behaves correctly from admin input through to iRacing wizard completion.
|
||
|
||
- Smoke:
|
||
- Provide fast feedback that critical wiring (Electron boot, Playwright initialization, browser-mode toggle) is intact.
|
||
|
||
This layered testing approach mirrors the architecture and makes it safe to evolve selectors, overlay UX or Electron wiring without regressing the core admin value of safe, repeatable automation.
|
||
|
||
---
|
||
|
||
## 8. Evolution and Integration with the Competition Platform
|
||
|
||
The concept docs describe a much broader platform: leagues, teams, seasons, stats, rating, social features and structured protests. This section explains how the current automation & companion architecture is designed to plug into that future.
|
||
|
||
### 8.1 Integration points
|
||
|
||
In a future GridPilot platform:
|
||
|
||
- A **core competition service** would own:
|
||
- League, season, and race schedules.
|
||
- Team and driver registration.
|
||
- Points, standings, penalties, stats and rating.
|
||
- The competition identity and rating model described in [`STATS.md`](docs/concept/STATS.md) and [`RATING.md`](docs/concept/RATING.md).
|
||
|
||
- The **automation engine** in this repo would be used as a **hosted-session execution backend**:
|
||
|
||
- The platform would derive [`HostedSessionConfig`](core/domain/entities/HostedSessionConfig.ts:1) objects from league/season metadata (cars, tracks, formats) defined in the platform.
|
||
- It would call into the automation layer via explicit use-cases or IPC/API, using the same ports used by the companion app today.
|
||
- The Electron companion may remain the primary operator UI, or an additional headless orchestration mode could be added (still respecting ToS and admin control).
|
||
|
||
- A **future rating service** (part of the core platform, not this repo) would:
|
||
- Compute GridPilot Rating using league results, incidents, team points and attendance as outlined in [`RATING.md`](docs/concept/RATING.md).
|
||
- Treat automation outputs (trusted hosted-session configs, results, and penalty-aware outcomes) as structured inputs from this repo, not as rating logic implemented here.
|
||
- Depend on the platform’s stats and competition data model from [`STATS.md`](docs/concept/STATS.md) rather than any in-repo class or service.
|
||
|
||
### 8.2 Design decisions that support this evolution
|
||
|
||
- **Clean separation of domain vs adapters**:
|
||
The automation domain (sessions, steps, validation) is free of Electron/Playwright details, making it straightforward to embed into other hosts.
|
||
|
||
- **Ports for automation & auth**:
|
||
Using [`IAutomationEngine`](core/application/ports/IAutomationEngine.ts:1), [`IScreenAutomation`](core/application/ports/IScreenAutomation.ts:1), [`IAuthenticationService`](core/application/ports/IAuthenticationService.ts:1) means new drivers (or alternative browser runtimes) can be introduced without changing the use-cases.
|
||
|
||
- **Explicit checkout/confirmation path**:
|
||
The combination of [`CheckoutPrice`](core/domain/value-objects/CheckoutPrice.ts:1), [`CheckoutState`](core/domain/value-objects/CheckoutState.ts:1), [`CheckoutConfirmation`](core/domain/value-objects/CheckoutConfirmation.ts:1) and the UI-side confirmation port aligns with the transparency and fairness requirements in [`ADMINS.md`](docs/concept/ADMINS.md) and [`RACING.md`](docs/concept/RACING.md).
|
||
|
||
- **Overlay and lifecycle emitter**:
|
||
The overlay and [`IAutomationLifecycleEmitter`](core/infrastructure/adapters/IAutomationLifecycleEmitter.ts:1) abstraction make it easy to:
|
||
- Drive visual overlays (like those described in admin QoL features) across different frontends.
|
||
- Feed telemetry into a central stats/ops UI for debugging, while keeping the engine itself small.
|
||
|
||
### 8.3 What this document intentionally does *not* specify
|
||
|
||
To stay honest to the current repo and avoid overpromising:
|
||
|
||
- There is **no** web API, web client or full league database in this repo. Any such components described in older docs were removed from this architecture description.
|
||
- There is **no** implementation of the full complaints/penalties engine, team scoring or cross-league stats here – only the automation slice that can support them later.
|
||
- Any mention of those features should be read as **future integration context**, not current implementation.
|
||
|
||
When those broader services exist (likely as separate apps/services), they should define their own architecture documents and link back here when describing how they use the hosted-session automation engine.
|
||
|
||
---
|
||
|
||
## 9. Cross-References
|
||
|
||
- Concept and product vision:
|
||
- [`CONCEPT.md`](docs/concept/CONCEPT.md)
|
||
- [`ADMINS.md`](docs/concept/ADMINS.md)
|
||
- [`DRIVERS.md`](docs/concept/DRIVERS.md)
|
||
- [`COMPETITION.md`](docs/concept/COMPETITION.md)
|
||
- [`RACING.md`](docs/concept/RACING.md)
|
||
- [`STATS.md`](docs/concept/STATS.md)
|
||
- [`RATING.md`](docs/concept/RATING.md)
|
||
- [`SOCIAL.md`](docs/concept/SOCIAL.md)
|
||
- [`TEAMS.md`](docs/concept/TEAMS.md)
|
||
|
||
- Technology and tests:
|
||
- [`TECH.md`](docs/TECH.md)
|
||
- [`TESTS.md`](docs/TESTS.md)
|
||
|
||
This [`ARCHITECTURE.md`](docs/ARCHITECTURE.md) is the source of truth for how the current repo implements the **automation engine + companion app**, and how that slice is intended to underpin the broader GridPilot competition platform over time. |