diff --git a/.gitignore b/.gitignore index ee79715..9204635 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env +__pycache__/ *.pyc \ No newline at end of file diff --git a/ai_intelligence_layer/.env b/ai_intelligence_layer/.env deleted file mode 100644 index 4603b93..0000000 --- a/ai_intelligence_layer/.env +++ /dev/null @@ -1,25 +0,0 @@ -# Gemini API Configuration -GEMINI_API_KEY=AIzaSyDK_jxVlJUpzyxuiGcopSFkiqMAUD3-w0I -GEMINI_MODEL=gemini-2.5-flash - -# Service Configuration -AI_SERVICE_PORT=9000 -AI_SERVICE_HOST=0.0.0.0 - -# Enrichment Service Integration -ENRICHMENT_SERVICE_URL=http://localhost:8000 -ENRICHMENT_FETCH_LIMIT=10 - -# Demo Mode (enables caching and consistent responses for demos) -DEMO_MODE=false - -# Fast Mode (use shorter prompts for faster responses) -FAST_MODE=true - -# Strategy Generation Settings -STRATEGY_COUNT=3 # Number of strategies to generate (3 for testing, 20 for production) - -# Performance Settings -BRAINSTORM_TIMEOUT=90 -ANALYZE_TIMEOUT=120 -GEMINI_MAX_RETRIES=3 diff --git a/ai_intelligence_layer/__pycache__/config.cpython-313.pyc b/ai_intelligence_layer/__pycache__/config.cpython-313.pyc deleted file mode 100644 index 4d2f9eb..0000000 Binary files a/ai_intelligence_layer/__pycache__/config.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/__pycache__/main.cpython-312.pyc b/ai_intelligence_layer/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 135fba6..0000000 Binary files a/ai_intelligence_layer/__pycache__/main.cpython-312.pyc and /dev/null differ diff --git a/ai_intelligence_layer/__pycache__/main.cpython-313.pyc b/ai_intelligence_layer/__pycache__/main.cpython-313.pyc deleted file mode 100644 index 2f3e0ff..0000000 Binary files a/ai_intelligence_layer/__pycache__/main.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/__pycache__/test_api.cpython-312-pytest-8.3.3.pyc b/ai_intelligence_layer/__pycache__/test_api.cpython-312-pytest-8.3.3.pyc deleted file mode 100644 index 2bd6b69..0000000 Binary files a/ai_intelligence_layer/__pycache__/test_api.cpython-312-pytest-8.3.3.pyc and /dev/null differ diff --git a/ai_intelligence_layer/__pycache__/test_buffer_usage.cpython-312-pytest-8.3.3.pyc b/ai_intelligence_layer/__pycache__/test_buffer_usage.cpython-312-pytest-8.3.3.pyc deleted file mode 100644 index 2da0960..0000000 Binary files a/ai_intelligence_layer/__pycache__/test_buffer_usage.cpython-312-pytest-8.3.3.pyc and /dev/null differ diff --git a/ai_intelligence_layer/__pycache__/test_components.cpython-312-pytest-8.3.3.pyc b/ai_intelligence_layer/__pycache__/test_components.cpython-312-pytest-8.3.3.pyc deleted file mode 100644 index 523b9f3..0000000 Binary files a/ai_intelligence_layer/__pycache__/test_components.cpython-312-pytest-8.3.3.pyc and /dev/null differ diff --git a/ai_intelligence_layer/__pycache__/test_webhook_push.cpython-312-pytest-8.3.3.pyc b/ai_intelligence_layer/__pycache__/test_webhook_push.cpython-312-pytest-8.3.3.pyc deleted file mode 100644 index 1b75c7b..0000000 Binary files a/ai_intelligence_layer/__pycache__/test_webhook_push.cpython-312-pytest-8.3.3.pyc and /dev/null differ diff --git a/ai_intelligence_layer/__pycache__/test_with_enrichment_service.cpython-312-pytest-8.3.3.pyc b/ai_intelligence_layer/__pycache__/test_with_enrichment_service.cpython-312-pytest-8.3.3.pyc deleted file mode 100644 index 1d3b1f2..0000000 Binary files a/ai_intelligence_layer/__pycache__/test_with_enrichment_service.cpython-312-pytest-8.3.3.pyc and /dev/null differ diff --git a/ai_intelligence_layer/main.py b/ai_intelligence_layer/main.py index f2fe67a..8140916 100644 --- a/ai_intelligence_layer/main.py +++ b/ai_intelligence_layer/main.py @@ -49,6 +49,7 @@ strategy_generator: StrategyGenerator = None telemetry_client: TelemetryClient = None current_race_context: RaceContext = None # Store race context globally last_control_command: Dict[str, int] = {"brake_bias": 5, "differential_slip": 5} # Store last command +strategy_history: List[Dict[str, Any]] = [] # Track past strategies for continuity # WebSocket connection manager class ConnectionManager: @@ -378,7 +379,7 @@ async def websocket_pi_endpoint(websocket: WebSocket): 2. AI layer processes telemetry and generates strategies 3. AI layer pushes control commands back to Pi (brake_bias, differential_slip) """ - global current_race_context, last_control_command + global current_race_context, last_control_command, strategy_history vehicle_id = await websocket_manager.connect(websocket) @@ -389,7 +390,11 @@ async def websocket_pi_endpoint(websocket: WebSocket): # Reset last control command to neutral for new session last_control_command = {"brake_bias": 5, "differential_slip": 5} + # Clear strategy history for new race + strategy_history = [] + logger.info("[WebSocket] Telemetry buffer cleared for new connection") + logger.info("[WebSocket] Strategy history cleared for new race") # Notify dashboards of new vehicle connection await dashboard_manager.broadcast({ @@ -454,12 +459,26 @@ async def websocket_pi_endpoint(websocket: WebSocket): try: response = await strategy_generator.generate( enriched_telemetry=buffer_data, - race_context=current_race_context + race_context=current_race_context, + strategy_history=strategy_history ) # Extract top strategy (first one) top_strategy = response.strategies[0] if response.strategies else None + # Add to strategy history + if top_strategy: + strategy_history.append({ + "lap": lap_number, + "strategy_name": top_strategy.strategy_name, + "risk_level": top_strategy.risk_level, + "brief_description": top_strategy.brief_description, + "reasoning": top_strategy.reasoning + }) + # Keep only last 10 strategies + if len(strategy_history) > 10: + strategy_history.pop(0) + # Generate control commands based on strategy control_command = generate_control_command( lap_number=lap_number, @@ -490,6 +509,11 @@ async def websocket_pi_endpoint(websocket: WebSocket): "type": "lap_data", "vehicle_id": vehicle_id, "lap_data": enriched, + "race_context": { + "position": current_race_context.driver_state.current_position, + "gap_to_leader": current_race_context.driver_state.gap_to_leader, + "gap_to_ahead": current_race_context.driver_state.gap_to_ahead + }, "control_output": { "brake_bias": control_command["brake_bias"], "differential_slip": control_command["differential_slip"] @@ -497,7 +521,8 @@ async def websocket_pi_endpoint(websocket: WebSocket): "strategy": { "strategy_name": top_strategy.strategy_name, "risk_level": top_strategy.risk_level, - "brief_description": top_strategy.brief_description + "brief_description": top_strategy.brief_description, + "reasoning": top_strategy.reasoning } if top_strategy else None, "timestamp": datetime.now().isoformat() }) @@ -527,6 +552,11 @@ async def websocket_pi_endpoint(websocket: WebSocket): "type": "lap_data", "vehicle_id": vehicle_id, "lap_data": enriched, + "race_context": { + "position": current_race_context.driver_state.current_position, + "gap_to_leader": current_race_context.driver_state.gap_to_leader, + "gap_to_ahead": current_race_context.driver_state.gap_to_ahead + }, "control_output": { "brake_bias": 5, "differential_slip": 5 diff --git a/ai_intelligence_layer/models/__pycache__/input_models.cpython-312.pyc b/ai_intelligence_layer/models/__pycache__/input_models.cpython-312.pyc deleted file mode 100644 index eb48bff..0000000 Binary files a/ai_intelligence_layer/models/__pycache__/input_models.cpython-312.pyc and /dev/null differ diff --git a/ai_intelligence_layer/models/__pycache__/input_models.cpython-313.pyc b/ai_intelligence_layer/models/__pycache__/input_models.cpython-313.pyc deleted file mode 100644 index 7e69982..0000000 Binary files a/ai_intelligence_layer/models/__pycache__/input_models.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/models/__pycache__/output_models.cpython-312.pyc b/ai_intelligence_layer/models/__pycache__/output_models.cpython-312.pyc deleted file mode 100644 index d1a9614..0000000 Binary files a/ai_intelligence_layer/models/__pycache__/output_models.cpython-312.pyc and /dev/null differ diff --git a/ai_intelligence_layer/models/__pycache__/output_models.cpython-313.pyc b/ai_intelligence_layer/models/__pycache__/output_models.cpython-313.pyc deleted file mode 100644 index 78b53da..0000000 Binary files a/ai_intelligence_layer/models/__pycache__/output_models.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/models/input_models.py b/ai_intelligence_layer/models/input_models.py index 797dcbb..2e7a7ee 100644 --- a/ai_intelligence_layer/models/input_models.py +++ b/ai_intelligence_layer/models/input_models.py @@ -62,6 +62,7 @@ class Strategy(BaseModel): pit_laps: List[int] = Field(..., description="Lap numbers for pit stops") tire_sequence: List[Literal["soft", "medium", "hard", "intermediate", "wet"]] = Field(..., description="Tire compounds in order") brief_description: str = Field(..., description="One sentence rationale") + reasoning: Optional[str] = Field(None, description="Detailed explanation including strategy change/continuity rationale") risk_level: Literal["low", "medium", "high", "critical"] = Field(..., description="Risk assessment") key_assumption: str = Field(..., description="Main assumption this strategy relies on") diff --git a/ai_intelligence_layer/prompts/__pycache__/analyze_prompt.cpython-313.pyc b/ai_intelligence_layer/prompts/__pycache__/analyze_prompt.cpython-313.pyc deleted file mode 100644 index 6ebc231..0000000 Binary files a/ai_intelligence_layer/prompts/__pycache__/analyze_prompt.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/prompts/__pycache__/brainstorm_prompt.cpython-313.pyc b/ai_intelligence_layer/prompts/__pycache__/brainstorm_prompt.cpython-313.pyc deleted file mode 100644 index 260ec82..0000000 Binary files a/ai_intelligence_layer/prompts/__pycache__/brainstorm_prompt.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/prompts/brainstorm_prompt.py b/ai_intelligence_layer/prompts/brainstorm_prompt.py index d1c4ae9..a9ad13b 100644 --- a/ai_intelligence_layer/prompts/brainstorm_prompt.py +++ b/ai_intelligence_layer/prompts/brainstorm_prompt.py @@ -9,7 +9,8 @@ from config import get_settings def build_brainstorm_prompt_fast( enriched_telemetry: List[EnrichedTelemetryWebhook], - race_context: RaceContext + race_context: RaceContext, + strategy_history: List[dict] = None ) -> str: """Build a faster, more concise prompt for quicker responses (lap-level data).""" settings = get_settings() @@ -27,17 +28,29 @@ def build_brainstorm_prompt_fast( if gap_to_leader > 0 and position > 1: comp_info += f", {gap_to_leader:.1f}s from leader" + # Format strategy history (last 3 strategies) + history_text = "" + if strategy_history and len(strategy_history) > 0: + recent_history = strategy_history[-3:] # Last 3 strategies + history_lines = [] + for h in recent_history: + history_lines.append(f"Lap {h['lap']}: {h['strategy_name']} (Risk: {h['risk_level']})") + history_text = f"\n\nPAST STRATEGIES:\n" + "\n".join(history_lines) + history_text += f"\n\nREQUIREMENT: If changing from previous strategy '{recent_history[-1]['strategy_name']}', explain WHY the switch is necessary. If staying with same approach, explain continuity." + else: + history_text = "\n\nNOTE: This is the first strategy generation - no previous strategy to compare." + if count == 1: # Ultra-fast mode: just generate 1 strategy return f"""Generate 1 F1 race strategy for {race_context.driver_state.driver_name} at {race_context.race_info.track_name}. CURRENT: Lap {race_context.race_info.current_lap}/{race_context.race_info.total_laps}, {comp_info}, {race_context.driver_state.current_tire_compound} tires ({race_context.driver_state.tire_age_laps} laps old) -TELEMETRY: Tire deg {latest.tire_degradation_rate:.2f}, Cliff risk {latest.tire_cliff_risk:.2f}, Pace {latest.pace_trend}, Position trend {latest.position_trend}, Competitive pressure {latest.competitive_pressure:.2f} +TELEMETRY: Tire deg {latest.tire_degradation_rate:.2f}, Cliff risk {latest.tire_cliff_risk:.2f}, Pace {latest.pace_trend}, Position trend {latest.position_trend}, Competitive pressure {latest.competitive_pressure:.2f}{history_text} Generate 1 optimal strategy considering competitive position. Min 2 tire compounds required. -JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "name", "stop_count": 1, "pit_laps": [32], "tire_sequence": ["medium", "hard"], "brief_description": "one sentence", "risk_level": "medium", "key_assumption": "main assumption"}}]}}""" +JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "name", "stop_count": 1, "pit_laps": [32], "tire_sequence": ["medium", "hard"], "brief_description": "one sentence", "reasoning": "detailed explanation including strategy change rationale if applicable", "risk_level": "medium", "key_assumption": "main assumption"}}]}}""" elif count <= 5: # Fast mode: 2-5 strategies with different approaches @@ -47,11 +60,13 @@ CURRENT: Lap {race_context.race_info.current_lap}/{race_context.race_info.total_ TELEMETRY: Tire deg {latest.tire_degradation_rate:.2f}, Cliff risk {latest.tire_cliff_risk:.2f}, Pace {latest.pace_trend}, Delta {latest.performance_delta:+.2f}s, Position trend {latest.position_trend}, Competitive pressure {latest.competitive_pressure:.2f} -COMPETITIVE SITUATION: Gap to ahead {gap_to_ahead:.1f}s ({"DRS RANGE - attack opportunity!" if gap_to_ahead < 1.0 else "close battle" if gap_to_ahead < 3.0 else "need to push"}) +COMPETITIVE SITUATION: Gap to ahead {gap_to_ahead:.1f}s ({"DRS RANGE - attack opportunity!" if gap_to_ahead < 1.0 else "close battle" if gap_to_ahead < 3.0 else "need to push"}){history_text} Generate {count} strategies balancing tire management with competitive pressure. Consider if aggressive undercut makes sense given gaps. Min 2 tire compounds each. -JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "Conservative Stay Out", "stop_count": 1, "pit_laps": [35], "tire_sequence": ["medium", "hard"], "brief_description": "extend current stint then hard tires to end", "risk_level": "low", "key_assumption": "tire cliff risk stays below 0.7"}}]}}""" +CRITICAL: Each strategy MUST include 'reasoning' field explaining the approach and, if applicable, why it differs from or continues the previous strategy. + +JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "Conservative Stay Out", "stop_count": 1, "pit_laps": [35], "tire_sequence": ["medium", "hard"], "brief_description": "extend current stint then hard tires to end", "reasoning": "Detailed explanation of this strategy including why we're switching from/continuing previous approach based on current telemetry and competitive situation", "risk_level": "low", "key_assumption": "tire cliff risk stays below 0.7"}}]}}""" return f"""Generate {count} F1 race strategies for {race_context.driver_state.driver_name} at {race_context.race_info.track_name}. @@ -59,16 +74,19 @@ CURRENT: Lap {race_context.race_info.current_lap}/{race_context.race_info.total_ TELEMETRY: Tire deg {latest.tire_degradation_rate:.2f}, Cliff risk {latest.tire_cliff_risk:.2f}, Pace {latest.pace_trend}, Delta {latest.performance_delta:+.2f}s, Position trend {latest.position_trend}, Competitive pressure {latest.competitive_pressure:.2f} -COMPETITIVE: Gap ahead {gap_to_ahead:.1f}s, Position trending {latest.position_trend} +COMPETITIVE: Gap ahead {gap_to_ahead:.1f}s, Position trending {latest.position_trend}{history_text} Generate {count} diverse strategies considering both tire management AND competitive positioning. Min 2 compounds. -JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "name", "stop_count": 1, "pit_laps": [32], "tire_sequence": ["medium", "hard"], "brief_description": "one sentence", "risk_level": "low|medium|high|critical", "key_assumption": "main assumption"}}]}}""" +CRITICAL: Each strategy MUST include 'reasoning' field with detailed rationale and strategy continuity/change explanation. + +JSON: {{"strategies": [{{"strategy_id": 1, "strategy_name": "name", "stop_count": 1, "pit_laps": [32], "tire_sequence": ["medium", "hard"], "brief_description": "one sentence", "reasoning": "Comprehensive explanation including strategy evolution context", "risk_level": "low|medium|high|critical", "key_assumption": "main assumption"}}]}}""" def build_brainstorm_prompt( enriched_telemetry: List[EnrichedTelemetryWebhook], - race_context: RaceContext + race_context: RaceContext, + strategy_history: List[dict] = None ) -> str: """ Build the brainstorm prompt for Gemini. @@ -76,6 +94,7 @@ def build_brainstorm_prompt( Args: enriched_telemetry: Recent enriched telemetry data race_context: Current race context + strategy_history: List of previous strategies for context Returns: Formatted prompt string @@ -108,6 +127,14 @@ def build_brainstorm_prompt( "gap_seconds": round(c.gap_seconds, 1) }) + # Format strategy history + history_section = "" + if strategy_history and len(strategy_history) > 0: + history_section = "\n\nSTRATEGY HISTORY (Recent decisions):\n" + for h in strategy_history[-5:]: # Last 5 strategies + history_section += f"- Lap {h['lap']}: {h['strategy_name']} (Risk: {h['risk_level']}) - {h.get('brief_description', 'N/A')}\n" + history_section += f"\nCONTEXT: Previous strategy was '{strategy_history[-1]['strategy_name']}'. If recommending a change, clearly explain WHY. If continuing same approach, explain the continuity.\n" + prompt = f"""You are an expert F1 strategist. Generate 20 diverse race strategies based on lap-level telemetry AND competitive positioning. LAP-LEVEL TELEMETRY METRICS: @@ -135,7 +162,7 @@ COMPETITORS: {competitors_data} ENRICHED TELEMETRY (Last {len(telemetry_data)} laps, newest first): -{telemetry_data} +{telemetry_data}{history_section} KEY INSIGHTS: - Latest tire degradation rate: {latest.tire_degradation_rate:.3f} @@ -160,9 +187,12 @@ For each strategy provide: - pit_laps: [array of lap numbers] - tire_sequence: [array of compounds: "soft", "medium", "hard"] - brief_description: One sentence rationale +- reasoning: DETAILED explanation (3-5 sentences) including: (1) Why this strategy fits current conditions, (2) How it addresses telemetry trends, (3) Strategy evolution - why changing from or continuing previous approach - risk_level: "low", "medium", "high", or "critical" - key_assumption: Main assumption this strategy relies on +CRITICAL: The 'reasoning' field must include strategy continuity/change rationale relative to past decisions. + OUTPUT FORMAT (JSON only, no markdown): {{ "strategies": [ @@ -173,6 +203,7 @@ OUTPUT FORMAT (JSON only, no markdown): "pit_laps": [32], "tire_sequence": ["medium", "hard"], "brief_description": "Extend mediums to lap 32, safe finish on hards", + "reasoning": "Current tire degradation at 0.45 suggests we can safely extend to lap 32 before hitting the cliff. This maintains our conservative approach from lap 3 as conditions haven't changed significantly - tire deg is stable and we're not under immediate competitive pressure. The hard tire finish provides low-risk race completion.", "risk_level": "low", "key_assumption": "Tire degradation stays below 0.85 until lap 32" }} diff --git a/ai_intelligence_layer/services/__pycache__/gemini_client.cpython-313.pyc b/ai_intelligence_layer/services/__pycache__/gemini_client.cpython-313.pyc deleted file mode 100644 index 13195d8..0000000 Binary files a/ai_intelligence_layer/services/__pycache__/gemini_client.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/services/__pycache__/strategy_analyzer.cpython-313.pyc b/ai_intelligence_layer/services/__pycache__/strategy_analyzer.cpython-313.pyc deleted file mode 100644 index 5c7b2fe..0000000 Binary files a/ai_intelligence_layer/services/__pycache__/strategy_analyzer.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/services/__pycache__/strategy_generator.cpython-313.pyc b/ai_intelligence_layer/services/__pycache__/strategy_generator.cpython-313.pyc deleted file mode 100644 index 36adeca..0000000 Binary files a/ai_intelligence_layer/services/__pycache__/strategy_generator.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/services/__pycache__/telemetry_client.cpython-313.pyc b/ai_intelligence_layer/services/__pycache__/telemetry_client.cpython-313.pyc deleted file mode 100644 index c263280..0000000 Binary files a/ai_intelligence_layer/services/__pycache__/telemetry_client.cpython-313.pyc and /dev/null differ diff --git a/ai_intelligence_layer/services/strategy_generator.py b/ai_intelligence_layer/services/strategy_generator.py index 82f2081..3e34b2f 100644 --- a/ai_intelligence_layer/services/strategy_generator.py +++ b/ai_intelligence_layer/services/strategy_generator.py @@ -25,7 +25,8 @@ class StrategyGenerator: async def generate( self, enriched_telemetry: List[EnrichedTelemetryWebhook], - race_context: RaceContext + race_context: RaceContext, + strategy_history: List[dict] = None ) -> BrainstormResponse: """ Generate 20 diverse race strategies. @@ -33,6 +34,7 @@ class StrategyGenerator: Args: enriched_telemetry: Recent enriched telemetry data race_context: Current race context + strategy_history: List of previous strategies for continuity Returns: BrainstormResponse with 20 strategies @@ -41,13 +43,15 @@ class StrategyGenerator: Exception: If generation fails """ logger.info(f"Generating strategies using {len(enriched_telemetry)} laps of telemetry") + if strategy_history: + logger.info(f"Including {len(strategy_history)} previous strategies in context") # Build prompt (use fast mode if enabled) if self.settings.fast_mode: from prompts.brainstorm_prompt import build_brainstorm_prompt_fast - prompt = build_brainstorm_prompt_fast(enriched_telemetry, race_context) + prompt = build_brainstorm_prompt_fast(enriched_telemetry, race_context, strategy_history or []) else: - prompt = build_brainstorm_prompt(enriched_telemetry, race_context) + prompt = build_brainstorm_prompt(enriched_telemetry, race_context, strategy_history or []) # Generate with Gemini (high temperature for creativity) response_data = await self.gemini_client.generate_json( diff --git a/ai_intelligence_layer/static/dashboard.html b/ai_intelligence_layer/static/dashboard.html index cb6a5d5..498aeb2 100644 --- a/ai_intelligence_layer/static/dashboard.html +++ b/ai_intelligence_layer/static/dashboard.html @@ -715,14 +715,14 @@ } function handleMessage(data) { - const { type, vehicle_id, lap_data, control_output, strategy, timestamp } = data; + const { type, vehicle_id, lap_data, race_context, control_output, strategy, timestamp } = data; if (type === 'vehicle_connected') { addVehicle(vehicle_id, timestamp); } else if (type === 'vehicle_disconnected') { removeVehicle(vehicle_id); } else if (type === 'lap_data') { - addLapData(vehicle_id, lap_data, control_output, strategy, timestamp); + addLapData(vehicle_id, lap_data, race_context, control_output, strategy, timestamp); } } @@ -750,7 +750,7 @@ } } - function addLapData(vehicleId, lapData, controlOutput, strategy, timestamp) { + function addLapData(vehicleId, lapData, raceContext, controlOutput, strategy, timestamp) { if (!vehicles.has(vehicleId)) { addVehicle(vehicleId, timestamp); } @@ -758,6 +758,7 @@ const vehicle = vehicles.get(vehicleId); vehicle.laps.push({ ...lapData, + race_context: raceContext, // Add race context with position and gaps control_output: controlOutput, strategy: strategy, timestamp: timestamp @@ -907,6 +908,37 @@ let bodyHtml = ''; + // Race Position section (lap_time, position, gaps) + if (lap.race_context || lap.lap_time) { + bodyHtml += ` +