refactor: restructure to monorepo with apps and packages directories - Move companion app to apps/companion with electron-vite - Move domain/application/infrastructure to packages/ - Fix ELECTRON_RUN_AS_NODE env var issue for VS Code terminal - Remove legacy esbuild bundler (replaced by electron-vite) - Update workspace scripts in root package.json

This commit is contained in:
2025-11-22 00:25:06 +01:00
parent d20554df55
commit 7eae6e3bd4
39 changed files with 515 additions and 1939 deletions

View File

@@ -0,0 +1,143 @@
import { randomUUID } from 'crypto';
import { StepId } from '../value-objects/StepId';
import { SessionState } from '../value-objects/SessionState';
import { HostedSessionConfig } from './HostedSessionConfig';
export class AutomationSession {
private readonly _id: string;
private _currentStep: StepId;
private _state: SessionState;
private readonly _config: HostedSessionConfig;
private _startedAt?: Date;
private _completedAt?: Date;
private _errorMessage?: string;
private constructor(
id: string,
currentStep: StepId,
state: SessionState,
config: HostedSessionConfig
) {
this._id = id;
this._currentStep = currentStep;
this._state = state;
this._config = config;
}
static create(config: HostedSessionConfig): AutomationSession {
if (!config.sessionName || config.sessionName.trim() === '') {
throw new Error('Session name cannot be empty');
}
if (!config.trackId || config.trackId.trim() === '') {
throw new Error('Track ID is required');
}
if (!config.carIds || config.carIds.length === 0) {
throw new Error('At least one car must be selected');
}
return new AutomationSession(
randomUUID(),
StepId.create(1),
SessionState.create('PENDING'),
config
);
}
get id(): string {
return this._id;
}
get currentStep(): StepId {
return this._currentStep;
}
get state(): SessionState {
return this._state;
}
get config(): HostedSessionConfig {
return this._config;
}
get startedAt(): Date | undefined {
return this._startedAt;
}
get completedAt(): Date | undefined {
return this._completedAt;
}
get errorMessage(): string | undefined {
return this._errorMessage;
}
start(): void {
if (!this._state.isPending()) {
throw new Error('Cannot start session that is not pending');
}
this._state = SessionState.create('IN_PROGRESS');
this._startedAt = new Date();
}
transitionToStep(targetStep: StepId): void {
if (!this._state.isInProgress()) {
throw new Error('Cannot transition steps when session is not in progress');
}
if (this._currentStep.equals(targetStep)) {
throw new Error('Already at this step');
}
if (targetStep.value < this._currentStep.value) {
throw new Error('Cannot move backward - steps must progress forward only');
}
if (targetStep.value !== this._currentStep.value + 1) {
throw new Error('Cannot skip steps - must transition sequentially');
}
this._currentStep = targetStep;
if (this._currentStep.isFinalStep()) {
this._state = SessionState.create('STOPPED_AT_STEP_18');
this._completedAt = new Date();
}
}
pause(): void {
if (!this._state.isInProgress()) {
throw new Error('Cannot pause session that is not in progress');
}
this._state = SessionState.create('PAUSED');
}
resume(): void {
if (this._state.value !== 'PAUSED') {
throw new Error('Cannot resume session that is not paused');
}
this._state = SessionState.create('IN_PROGRESS');
}
fail(errorMessage: string): void {
if (this._state.isTerminal()) {
throw new Error('Cannot fail terminal session');
}
this._state = SessionState.create('FAILED');
this._errorMessage = errorMessage;
this._completedAt = new Date();
}
isAtModalStep(): boolean {
return this._currentStep.isModalStep();
}
getElapsedTime(): number {
if (!this._startedAt) {
return 0;
}
const endTime = this._completedAt || new Date();
const elapsed = endTime.getTime() - this._startedAt.getTime();
return elapsed > 0 ? elapsed : 1;
}
}

View File

@@ -0,0 +1,20 @@
export interface HostedSessionConfig {
sessionName: string;
serverName: string;
password: string;
adminPassword: string;
maxDrivers: number;
trackId: string;
carIds: string[];
weatherType: 'static' | 'dynamic';
timeOfDay: 'morning' | 'afternoon' | 'evening' | 'night';
sessionDuration: number;
practiceLength: number;
qualifyingLength: number;
warmupLength: number;
raceLength: number;
startType: 'standing' | 'rolling';
restarts: 'single-file' | 'double-file';
damageModel: 'off' | 'limited' | 'realistic';
trackState: 'auto' | 'clean' | 'moderately-low' | 'moderately-high' | 'optimum';
}

View File

@@ -0,0 +1,9 @@
import { StepId } from '../value-objects/StepId';
export interface StepExecution {
stepId: StepId;
startedAt: Date;
completedAt?: Date;
success: boolean;
error?: string;
}