TiGa-RCE commited on
Commit
6556847
·
verified ·
1 Parent(s): 94faee6

Polish run summary, replay identity and responsive workspace

Browse files

Compact result and download actions; keep replay map and playback together beside native black box; show validated recording identity and current setup state; compact initial guide/agent utilities; measure wrapped navigation height. Verified isolated private preview before production using fresh Luna and Sol visual GPU training, native CPU recordings, downloads, deletion and imports; strict artifact validation, 91 frontend/controller checks and responsive geometry. Only three frontend files and their two hash manifests change. No simulator, Godot, Swift, auth/storage, visibility or retained parent changes.

Files changed (5) hide show
  1. FRONTEND_SOURCE.json +8 -8
  2. RELEASE_MANIFEST.json +4 -4
  3. client.js +80 -12
  4. index.html +33 -20
  5. track-map.js +1 -1
FRONTEND_SOURCE.json CHANGED
@@ -5,23 +5,23 @@
5
  "source": "Runner r14 single-engine browser UI; selected .track tile renderer added from simulator atlas",
6
  "source_sha256": "65d1e5f2c4e19b4951b828e82510005ab99a5270cf3dcb9f4eaa0fa1af8c40f8"
7
  },
8
- "client_sha256": "cfb536dc3903beeafae8f1f883390423fd0a3b328575bb935184ee537022e656",
9
  "identity_note": "The historical source fields describe the base frontend; local packaged file hashes describe this candidate's derived frontend.",
10
- "local_customization_id": "replay-document-scrolling-v1-20261023",
11
- "output_sha256": "2ca75bb63f2fd64368c9d58bca51d724b6d177422e0961fb8abbecbf28d61b69",
12
  "packaged_frontend_files": {
13
  "agent-manifest.json": "7187c85327d588cb3b81f5ddf60bae099567a656bcceac8147ecb2227e07f561",
14
  "agent-tools.js": "134b8c511a04671edf2129b7b4f42d2c0399713471feefaaeee542efab7127b8",
15
  "agents.html": "3e129500b30299ce628f04208dc7e77a2592991adb9c0d92042b63249077caa6",
16
  "assets/track-atlas.json": "d97c8900e3e5fb17734b5bd61d1f4f4b196d9be30af9848e2e273ae7e4079ae4",
17
- "client.js": "cfb536dc3903beeafae8f1f883390423fd0a3b328575bb935184ee537022e656",
18
  "help.html": "58959e994a8e4bbd3c70b2a6256cb699ac615cde718c6d0288fabeee909098a2",
19
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
20
- "index.html": "2ca75bb63f2fd64368c9d58bca51d724b6d177422e0961fb8abbecbf28d61b69",
21
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
22
- "track-map.js": "1612b75e95ccbbb2da4a47d8c8bf2ee406e8f40c626475ae2a1c3c19e119a139"
23
  },
24
- "packaged_frontend_sha256": "4742169f2e37cf676a10f6f16ce4f829ff21f32aefc33dd652a9a30aa8946d62",
25
  "packaged_frontend_sha256_format": "SHA-256 of UTF-8 JSON for packaged_frontend_files, sorted keys, compact separators, no trailing newline",
26
- "status": "Replay actions before viewer, document-scrolling black box and accessible return to playback"
27
  }
 
5
  "source": "Runner r14 single-engine browser UI; selected .track tile renderer added from simulator atlas",
6
  "source_sha256": "65d1e5f2c4e19b4951b828e82510005ab99a5270cf3dcb9f4eaa0fa1af8c40f8"
7
  },
8
+ "client_sha256": "401011a6fd4ad5133c7d7688080a53c0ad22246e6b12b7f3bb9e92534a0639d3",
9
  "identity_note": "The historical source fields describe the base frontend; local packaged file hashes describe this candidate's derived frontend.",
10
+ "local_customization_id": "five-issue-ui-polish-v2-20261023",
11
+ "output_sha256": "8d33f953dc2decc0895841940b7d8019684c968fdda17309c2b2405486e8bf3f",
12
  "packaged_frontend_files": {
13
  "agent-manifest.json": "7187c85327d588cb3b81f5ddf60bae099567a656bcceac8147ecb2227e07f561",
14
  "agent-tools.js": "134b8c511a04671edf2129b7b4f42d2c0399713471feefaaeee542efab7127b8",
15
  "agents.html": "3e129500b30299ce628f04208dc7e77a2592991adb9c0d92042b63249077caa6",
16
  "assets/track-atlas.json": "d97c8900e3e5fb17734b5bd61d1f4f4b196d9be30af9848e2e273ae7e4079ae4",
17
+ "client.js": "401011a6fd4ad5133c7d7688080a53c0ad22246e6b12b7f3bb9e92534a0639d3",
18
  "help.html": "58959e994a8e4bbd3c70b2a6256cb699ac615cde718c6d0288fabeee909098a2",
19
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
20
+ "index.html": "8d33f953dc2decc0895841940b7d8019684c968fdda17309c2b2405486e8bf3f",
21
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
22
+ "track-map.js": "c5f56f1083f4c9487f8d3d663c939c736bdc478217e2b60142f820f3263adb44"
23
  },
24
+ "packaged_frontend_sha256": "ab985a034eddf2c9a04a7bf8e4d98d92168db81b254a7a9cb0e41d2cd4a9efdc",
25
  "packaged_frontend_sha256_format": "SHA-256 of UTF-8 JSON for packaged_frontend_files, sorted keys, compact separators, no trailing newline",
26
+ "status": "Compact run summary, truthful recording identity, paired map and transport, compact utilities and measured navigation offsets"
27
  }
RELEASE_MANIFEST.json CHANGED
@@ -7,7 +7,7 @@
7
  "CUDA_REFERENCE.md": "f7453c3254d244f8b0de41fde5851cb025159f5cea4a4d1a5d8208a955550c6c",
8
  "DATA_POLICY.md": "fc5978528abe1c21c14882417ac8a706e04a0831bbd3fdb3de8c64442c1a7b3d",
9
  "FEEDBACK.md": "f95b8f6afbf4a360976c0edca5d52f5b3a0b668c9a944d4947b840a7c53a122a",
10
- "FRONTEND_SOURCE.json": "7a2c0de24cd3f523a6d271d9435f7a4293ad6fa7344fe509f1e4891659d69ef3",
11
  "GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
12
  "IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
13
  "LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
@@ -63,13 +63,13 @@
63
  "builtin_tracks/tracks/Replica R08 Demolition Derby.track": "6f11a9ffbcb477c9d19964566b1b612d2a94675605246e93e21f27bbbdb357ef",
64
  "builtin_tracks/tracks/Replica R09 Eau Noir.track": "3ec0a78478cca1a4f0d2d98ac81f40783928adf271b70f0c32d295a6da2cdaa9",
65
  "builtin_tracks/tracks/Replica The Champions Trial.track": "a3bfb76422affc774b44c739b0636d08acb901fa7615b9adcb52c069f79834e1",
66
- "client.js": "cfb536dc3903beeafae8f1f883390423fd0a3b328575bb935184ee537022e656",
67
  "delivery.py": "6bdba2e87c3e19a41440cb16103695af27105f8bafbae38948bbb1b936ed0c1b",
68
  "gpu_tickets.py": "20928fcf1ca24648809ad8712c929377bdcc8b5e6b4387bfff3503ec5711928b",
69
  "help.html": "58959e994a8e4bbd3c70b2a6256cb699ac615cde718c6d0288fabeee909098a2",
70
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
71
  "hf_identity.py": "913fb1e089af21f376e8359a20cbc838d5bcd7971def6989a4382d53c1127b37",
72
- "index.html": "2ca75bb63f2fd64368c9d58bca51d724b6d177422e0961fb8abbecbf28d61b69",
73
  "native/analysis.patch": "42f79de67cb0734cffb2a7582f5479cab42ab82675884b8b42b9f158d46a98e8",
74
  "native/blackbox-build-receipt.json": "de3eea6c45a5ae4897287a97b0e2822fdae552a0655250bf940cef20d9b60d96",
75
  "native/blackbox-source.tar.gz": "56b43430c68b95bf7ac0d8312975c366c920f99b1653d790fb15b59453728e28",
@@ -91,7 +91,7 @@
91
  "star_check.py": "5c17080957b81ffccf0de8314635b471562e55c92c97a1207d9034a99279b2c3",
92
  "steam_leaderboard.py": "d76397c39cae58f636ddf9a9be1a46ae3d77fefd6b7269feac73d34d97165481",
93
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
94
- "track-map.js": "1612b75e95ccbbb2da4a47d8c8bf2ee406e8f40c626475ae2a1c3c19e119a139",
95
  "training_config.py": "6eb7282bbee3b3a6273bddc29c949d9904067414c26e501d815012f033173a1c",
96
  "vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
97
  "vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
 
7
  "CUDA_REFERENCE.md": "f7453c3254d244f8b0de41fde5851cb025159f5cea4a4d1a5d8208a955550c6c",
8
  "DATA_POLICY.md": "fc5978528abe1c21c14882417ac8a706e04a0831bbd3fdb3de8c64442c1a7b3d",
9
  "FEEDBACK.md": "f95b8f6afbf4a360976c0edca5d52f5b3a0b668c9a944d4947b840a7c53a122a",
10
+ "FRONTEND_SOURCE.json": "485c8c25299da7e0adf80e471eb8f19007a2015c07e65783a4d26ae46bc51085",
11
  "GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
12
  "IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
13
  "LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
 
63
  "builtin_tracks/tracks/Replica R08 Demolition Derby.track": "6f11a9ffbcb477c9d19964566b1b612d2a94675605246e93e21f27bbbdb357ef",
64
  "builtin_tracks/tracks/Replica R09 Eau Noir.track": "3ec0a78478cca1a4f0d2d98ac81f40783928adf271b70f0c32d295a6da2cdaa9",
65
  "builtin_tracks/tracks/Replica The Champions Trial.track": "a3bfb76422affc774b44c739b0636d08acb901fa7615b9adcb52c069f79834e1",
66
+ "client.js": "401011a6fd4ad5133c7d7688080a53c0ad22246e6b12b7f3bb9e92534a0639d3",
67
  "delivery.py": "6bdba2e87c3e19a41440cb16103695af27105f8bafbae38948bbb1b936ed0c1b",
68
  "gpu_tickets.py": "20928fcf1ca24648809ad8712c929377bdcc8b5e6b4387bfff3503ec5711928b",
69
  "help.html": "58959e994a8e4bbd3c70b2a6256cb699ac615cde718c6d0288fabeee909098a2",
70
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
71
  "hf_identity.py": "913fb1e089af21f376e8359a20cbc838d5bcd7971def6989a4382d53c1127b37",
72
+ "index.html": "8d33f953dc2decc0895841940b7d8019684c968fdda17309c2b2405486e8bf3f",
73
  "native/analysis.patch": "42f79de67cb0734cffb2a7582f5479cab42ab82675884b8b42b9f158d46a98e8",
74
  "native/blackbox-build-receipt.json": "de3eea6c45a5ae4897287a97b0e2822fdae552a0655250bf940cef20d9b60d96",
75
  "native/blackbox-source.tar.gz": "56b43430c68b95bf7ac0d8312975c366c920f99b1653d790fb15b59453728e28",
 
91
  "star_check.py": "5c17080957b81ffccf0de8314635b471562e55c92c97a1207d9034a99279b2c3",
92
  "steam_leaderboard.py": "d76397c39cae58f636ddf9a9be1a46ae3d77fefd6b7269feac73d34d97165481",
93
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
94
+ "track-map.js": "c5f56f1083f4c9487f8d3d663c939c736bdc478217e2b60142f820f3263adb44",
95
  "training_config.py": "6eb7282bbee3b3a6273bddc29c949d9904067414c26e501d815012f033173a1c",
96
  "vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
97
  "vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
client.js CHANGED
@@ -4,6 +4,10 @@ let job = null;
4
  let busy = false;
5
  let replayBusy = false;
6
  let localReplay = null;
 
 
 
 
7
  let replayRequestId = 0;
8
  let localResult = null;
9
  let localAnalysis = null;
@@ -133,7 +137,56 @@ function buttons() {
133
  if($('bucket_recipe_import'))$('bucket_recipe_import').disabled=unavailable||!bucketToken||!$('bucket_recipe_items')?.value;
134
  if($('bucket_autosave'))$('bucket_autosave').disabled=unavailable||!bucketToken;
135
  syncRunModeUI(settingsUnavailable);
 
 
136
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  const trainingOnlyControls=['population','generations','mutation','mutation_end','schedule','adaptive_mutation','selection','parents','elite','crossover','decay','preset','reward_preset','add','rewards'];
138
  function syncRunModeUI(unavailable=busy||bundleRestoring||bucketOperation){
139
  const evaluation=$('run_mode')?.value==='evaluate';
@@ -469,7 +522,8 @@ const networkSource=()=>stagedNetwork || $('network').files[0];
469
  function replayNetworkSource(){return networkSource()||localResult;}
470
  function invalidateReplay(reason){
471
  replayRequestId++;
472
- localReplay=null;
 
473
  window.TrackReplay?.clearRecording?.(reason);
474
  if($('replay_download'))$('replay_download').disabled=true;
475
  }
@@ -508,7 +562,7 @@ if($('replay_record'))$('replay_record').onclick=async()=>{
508
  if(!window.TrackReplay?.setRecording)throw Error('The recorded-drive viewer is unavailable. Reload this page.');
509
  await window.TrackReplay.setRecording(recording,trackSha,networkSha);
510
  if(attempt!==replayRequestId){window.TrackReplay.clearRecording();throw Error('Selection changed during recording. Record the current selection again.');}
511
- localReplay=recording;
512
  }catch(error){
513
  if(attempt===replayRequestId&&$('replay_status'))$('replay_status').textContent=
514
  error?.name==='AbortError'?'Recording timed out. Try a 5-second drive.':'Recording unavailable: '+error.message;
@@ -517,6 +571,13 @@ if($('replay_record'))$('replay_record').onclick=async()=>{
517
  }
518
  };
519
  if($('replay_focus'))$('replay_focus').onclick=()=>{const focused=$('drive').classList.toggle('replay-focused');$('replay_focus').textContent=focused?'Return to run setup':'Expand replay workspace';$('replay_focus').setAttribute('aria-pressed',String(focused));};
 
 
 
 
 
 
 
520
  if($('replay_import'))$('replay_import').onchange=async()=>{
521
  const file=$('replay_import').files[0];$('replay_import').value='';
522
  if(!file||busy||job||batchActive||bucketOperation||bundleRestoring)return;
@@ -527,7 +588,7 @@ if($('replay_import'))$('replay_import').onchange=async()=>{
527
  const [trackSha,networkSha]=await Promise.all([track.arrayBuffer().then(sha),network.arrayBuffer().then(sha)]);
528
  const recording=JSON.parse(await file.text());
529
  if(attempt!==replayRequestId)throw Error('Selection changed while opening recording.');
530
- window.TrackReplay.setRecording(recording,trackSha,networkSha);localReplay=recording;
531
  }catch(error){if(attempt===replayRequestId)window.TrackReplay?.clearRecording?.('Recording unavailable: '+error.message);}
532
  finally{busy=false;replayBusy=false;buttons();}
533
  };
@@ -665,7 +726,9 @@ function retainVerifiedPrimary(error){
665
  }
666
  async function acknowledge(current,received){
667
  const body={receipt:received.receipt};if(received.analysisReceipt)body.analysis_receipt=received.analysisReceipt;
668
- return (await request('/api/jobs/'+current.id+'/ack',{method:'POST',headers:{'X-Job-Key':current.key,'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
 
 
669
  }
670
  async function sha(bytes){return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),x=>x.toString(16).padStart(2,'0')).join('');}
671
  const ANALYSIS_ARCHIVE_MAX=32*1024*1024,ANALYSIS_EXPANDED_MAX=256*1024*1024,ANALYSIS_ROWS_MAX=128000;
@@ -832,7 +895,7 @@ function showDescriptor(){
832
  if(!Number.isInteger(generation)||!Number.isInteger(car)){ $('descriptor_status').textContent='Enter whole generation and car numbers.';return; }
833
  const row=localAnalysis.descriptorRows.find(item=>item.generation===generation&&item.car===car);
834
  if(!row){$('descriptor_status').textContent='No descriptor was recorded for that generation and car.';$('descriptor_tables').replaceChildren();return;}
835
- window.TrackReplay?.focusCandidate?.('swarm');
836
  const wrap=document.createElement('div');
837
  const summary=document.createElement('p');summary.textContent='Candidate '+row.candidate_id+' · ticks '+row.ticks_elapsed+' · stats updates '+row.stats_update_count+' · weights '+row.weights_sha256;wrap.append(summary);
838
  const table=(title,entries)=>{const h=document.createElement('h3');h.textContent=title;wrap.append(h);const t=document.createElement('table'),body=document.createElement('tbody');for(const [name,value] of entries){const tr=document.createElement('tr'),a=document.createElement('th'),b=document.createElement('td');a.textContent=name;b.textContent=value===null?'Not observed':typeof value==='number'?String(value):String(value);tr.append(a,b);body.append(tr);}t.append(body);wrap.append(t);};
@@ -1050,7 +1113,8 @@ $('run').onclick = async () => {
1050
  const started=performance.now();
1051
  $('runState').textContent='RUNNING — waiting for server confirmation';
1052
  $('runState').style.color='#ffd073';
1053
- runReceipt=null;pairedAnalysisPending=false;
 
1054
  runResultContext=null;
1055
  showDetails(null);
1056
  const tick=()=>{$('runSummary').textContent='Backend: '+$('backend').value.toUpperCase()+'\nElapsed: '+((performance.now()-started)/1000).toFixed(1)+' s\nCompletion is not yet confirmed.';};
@@ -1074,7 +1138,7 @@ $('run').onclick = async () => {
1074
  const body={track:await encoded(trackFile),network:await encoded(networkFile),automatic:autosaveSnapshot?true:!$('keepServer').checked,backend:$('backend').value};
1075
  if(window.getTrainingSettings){body.settings={...settings};body.settings.mode=$('run_mode')?.value==='evaluate'?'evaluate':'train';body.settings.clean_min_laps=cleanMinLaps;
1076
  if(body.settings.mode==='evaluate')Object.assign(body.settings,{population:1,generations:1,mutation:0,mutation_end:0,schedule:'fixed',selection:'best',parents:1,elite:0,crossover:'none',decay:0,adaptive_mutation:false});}
1077
- job=await submit(body);localResult=null;localAnalysis=null;pairedAnalysisPending=false;selectedWinner=null;renderAnalysisPanel();
1078
  clearInterval(progress);
1079
  $('runState').textContent='COMPLETE — candidate ready to download';
1080
  $('runState').style.color='#62dfb1';
@@ -1100,7 +1164,7 @@ $('run').onclick = async () => {
1100
  if($('leaderboard_opt_in')?.checked)void checkSteamLeaderboard(trackInfo,r);
1101
  else if($('leaderboard_status'))$('leaderboard_status').textContent='';
1102
  if(!comparisonPlan)$('track').value=''; $('network').value='';
1103
- stagedNetwork=null;window.setFreezeNetwork?.(null);if($('networkChoice'))$('networkChoice').textContent='Network input released after the run.';
1104
  say('Result ready ('+job.backend.toUpperCase()+'). Inputs and intermediate worker files have been removed.');
1105
  if(autosaveSnapshot){
1106
  try{
@@ -1182,7 +1246,7 @@ $('run').onclick = async () => {
1182
  : 'No downloadable result was received. A connection error does not prove the server stopped.');
1183
  say(displayError(error));
1184
  }
1185
- finally {clearInterval(progress);busy=false;$('keepServer').checked=false;buttons();if(comparisonPlan)launchNextComparison();}
1186
  };
1187
  $('download').onclick = async () => {
1188
  busy=true; buttons();
@@ -1203,7 +1267,7 @@ $('download').onclick = async () => {
1203
  $('erase').onclick=async()=>{
1204
  if(!await window.appConfirm('Delete remaining server-side result and revoke download access? Local files are unaffected.',{heading:'Delete server result',confirmLabel:'Delete result',danger:true}))return;
1205
  busy=true; buttons();
1206
- try {await request('/api/jobs/'+job.id,{method:'DELETE',headers:headers()}); job=null; say('Server result deleted. Local files are unaffected.');}
1207
  catch(error){say(error.message);}
1208
  finally {busy=false; buttons();}
1209
  };
@@ -1218,12 +1282,14 @@ if($('batch_start')){
1218
  let plan,failedCandidateStep=null;const history=[];const started=Date.now();
1219
  try{
1220
  plan=window.getBatchPlan();
 
1221
  busy=true;buttons(); // Freeze both selectors before an asynchronous track fetch.
1222
  const trackFile=await trackSource();
1223
  if(!trackFile)throw Error('Choose a track in Run & results before running a batch.');
1224
  const trackBytes=new Uint8Array(await trackFile.arrayBuffer()),track=await encoded(trackFile),trackInfo=trackLibrary?.info()??{source:'upload'},original=networkSource();
1225
  await encoded(original); // Reject missing/oversize inputs before dispatch.
1226
  const backend=$('backend').value,autosaveSnapshot=bucketToken&&$('bucket_autosave')?.checked?bucketSnapshot():null;
 
1227
  busy=true;batchActive=true;batchStopRequested=false;batchReceipt=null;buttons();
1228
  localResult=null;localAnalysis=null;pairedAnalysisPending=false;
1229
  $('batch_history').replaceChildren();
@@ -1233,8 +1299,10 @@ if($('batch_start')){
1233
  showDetails(null);
1234
  const result=await window.BatchTraining.execute(plan,original,async(source,settings,i)=>{
1235
  $('batch_status').textContent='RUNNING · '+plan.name+' · step '+(i+1)+' / '+plan.runs.length+' · '+plan.runs[i].slot+' · '+plan.runs[i].name;
 
1236
  const inputHash=await sha(await source.arrayBuffer());
1237
  job=await submit({track,network:await encoded(source),settings,backend,automatic:true});
 
1238
  const stepJob=job,report=stepJob.report,stepRunReceipt={version:1,state:'completed',build:report?.build??appBuild,backend,output_sha256:stepJob.sha256,report};
1239
  let received;
1240
  try{received=await receive(job);}catch(error){if(retainVerifiedPrimary(error)){runReceipt=stepRunReceipt;runResultContext={trackInfo,trackBytes,inputNetworkSha256:inputHash,autosaveSnapshot};}throw error;}
@@ -1295,7 +1363,7 @@ setInterval(()=>{
1295
  if(!job){$('deadline').textContent='';return;}
1296
  const remaining=Math.max(0,Math.ceil(job.expires_at-Date.now()/1000));
1297
  $('deadline').textContent='Automatic expiry: '+new Date(job.expires_at*1000).toLocaleTimeString()+' · '+Math.floor(remaining/60)+'m '+remaining%60+'s remaining';
1298
- if(!remaining && !busy){job=null; buttons(); say('Result access has expired. Server cleanup runs independently of this browser.');}
1299
  },1000);
1300
  buttons();
1301
  async function connect(){
@@ -1588,7 +1656,7 @@ async function applyPreparedPortable(prepared,{isCurrent=null}={}){
1588
  }
1589
  window.TrainingClient.useNetwork(networkFile);
1590
  if(parsed.recipe)window.stagePortableRecipe(parsed.recipe);
1591
- portableReceipt=parsed.receipt||null;
1592
  localAnalysis=parsed.analysis||null;selectedWinner=null;renderAnalysisPanel();
1593
  $('bundle_receipt_download').disabled=!portableReceipt;
1594
  bundleMessage('Bundle restored in this page only. Network and optional track/recipe are staged; no run started and no browser storage or server was changed.'+(portableReceipt?' Sanitized receipt is available for separate download.':''));
 
4
  let busy = false;
5
  let replayBusy = false;
6
  let localReplay = null;
7
+ let localReplayName = null;
8
+ let localReplayDriver = null;
9
+ let serverCopyStatus = 'no server result has been published.';
10
+ let runDisplayContext = null;
11
  let replayRequestId = 0;
12
  let localResult = null;
13
  let localAnalysis = null;
 
137
  if($('bucket_recipe_import'))$('bucket_recipe_import').disabled=unavailable||!bucketToken||!$('bucket_recipe_items')?.value;
138
  if($('bucket_autosave'))$('bucket_autosave').disabled=unavailable||!bucketToken;
139
  syncRunModeUI(settingsUnavailable);
140
+ renderResultSummary();
141
+ renderRecordingIdentity();
142
  }
143
+ // Presentation only: these summaries reuse the verified run/replay state and existing controls.
144
+ function selectedReplayDriverLabel(){
145
+ const selected=networkSource();
146
+ if(selected)return selected.name||'Selected driver';
147
+ if(localResult){
148
+ const winner=localAnalysis?.summary?.winners?.fitness;
149
+ return winner?.status==='available'?'Final fitness leader · '+winner.candidate_id:'Latest received result';
150
+ }
151
+ return null;
152
+ }
153
+ function renderResultSummary(){
154
+ const panel=$('resultSummary');if(!panel)return;
155
+ const running=$('runState').textContent.startsWith('RUNNING')||$('runState').textContent.startsWith('BATCH RUNNING');
156
+ panel.hidden=!runDisplayContext&&!runReceipt&&!portableReceipt&&!localResult&&!job&&!running;
157
+ const receipt=runReceipt||portableReceipt,report=runDisplayContext?runDisplayContext.report:receipt?.report;
158
+ const mode=report?.mode??report?.settings?.mode??report?.analysis_summary?.mode??runDisplayContext?.mode;
159
+ $('result_backend').textContent=(runDisplayContext?.backend||receipt?.backend||job?.backend||$('backend').value).toUpperCase()+(running?' · running':'');
160
+ $('result_outcome').textContent=running?'Completion is not yet confirmed.':report
161
+ ?((mode==='evaluate'?'Frozen evaluation':'Training')+' · '+(report.generations_completed??'—')+' / '+(report.generations_requested??'—')+' generations · '+(report.best_lap_seconds==null?'No completed lap observed':Number(report.best_lap_seconds).toFixed(3)+' s best lap observed')+'. A completed run does not establish improved driving.')
162
+ :'No verified run measurements are available.';
163
+ const driver=selectedReplayDriverLabel();
164
+ $('result_driver').textContent=networkSource()?'Selected for replay / next run: '+driver+'.':localResult?'Received driver for replay: '+driver+'. To use it for another run or batch, select a winner below or upload the downloaded driver.':'No driver selected for replay or the next run.';
165
+ const serverState=running?'waiting for a published result.':!runDisplayContext&&portableReceipt&&!runReceipt?'not tracked for this restored bundle.':serverCopyStatus;
166
+ $('result_server').textContent='Server copy: '+serverState+(localResult&&!running?' Verified browser driver remains available.':'');
167
+ $('result_replay').disabled=busy||replayBusy||!!job||!driver;
168
+ $('download').hidden=!job;
169
+ $('saveLocal').hidden=!!job&&!localResult;
170
+ $('saveLocal').textContent='Download received driver';
171
+ if($('setup_next_step'))$('setup_next_step').textContent=running?'Run in progress. Its result will appear above when confirmed.':networkSource()
172
+ ?'Selected driver is ready. Review Training & recipes, then start the next run.'
173
+ :localResult?'Your received driver is available in Latest run above. Download it, replay it, or select a winner below to continue.'
174
+ :job?'Receive the completed driver using Download driver in Latest run above. Its weights can then be replayed or inspected.'
175
+ :'Choose or create a driver, then review Training & recipes and start a run.';
176
+ if($('starter_network_status'))$('starter_network_status').textContent=networkSource()?'Selected driver: '+(networkSource().name||'Browser driver')+'. A new starter replaces this selection without starting a run.':localResult?'The latest trained or evaluated driver is received. Use Latest run above to download or replay it. Create a starter only to begin a new experiment.':job?'The run has finished. Receive the driver using Latest run above, or delete the server result before starting another experiment.':'Creates a small network in this page only. It does not start a run or change saved recipes.';
177
+ }
178
+ function renderRecordingIdentity(){
179
+ const label=$('replay_loaded');if(!label)return;
180
+ label.hidden=!localReplay;
181
+ label.textContent=localReplay?'Recording loaded: '+localReplayName+' · '+(localReplay.requested_ticks/60).toFixed(2)+' seconds · '+localReplay.frames.length+' measured samples · Matching driver: '+localReplayDriver+'. Fresh CPU evaluation.':'';
182
+ }
183
+ function openReplayWorkspace({focusRecord=false}={}){
184
+ $('drive').classList.add('replay-focused');
185
+ $('replay_focus').textContent='Return to run setup';$('replay_focus').setAttribute('aria-pressed','true');
186
+ const target=focusRecord?$('replayControls'):$('replay_workspace');
187
+ target.tabIndex=-1;target.scrollIntoView({block:'start'});target.focus({preventScroll:true});
188
+ }
189
+ if($('result_replay'))$('result_replay').onclick=()=>openReplayWorkspace({focusRecord:true});
190
  const trainingOnlyControls=['population','generations','mutation','mutation_end','schedule','adaptive_mutation','selection','parents','elite','crossover','decay','preset','reward_preset','add','rewards'];
191
  function syncRunModeUI(unavailable=busy||bundleRestoring||bucketOperation){
192
  const evaluation=$('run_mode')?.value==='evaluate';
 
522
  function replayNetworkSource(){return networkSource()||localResult;}
523
  function invalidateReplay(reason){
524
  replayRequestId++;
525
+ localReplay=null;localReplayName=null;localReplayDriver=null;
526
+ renderRecordingIdentity();
527
  window.TrackReplay?.clearRecording?.(reason);
528
  if($('replay_download'))$('replay_download').disabled=true;
529
  }
 
562
  if(!window.TrackReplay?.setRecording)throw Error('The recorded-drive viewer is unavailable. Reload this page.');
563
  await window.TrackReplay.setRecording(recording,trackSha,networkSha);
564
  if(attempt!==replayRequestId){window.TrackReplay.clearRecording();throw Error('Selection changed during recording. Record the current selection again.');}
565
+ localReplay=recording;localReplayName='ald-recorded-drive.json';localReplayDriver=selectedReplayDriverLabel();renderRecordingIdentity();openReplayWorkspace();
566
  }catch(error){
567
  if(attempt===replayRequestId&&$('replay_status'))$('replay_status').textContent=
568
  error?.name==='AbortError'?'Recording timed out. Try a 5-second drive.':'Recording unavailable: '+error.message;
 
571
  }
572
  };
573
  if($('replay_focus'))$('replay_focus').onclick=()=>{const focused=$('drive').classList.toggle('replay-focused');$('replay_focus').textContent=focused?'Return to run setup':'Expand replay workspace';$('replay_focus').setAttribute('aria-pressed',String(focused));};
574
+ // Returning from the black box keeps the map in view as well as keyboard focus on playback.
575
+ document.querySelectorAll('a[href="#replay_transport"]').forEach(link=>link.addEventListener('click',event=>{
576
+ event.preventDefault();
577
+ const viewer=$('trackMap').closest('.replay-viewer');
578
+ viewer.scrollIntoView({block:'start'});
579
+ $('replay_transport').focus({preventScroll:true});
580
+ }));
581
  if($('replay_import'))$('replay_import').onchange=async()=>{
582
  const file=$('replay_import').files[0];$('replay_import').value='';
583
  if(!file||busy||job||batchActive||bucketOperation||bundleRestoring)return;
 
588
  const [trackSha,networkSha]=await Promise.all([track.arrayBuffer().then(sha),network.arrayBuffer().then(sha)]);
589
  const recording=JSON.parse(await file.text());
590
  if(attempt!==replayRequestId)throw Error('Selection changed while opening recording.');
591
+ window.TrackReplay.setRecording(recording,trackSha,networkSha);localReplay=recording;localReplayName=file.name;localReplayDriver=selectedReplayDriverLabel();renderRecordingIdentity();openReplayWorkspace();
592
  }catch(error){if(attempt===replayRequestId)window.TrackReplay?.clearRecording?.('Recording unavailable: '+error.message);}
593
  finally{busy=false;replayBusy=false;buttons();}
594
  };
 
726
  }
727
  async function acknowledge(current,received){
728
  const body={receipt:received.receipt};if(received.analysisReceipt)body.analysis_receipt=received.analysisReceipt;
729
+ const acknowledgment=await (await request('/api/jobs/'+current.id+'/ack',{method:'POST',headers:{'X-Job-Key':current.key,'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
730
+ serverCopyStatus=acknowledgment.deleted===true?'deleted after verified receipt.':'retained for repeat download until its five-minute deadline. Use Delete server-side job data now to remove it sooner.';
731
+ return acknowledgment;
732
  }
733
  async function sha(bytes){return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),x=>x.toString(16).padStart(2,'0')).join('');}
734
  const ANALYSIS_ARCHIVE_MAX=32*1024*1024,ANALYSIS_EXPANDED_MAX=256*1024*1024,ANALYSIS_ROWS_MAX=128000;
 
895
  if(!Number.isInteger(generation)||!Number.isInteger(car)){ $('descriptor_status').textContent='Enter whole generation and car numbers.';return; }
896
  const row=localAnalysis.descriptorRows.find(item=>item.generation===generation&&item.car===car);
897
  if(!row){$('descriptor_status').textContent='No descriptor was recorded for that generation and car.';$('descriptor_tables').replaceChildren();return;}
898
+ invalidateReplay('Descriptor selected. Record the selected policy again to inspect a drive.');
899
  const wrap=document.createElement('div');
900
  const summary=document.createElement('p');summary.textContent='Candidate '+row.candidate_id+' · ticks '+row.ticks_elapsed+' · stats updates '+row.stats_update_count+' · weights '+row.weights_sha256;wrap.append(summary);
901
  const table=(title,entries)=>{const h=document.createElement('h3');h.textContent=title;wrap.append(h);const t=document.createElement('table'),body=document.createElement('tbody');for(const [name,value] of entries){const tr=document.createElement('tr'),a=document.createElement('th'),b=document.createElement('td');a.textContent=name;b.textContent=value===null?'Not observed':typeof value==='number'?String(value):String(value);tr.append(a,b);body.append(tr);}t.append(body);wrap.append(t);};
 
1113
  const started=performance.now();
1114
  $('runState').textContent='RUNNING — waiting for server confirmation';
1115
  $('runState').style.color='#ffd073';
1116
+ runReceipt=null;pairedAnalysisPending=false;serverCopyStatus='no new result is confirmed.';
1117
+ runDisplayContext={backend:$('backend').value,mode:$('run_mode')?.value??'train',report:null};
1118
  runResultContext=null;
1119
  showDetails(null);
1120
  const tick=()=>{$('runSummary').textContent='Backend: '+$('backend').value.toUpperCase()+'\nElapsed: '+((performance.now()-started)/1000).toFixed(1)+' s\nCompletion is not yet confirmed.';};
 
1138
  const body={track:await encoded(trackFile),network:await encoded(networkFile),automatic:autosaveSnapshot?true:!$('keepServer').checked,backend:$('backend').value};
1139
  if(window.getTrainingSettings){body.settings={...settings};body.settings.mode=$('run_mode')?.value==='evaluate'?'evaluate':'train';body.settings.clean_min_laps=cleanMinLaps;
1140
  if(body.settings.mode==='evaluate')Object.assign(body.settings,{population:1,generations:1,mutation:0,mutation_end:0,schedule:'fixed',selection:'best',parents:1,elite:0,crossover:'none',decay:0,adaptive_mutation:false});}
1141
+ job=await submit(body);runDisplayContext={backend:job.backend,mode:body.settings?.mode??'train',report:job.report};serverCopyStatus='available until verified receipt, deletion or expiry. Receive and download the driver below.';localResult=null;localAnalysis=null;pairedAnalysisPending=false;selectedWinner=null;renderAnalysisPanel();
1142
  clearInterval(progress);
1143
  $('runState').textContent='COMPLETE — candidate ready to download';
1144
  $('runState').style.color='#62dfb1';
 
1164
  if($('leaderboard_opt_in')?.checked)void checkSteamLeaderboard(trackInfo,r);
1165
  else if($('leaderboard_status'))$('leaderboard_status').textContent='';
1166
  if(!comparisonPlan)$('track').value=''; $('network').value='';
1167
+ stagedNetwork=null;window.setFreezeNetwork?.(null);if($('networkChoice'))$('networkChoice').textContent='Run input released. The latest result and its download controls are above.';
1168
  say('Result ready ('+job.backend.toUpperCase()+'). Inputs and intermediate worker files have been removed.');
1169
  if(autosaveSnapshot){
1170
  try{
 
1246
  : 'No downloadable result was received. A connection error does not prove the server stopped.');
1247
  say(displayError(error));
1248
  }
1249
+ finally {clearInterval(progress);busy=false;$('keepServer').checked=false;buttons();if(comparisonPlan)launchNextComparison();else if(!batchActive&&!$('resultSummary').hidden){const heading=$('result_summary_title');heading.scrollIntoView({block:'start'});heading.focus({preventScroll:true});}}
1250
  };
1251
  $('download').onclick = async () => {
1252
  busy=true; buttons();
 
1267
  $('erase').onclick=async()=>{
1268
  if(!await window.appConfirm('Delete remaining server-side result and revoke download access? Local files are unaffected.',{heading:'Delete server result',confirmLabel:'Delete result',danger:true}))return;
1269
  busy=true; buttons();
1270
+ try {await request('/api/jobs/'+job.id,{method:'DELETE',headers:headers()}); job=null; serverCopyStatus='deleted by request. Local files are unaffected.';say('Server result deleted. Local files are unaffected.');}
1271
  catch(error){say(error.message);}
1272
  finally {busy=false; buttons();}
1273
  };
 
1282
  let plan,failedCandidateStep=null;const history=[];const started=Date.now();
1283
  try{
1284
  plan=window.getBatchPlan();
1285
+ runDisplayContext={backend:$('backend').value,mode:'train',report:null};serverCopyStatus='no new result is confirmed.';
1286
  busy=true;buttons(); // Freeze both selectors before an asynchronous track fetch.
1287
  const trackFile=await trackSource();
1288
  if(!trackFile)throw Error('Choose a track in Run & results before running a batch.');
1289
  const trackBytes=new Uint8Array(await trackFile.arrayBuffer()),track=await encoded(trackFile),trackInfo=trackLibrary?.info()??{source:'upload'},original=networkSource();
1290
  await encoded(original); // Reject missing/oversize inputs before dispatch.
1291
  const backend=$('backend').value,autosaveSnapshot=bucketToken&&$('bucket_autosave')?.checked?bucketSnapshot():null;
1292
+ runDisplayContext={backend,mode:'train',report:null};serverCopyStatus='no new result is confirmed.';
1293
  busy=true;batchActive=true;batchStopRequested=false;batchReceipt=null;buttons();
1294
  localResult=null;localAnalysis=null;pairedAnalysisPending=false;
1295
  $('batch_history').replaceChildren();
 
1299
  showDetails(null);
1300
  const result=await window.BatchTraining.execute(plan,original,async(source,settings,i)=>{
1301
  $('batch_status').textContent='RUNNING · '+plan.name+' · step '+(i+1)+' / '+plan.runs.length+' · '+plan.runs[i].slot+' · '+plan.runs[i].name;
1302
+ runDisplayContext={backend,mode:settings.mode??'train',report:null};serverCopyStatus='no new result is confirmed.';
1303
  const inputHash=await sha(await source.arrayBuffer());
1304
  job=await submit({track,network:await encoded(source),settings,backend,automatic:true});
1305
+ runDisplayContext={backend:job.backend,mode:settings.mode??'train',report:job.report};serverCopyStatus='available until verified receipt, deletion or expiry. Receive and download the driver below.';
1306
  const stepJob=job,report=stepJob.report,stepRunReceipt={version:1,state:'completed',build:report?.build??appBuild,backend,output_sha256:stepJob.sha256,report};
1307
  let received;
1308
  try{received=await receive(job);}catch(error){if(retainVerifiedPrimary(error)){runReceipt=stepRunReceipt;runResultContext={trackInfo,trackBytes,inputNetworkSha256:inputHash,autosaveSnapshot};}throw error;}
 
1363
  if(!job){$('deadline').textContent='';return;}
1364
  const remaining=Math.max(0,Math.ceil(job.expires_at-Date.now()/1000));
1365
  $('deadline').textContent='Automatic expiry: '+new Date(job.expires_at*1000).toLocaleTimeString()+' · '+Math.floor(remaining/60)+'m '+remaining%60+'s remaining';
1366
+ if(!remaining && !busy){job=null; serverCopyStatus='access expired. Server cleanup runs independently of this browser.';buttons(); say('Result access has expired. Server cleanup runs independently of this browser.');}
1367
  },1000);
1368
  buttons();
1369
  async function connect(){
 
1656
  }
1657
  window.TrainingClient.useNetwork(networkFile);
1658
  if(parsed.recipe)window.stagePortableRecipe(parsed.recipe);
1659
+ portableReceipt=parsed.receipt||null;runDisplayContext=null;
1660
  localAnalysis=parsed.analysis||null;selectedWinner=null;renderAnalysisPanel();
1661
  $('bundle_receipt_download').disabled=!portableReceipt;
1662
  bundleMessage('Bundle restored in this page only. Network and optional track/recipe are staged; no run started and no browser storage or server was changed.'+(portableReceipt?' Sanitized receipt is available for separate download.':''));
index.html CHANGED
@@ -8,6 +8,13 @@
8
  .replay-bar{background:#101d2c;border:1px solid #2a3d52;border-radius:11px;padding:10px 12px;margin:10px 0}.replay-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.replay-row button{min-height:36px;padding:6px 11px;font-size:13px;font-weight:650}.replay-row select{width:auto;min-height:36px;padding:5px 9px;font-size:13px}.replay-row input[type=range]{flex:1;min-width:120px;accent-color:#62dfb1;padding:0}.replay-time{font:600 12px ui-monospace,SFMono-Regular,monospace;color:#91efc6;white-space:nowrap}.replay-layers{display:flex;gap:6px 12px;flex-wrap:wrap;margin-top:8px;padding-top:8px;border-top:1px solid #223447;font-size:12.5px;color:#b9cddd}.replay-layers label{display:inline-flex;align-items:center;gap:5px;margin:0;cursor:pointer}.replay-layers input{margin:0}.kpi-strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:12px 0}.kpi-tile{background:#101d2c;border:1px solid #2a3d52;border-radius:10px;padding:10px 12px}.kpi-tile .kpi-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;color:#a2b4c8}.kpi-tile .kpi-val{font:800 19px ui-monospace,SFMono-Regular,monospace;color:#62dfb1;margin-top:3px}.kpi-tile .kpi-sub{font-size:11.5px;color:#8fa6bc;margin-top:2px}.chart-box{background:#09111b;border:1px solid #2a3d52;border-radius:10px;padding:10px;margin:10px 0}.chart-box canvas{max-height:190px;display:block}.subtab-bar{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.subtab-bar button{min-height:36px;padding:6px 12px;font-size:13px;border-radius:18px}.subtab-bar button[aria-pressed=true]{background:#62dfb1;color:#102522;font-weight:700}.editor-visual-grid{display:grid;grid-template-columns:minmax(260px,.85fr) minmax(0,1.35fr);gap:16px;align-items:start;margin:14px 0}@media(max-width:850px){.kpi-strip{grid-template-columns:1fr 1fr}.editor-visual-grid{grid-template-columns:minmax(0,1fr)}}
9
 
10
  .replay-workbench{display:grid;gap:14px;align-items:start}#replayControls,#replay_transport,#replay_blackbox{scroll-margin-top:160px}#replayControls{min-width:0}.replay-row>*{max-width:100%}#replay_blackbox{padding:12px;border:1px solid #2a3d52;border-radius:10px;min-width:0}#replay_blackbox h3{margin-top:0}#replay_blackbox td,#replay_blackbox th{padding:6px;font-size:12px}#drive.replay-focused>.grid{grid-template-columns:minmax(0,1fr)}#drive.replay-focused>.grid>section:first-child{display:none}#drive.replay-focused .replay-workbench{grid-template-columns:minmax(0,1.35fr) minmax(320px,1fr)}#drive.replay-focused .map{position:sticky;top:70px}#drive.replay-focused .map canvas{max-height:500px}@media(max-width:850px){#drive.replay-focused .replay-workbench{grid-template-columns:minmax(0,1fr)}#drive.replay-focused .map{position:static}}
 
 
 
 
 
 
 
11
  </style>
12
  <dialog id="appConfirm" aria-labelledby="appConfirmTitle" aria-describedby="appConfirmMessage">
13
  <h2 id="appConfirmTitle">Confirm action</h2>
@@ -36,9 +43,9 @@
36
  })();
37
  </script>
38
  <header class="lab-header"><div><div class="eyebrow">AI LEARNS TO DRIVE</div><h1>Training lab</h1><p class="intro">Build a driver, train it, inspect the result.</p></div><div class="header-actions"><button id="quick_start_toggle" type="button" aria-controls="firstRunGuide" aria-expanded="false">Quick start</button><a class="help-link" href="/help#start-here" target="_blank" rel="noopener">Help &amp; tutorial ↗</a></div></header>
39
- <details class="about"><summary>About &amp; feedback</summary><p>Simulator by <a href="https://github.com/Zorro909/driving_sim_rs">Zorro909</a>; interface and hosting by <a href="https://github.com/seeker-cyber-maker/driving_sim_rs">TiGa</a>. Inspired by the <a href="https://store.steampowered.com/app/3312030/AI_Learns_To_Drive/">game by ActivationFunction</a>. Built-in tracks are reconstructed replicas. For supported conditions, the lab checks the run’s final fitness policy (or frozen input) with a bounded fresh replay. It predicts conditions only and does not award or accumulate original-game stars; opponent races and other unsupported conditions remain unchecked.</p><p>Uploaded files are used for the requested run. Results are held briefly in application memory and deleted after verified download or expiry. Browser recipes contain settings only. <a href="/policy" target="_blank" rel="noopener">Read the data policy</a>.</p><p>The map follows the selected track’s native centerline. Record a short CPU drive to watch an unchanged policy using real vehicle positions. A new recording can differ from an earlier training run. Steering commands, sensor rays and neural activity are not included in the current recording.</p><p><a id="feedbackLink" href="https://github.com/seeker-cyber-maker/driving_sim_rs/issues/new" target="_blank" rel="noopener">Report a problem</a> · Opens an editable draft for you to review.</p><p id="buildLabel" class="hint">Build: connecting…</p></details>
40
- <details id="firstRunGuide" class="quick-start"><summary id="firstRunTitle">New here? Start a first run <span class="hint">Four steps, whenever you need them</span></summary><ol class="quick-steps"><li><strong>Choose a track</strong><p>Pick a built-in replica or upload your own track.</p><button id="first_run_track" type="button">Choose a track</button></li><li><strong>Create a driver</strong><p>Choose a car and create a small starter network, or upload a network you already have.</p><button type="button" data-open-tab="drive" data-focus="starter_create">Set up a network</button></li><li><strong>Review the settings</strong><p>The starter settings are ready for a short first experiment. You can save them as a recipe later.</p><button id="first_run_settings" type="button">Review first-run settings</button></li><li><strong>Run and inspect</strong><p>Start training, then download the result to inspect your drivers. A first run may not finish a lap.</p><button type="button" data-open-tab="drive" data-focus="run">Go to run controls</button></li></ol><div class="quick-footer"><a href="/help#start-here" target="_blank" rel="noopener">Read the full step-by-step tutorial ↗</a><button id="quick_start_close" type="button">Close quick start</button></div></details>
41
- <details id="agent-interface" class="card"><summary>Agent tools &amp; interface status</summary><p>Use the same local controls as the lab: read the current state, open a section, create a starter, or inspect the active driver. These tools do not submit jobs or save data. <a href="/agents" target="_blank" rel="noopener">Agent interface guide</a> · <a href="/agent-manifest.json" target="_blank" rel="noopener">Action manifest (JSON)</a></p><p id="agent_tools_status" role="status" aria-live="polite">Checking browser tool support…</p><p>The state below is also available to ordinary browser automation. Each listed action identifies a stable control in this page.</p><pre id="agent_state" aria-label="Current lab state (JSON)"></pre></details><nav class="tabs" role="tablist" aria-label="Lab sections"><button type="button" id="tab-drive" role="tab" data-tab="drive" aria-controls="drive" aria-selected="true" tabindex="0">Run &amp; results</button><button type="button" id="tab-training" role="tab" data-tab="training" aria-controls="training" aria-selected="false" tabindex="-1">Training &amp; recipes</button><button type="button" id="tab-batch" role="tab" data-tab="batch" aria-controls="batch" aria-selected="false" tabindex="-1">Batches</button><button type="button" id="tab-editor" role="tab" data-tab="editor" aria-controls="editor" aria-selected="false" tabindex="-1">Network editor</button></nav><div id="drive" class="pane" role="tabpanel" aria-labelledby="tab-drive"><div class="grid"><section class="card"><h2>Simulation</h2><div class="toolbar" aria-label="Create a starter network"><label for="starter_vehicle">Starter car</label><select id="starter_vehicle"><option value="0" selected>Formula</option><option value="1">Rally</option><option value="2">Snowmobile</option><option value="3">Truck</option></select><button id="starter_create" type="button">Create starter network</button></div><p id="starter_network_status" class="hint" role="status">Creates a small network in this page only. It does not start a run or change saved recipes.</p><label for="run_mode">Action</label><select id="run_mode"><option value="train" selected>Train a population</option><option value="evaluate">Frozen evaluation · one policy, unchanged weights</option></select><p id="run_mode_help" class="hint">Training uses the selected recipe. Frozen evaluation runs the selected policy in a fresh car without changing its weights.</p><button id="run" disabled>Run training</button><p id="starCheckNotice" class="hint">After each run, supported star challenges are checked with unchanged weights. Results are predictions, not game awards. <a href="/help#concepts" target="_blank" rel="noopener">How checks work</a>.</p><p class="hint">Select or create a network below to enable training. Review Training &amp; recipes before running.</p><p>Supports Formula, Rally, Snowmobile and Truck networks. Control outputs follow the original vehicle editor rules. Network shape and supported sensors are bounded by the original game contract.</p>
42
  <p id="computeBlock"><label for="backend">Compute</label><select id="backend" disabled><option value="cpu">CPU — no GPU allowance</option><option id="gpuOption" value="gpu" disabled>GPU — uses GPU allowance</option></select></p><p id="backendNote" class="hint">Checking available backends…</p>
43
  <div id="connectionRecovery" hidden><button id="retry" type="button">Reconnect to app</button><p class="hint">Checks server availability. Does not restart training, sign you in, or reset GPU allowance.</p></div>
44
  <div id="gpuAccount" aria-label="Optional GPU account"><p><button id="hfSignIn" type="button" disabled>Sign in with HF for account GPU allowance</button><span id="hfAccount" hidden></span> <button id="hfSignOut" type="button" disabled hidden>Disconnect account</button></p><p id="hfIdentity" role="status" class="hint">CPU needs no sign-in. Checking optional GPU account connection…</p>
@@ -53,27 +60,23 @@
53
  </details></section><section class="card"><h2>Visual replay &amp; car black box</h2><p id="mapStatus" class="map-status" role="status">Loading selected track…</p><button type="button" id="replay_focus" aria-pressed="false">Expand replay workspace</button><div class="replay-bar" id="replayControls" tabindex="-1" aria-label="Recorded drive controls">
54
  <div class="replay-row"><button type="button" id="replay_record" disabled>Record selected policy</button><label for="replay_duration" style="margin:0">Length</label><select id="replay_duration"><option value="300" selected>5 seconds</option><option value="900">15 seconds</option></select><button type="button" id="replay_download" disabled>Download recording</button><label for="replay_import">Open recording<input type="file" id="replay_import" accept=".json,application/json" disabled></label></div>
55
  <p id="replay_policy" class="hint">Choose or create a network to record a drive.</p>
56
- <p class="hint">Fresh CPU drive · unchanged weights · 60 Hz physics and decisions · wall-hit and idle elimination off. Recording takes a few seconds; playback runs in your browser.</p>
57
- <p id="replay_status" role="status">No replay recorded. The map shows the selected track and its saved starting point.</p>
58
- <div class="replay-row" id="replay_transport" role="group" aria-label="Playback controls" tabindex="-1"><button type="button" id="replay_play" disabled>Play</button><button type="button" id="replay_reset" disabled>Restart</button><button type="button" id="replay_prev" disabled>Previous sample</button><button type="button" id="replay_next" disabled>Next sample</button><input type="range" id="replay_scrub" min="1" max="300" value="1" disabled aria-label="Replay tick scrubber"><span id="replay_time_label" class="replay-time">No recording</span></div>
59
- <div class="replay-row" style="margin-top:8px"><label for="replay_camera" style="margin:0">Camera</label><select id="replay_camera" aria-label="Replay camera mode" disabled><option value="full" selected>Full circuit</option><option value="follow">Follow car</option></select><label for="replay_speed" style="margin:0">Speed</label><select id="replay_speed" aria-label="Replay playback speed" disabled><option value="0.25">0.25×</option><option value="0.5">0.5×</option><option value="1" selected>1×</option><option value="2">2×</option></select></div>
 
60
  <div class="replay-layers"><label><input type="checkbox" id="replay_layer_outline" checked disabled> Car outline</label><label><input type="checkbox" id="replay_layer_line" checked disabled> Recorded path</label><label><input type="checkbox" id="replay_layer_hud" checked disabled> Recorded telemetry</label></div>
61
  <div class="replay-row"><label><input type="checkbox" id="replay_interpolate" disabled> Smooth pose between measured samples</label><label for="replay_event">Contact / active-state interval</label><select id="replay_event" aria-label="Recorded event interval" disabled><option value="">No recording</option></select></div>
62
 
63
- <p id="replay_readout" style="font-size:16px;font-variant-numeric:tabular-nums" aria-live="off"></p>
64
- <details><summary>What does this recording show?</summary><p>A single fresh CPU drive records native state, raw and normalized control commands, and the exact cached sensor inputs every 0.1 seconds. Physics and policy decisions run at 60 Hz. Playback starts paused and shows measured poses by default; optional pose smoothing only interpolates between adjacent measured samples. It does not simulate the car. Black-box values always remain at an actual recorded sample. Ray-hit positions, skid marks and hidden-layer activations are not recorded. This is a selected-policy CPU replay, including for GPU-trained policies; it is not a recording of the original training population.</p><p>The recording remains in this page until replaced, cleared by a changed selection, or the page is closed. Use Download recording to keep its measured samples.</p></details>
65
- </div><div class="replay-workbench"><div class="map"><canvas id="trackMap" width="1000" height="500" role="img" aria-label="Selected track tile layout and terrain"></canvas></div><section id="replay_blackbox" tabindex="-1" hidden aria-label="Recorded car black box"><div class="section-heading"><h3>Car black box</h3><a href="#replay_transport">Back to playback</a></div><p id="blackbox_phase" class="hint"></p>
66
  <p class="hint">All values below belong to the selected native sample. Sensor inputs were consumed before physics at the decision tick; vehicle state is captured after physics. Samples are 0.1 seconds apart. Contact events identify intervals, not exact impact ticks; the counter does not count unique crashes.</p>
67
  <details><summary>Recorded vehicle state</summary><div class="table-scroll"><table><thead><tr><th>Vehicle state</th><th>Recorded value</th></tr></thead><tbody id="blackbox_state"></tbody></table></div></details>
68
  <h4>Native control commands</h4><div class="table-scroll"><table><thead><tr><th>Control</th><th>Raw command</th><th>Applied after normalization</th><th>Source</th></tr></thead><tbody id="blackbox_controls"></tbody></table></div>
69
  <details><summary>Inputs consumed by the native network</summary><div class="table-scroll"><table><thead><tr><th>Input index</th><th>Sensor</th><th>Parameters</th><th>Native input value</th></tr></thead><tbody id="blackbox_inputs"></tbody></table></div><p class="hint">These are the input values the policy consumed. Ray inputs are not ray-hit positions. Hidden-layer activations are not recorded.</p></details>
70
- <details><summary>Recorded cumulative reward metrics</summary><p class="hint">Metrics accumulate at native statistics callbacks; they are not instantaneous forces or per-reward contributions.</p><div class="table-scroll"><table><thead><tr><th>Metric</th><th>Recorded cumulative value</th></tr></thead><tbody id="blackbox_metrics"></tbody></table></div></details><p><a href="#replay_transport">Back to playback</a></p></section></div><div class="map-legend"><span><i class="terrain asphalt"></i> Asphalt</span><span><i class="terrain dirt"></i> Dirt</span><span><i class="terrain ice"></i> Ice</span><span><i class="terrain start"></i> Start</span></div><p id="mapLegend" class="hint"></p><h2>Run &amp; results</h2><button id="download" disabled>Download result</button> <button id="erase" disabled>Delete server-side job data now</button>
71
- <p><button id="saveLocal" disabled>Download received result (browser only)</button> <button id="forgetLocal" disabled>Discard browser result</button></p>
72
- <p>The browser keeps the received result in memory until discarded, replaced by a new run, or this page is closed. It is not saved to browser storage.</p>
73
- <h3 id="runState" role="status" aria-live="polite">READY — no run started</h3>
74
- <div id="runKpiStrip" class="kpi-strip" hidden><div class="kpi-tile"><div class="kpi-label">Best Fitness</div><div id="kpi_best_score" class="kpi-val">—</div><div id="kpi_best_sub" class="kpi-sub">Final generation</div></div><div class="kpi-tile"><div class="kpi-label">Mean Fitness</div><div id="kpi_mean_score" class="kpi-val" style="color:#58a6ff">—</div><div id="kpi_mean_sub" class="kpi-sub">Population mean</div></div><div class="kpi-tile"><div class="kpi-label">Best Lap Time</div><div id="kpi_best_lap" class="kpi-val" style="color:#ffd073">—</div><div id="kpi_lap_sub" class="kpi-sub">Fastest observed</div></div><div class="kpi-tile"><div class="kpi-label">Lapped Cars</div><div id="kpi_lapped_cars" class="kpi-val" style="color:#c792ea">—</div><div id="kpi_lapped_sub" class="kpi-sub">Completed lap</div></div></div>
75
  <div id="historyChartWrap" class="chart-box" hidden><canvas id="historyChartCanvas" width="920" height="185" role="img" aria-label="Generation fitness and lap time progression chart"></canvas></div>
76
- <pre id="runSummary" style="white-space:pre-wrap;line-height:1.7" aria-live="polite"></pre>
77
  <section id="analysisPanel" hidden aria-labelledby="analysisTitle">
78
  <h2 id="analysisTitle">Winners and frozen evaluation</h2>
79
  <p id="analysisStatus" role="status" aria-live="polite">No complete analysis is available yet.</p>
@@ -84,12 +87,12 @@
84
  <div class="toolbar"><button id="compare_winners" type="button" disabled>Compare selected winners</button></div><p class="hint">Comparison starts one frozen evaluation per distinct selected policy, in sequence. It uses the currently selected track, vehicle, tick limit, control rate and collision/idle settings, then holds those conditions fixed for the full comparison.</p>
85
  <details><summary>Inspect complete measurements for an evaluated car</summary><div class="row"><label for="descriptor_generation">Generation<input id="descriptor_generation" type="number" min="1" value="1"></label><label for="descriptor_car">Car index<input id="descriptor_car" type="number" min="0" value="0"></label></div><button id="descriptor_show" type="button" disabled>Show all descriptors</button><p id="descriptor_status" role="status"></p><div id="descriptor_tables"></div></details>
86
  </div>
87
- <p><button id="analysis_download" type="button" disabled>Download full analysis archive</button></p>
88
  <div id="evaluationTools" hidden><p id="evaluationStatus" role="status" aria-live="polite"></p><p class="hint">Frozen evaluation does not select winners or compare population-relative fitness. Each comparison row keeps its network hash and must match the same track, vehicle, horizon, control rate, collision/idle rules, backend and engine build.</p><button id="compare_add" type="button">Add this frozen result to comparison</button><div class="table-scroll"><table><thead><tr><th>Policy</th><th>Vehicle</th><th>Track</th><th>Ticks</th><th>Best lap</th><th>Forward laps</th><th>Physical contacts</th><th>Distance (simulation units)</th><th>Comparison</th></tr></thead><tbody id="evaluationRows"></tbody></table></div><button id="compare_clear" type="button">Clear comparison</button></div>
89
  </section>
90
  <section id="starCheckPanel" class="card" hidden aria-labelledby="starCheckTitle"><h2 id="starCheckTitle">Star challenge check</h2><p id="starCheckStatus" role="status" aria-live="polite"></p><div id="starCheckDetails" hidden><p id="starCheckPolicy" class="hint"></p><p id="starCheckBasis" class="hint"></p><p id="starCheckConditions" class="hint"></p><div class="table-scroll"><table><thead><tr><th>Challenge</th><th>Result</th><th>Reason</th><th>Completion time</th></tr></thead><tbody id="starCheckRows"></tbody></table></div><p class="hint">This is an independent check of the named run policy. It does not grant or accumulate original-game stars; replica challenges may differ from the original game.</p></div></section>
91
  <label class="keep-result"><input id="leaderboard_opt_in" type="checkbox"> After a completed lap, check its position against the public Steam leaderboard</label><p class="privacy-note">Optional, read-only check for supported built-in tracks. It sends the track ID and best lap to this app server; Steam receives only a public leaderboard read. No score is submitted. The result is an estimate, not an official rank.</p><p id="leaderboard_status" role="status" aria-live="polite"></p>
92
- <p><button id="run_export" disabled>Export run receipt</button> <button id="player_report_download" disabled>Download player report (.txt)</button> <button id="history_csv_download" disabled>Download generation history (.csv)</button></p><p class="hint">Reports use only server-reported run and generation fields. The engine does not report per-reward contribution, so reward weights are shown as configured values only.</p>
93
  <details id="portableBundle"><summary>Portable bucket bundle</summary><p>Build, export, or restore one validated bundle. Bucket storage is optional. When connected, automatic save is on by default; turn it off here to keep saves manual. Disconnected runs never call bucket storage.</p><label><input id="bundle_track" type="checkbox"> Include the selected track</label><label><input id="bundle_recipe" type="checkbox"> Include the recipe in the selected slot, if saved</label><label><input id="bundle_receipt" type="checkbox"> Include a sanitized receipt for the selected completed result</label><div class="toolbar"><button id="bundle_export" type="button">Export portable bundle</button><label for="bundle_import">Restore portable bundle (.json)<input id="bundle_import" type="file" accept=".json,application/json"></label><button id="bundle_receipt_download" type="button" disabled>Download staged receipt</button></div><p id="bundle_status" role="status" aria-live="polite">No bucket action has been requested.</p>
94
  <details id="bucketStorage"><summary>Connect to an existing private bucket</summary><p>Bucket storage is optional. Authorizing storage also identifies your account for GPU requests; ordinary GPU sign-in remains available with the basic identity permission. Then enter your existing private bucket ID. Connected runs automatically save by default; uncheck this option to keep run saves manual. No personal token is requested or stored.</p><p id="bucket_scope_notice" class="privacy-note">HF describes the requested <code>write-repos</code> permission as read/write access to your personal repositories. That grant is broader than one selected bucket and is not a bucket-only permission; the app limits its own operations to the bucket you enter. The minimum current scope for bucket read/write has not been confirmed by HF.</p><button id="bucket_authorize" type="button" hidden disabled>Authorize bucket access with HF</button><p id="bucket_auth_unavailable" class="hint" hidden>HF bucket authorization is unavailable for this app. Local bundle export remains available.</p><p id="bucket_auth_status" role="status" aria-live="polite">Checking optional HF bucket authorization…</p><div id="bucketConnectForm" hidden><label for="bucket_id">Private bucket ID</label><input id="bucket_id" type="text" autocomplete="off" placeholder="username/bucket-name" disabled><button id="bucket_connect" type="button" disabled>Connect bucket</button></div><div id="bucketConnected" hidden><p id="bucket_account"></p><label class="keep-result"><input id="bucket_autosave" type="checkbox" checked> Automatically save every verified completed network and run log to this bucket</label><p class="hint">For each completed run, the browser fetches and verifies the network, then confirms server deletion before sending the network and sanitized log to your bucket. If deletion is not confirmed, no save occurs. If bucket saving is not confirmed, the verified local candidate stays available to download or retry; the remote save may have completed. Each batch step saves before the next starts, and a failed save stops the batch.</p><button id="bucket_disconnect" type="button">Disconnect bucket and clear HF authorization</button><div class="toolbar"><button id="bucket_save" type="button" disabled>Save selected bundle</button><button id="bucket_refresh" type="button" disabled>Refresh saved bundles</button></div><label for="bucket_items">Saved bundles from this app</label><select id="bucket_items" size="4" disabled><option value="">Refresh to list saved bundles</option></select><button id="bucket_restore" type="button" disabled>Restore selected bundle</button><hr><h3 id="bucketRecipeTitle" tabindex="-1">Recipe library (settings only)</h3><p class="hint">Save and import the validated recipes and batch definitions from this browser. This 32 KB library contains settings only; networks, tracks, run results and access tokens are never included. Import asks before replacing the local recipe library and never starts a run.</p><div class="toolbar"><button id="bucket_recipe_save" type="button" disabled>Save recipe library</button><button id="bucket_recipe_refresh" type="button" disabled>Refresh saved recipe libraries</button></div><label for="bucket_recipe_items">Saved recipe libraries</label><select id="bucket_recipe_items" size="4" disabled><option value="">Refresh to list saved recipe libraries</option></select><button id="bucket_recipe_import" type="button" disabled>Import selected recipe library</button><p id="bucket_recipe_status" role="status" aria-live="polite">No recipe library bucket action requested.</p></div><p id="bucket_status" role="status" aria-live="polite">Not connected. Bucket storage is optional.</p></details></details>
95
  <p id="deadline"></p><p id="status" role="status">Ready. Inputs remain on this device until you run the test.</p>
@@ -805,10 +808,20 @@
805
  // Navigation and help preference only; these controls never start a run.
806
  (() => {
807
  const tabs=[...document.querySelectorAll('.tabs [data-tab]')];
 
 
 
 
 
 
 
 
 
808
  function activate(button,{focusPanel=true}={}){
 
809
  document.querySelectorAll('.pane').forEach(pane=>pane.hidden=pane.id!==button.dataset.tab);
810
  tabs.forEach(tab=>{const active=tab===button;tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;});
811
- const heading=document.getElementById(button.dataset.tab)?.querySelector('h2');
812
  if(heading){heading.tabIndex=-1;heading.scrollIntoView({block:'start',behavior:'instant'});if(focusPanel)heading.focus({preventScroll:true});}
813
  window.dispatchEvent(new CustomEvent('ald:section-change'));
814
  }
@@ -831,7 +844,7 @@
831
  const target=requested&&!requested.matches(':disabled')&&!requested.closest('[hidden]')?requested:tab;
832
  target.scrollIntoView({block:'center'});target.focus();
833
  }));
834
- const guide=document.getElementById('firstRunGuide'),toggle=document.getElementById('quick_start_toggle'),key='tiga.ald.quick-start.v1';
835
  try{guide.open=window.localStorage.getItem(key)==='open';}catch{}
836
  const syncHelp=()=>toggle.setAttribute('aria-expanded',String(guide.open));syncHelp();
837
  guide.addEventListener('toggle',()=>{syncHelp();try{window.localStorage.setItem(key,guide.open?'open':'closed');}catch{}});
 
8
  .replay-bar{background:#101d2c;border:1px solid #2a3d52;border-radius:11px;padding:10px 12px;margin:10px 0}.replay-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.replay-row button{min-height:36px;padding:6px 11px;font-size:13px;font-weight:650}.replay-row select{width:auto;min-height:36px;padding:5px 9px;font-size:13px}.replay-row input[type=range]{flex:1;min-width:120px;accent-color:#62dfb1;padding:0}.replay-time{font:600 12px ui-monospace,SFMono-Regular,monospace;color:#91efc6;white-space:nowrap}.replay-layers{display:flex;gap:6px 12px;flex-wrap:wrap;margin-top:8px;padding-top:8px;border-top:1px solid #223447;font-size:12.5px;color:#b9cddd}.replay-layers label{display:inline-flex;align-items:center;gap:5px;margin:0;cursor:pointer}.replay-layers input{margin:0}.kpi-strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:12px 0}.kpi-tile{background:#101d2c;border:1px solid #2a3d52;border-radius:10px;padding:10px 12px}.kpi-tile .kpi-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;color:#a2b4c8}.kpi-tile .kpi-val{font:800 19px ui-monospace,SFMono-Regular,monospace;color:#62dfb1;margin-top:3px}.kpi-tile .kpi-sub{font-size:11.5px;color:#8fa6bc;margin-top:2px}.chart-box{background:#09111b;border:1px solid #2a3d52;border-radius:10px;padding:10px;margin:10px 0}.chart-box canvas{max-height:190px;display:block}.subtab-bar{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.subtab-bar button{min-height:36px;padding:6px 12px;font-size:13px;border-radius:18px}.subtab-bar button[aria-pressed=true]{background:#62dfb1;color:#102522;font-weight:700}.editor-visual-grid{display:grid;grid-template-columns:minmax(260px,.85fr) minmax(0,1.35fr);gap:16px;align-items:start;margin:14px 0}@media(max-width:850px){.kpi-strip{grid-template-columns:1fr 1fr}.editor-visual-grid{grid-template-columns:minmax(0,1fr)}}
9
 
10
  .replay-workbench{display:grid;gap:14px;align-items:start}#replayControls,#replay_transport,#replay_blackbox{scroll-margin-top:160px}#replayControls{min-width:0}.replay-row>*{max-width:100%}#replay_blackbox{padding:12px;border:1px solid #2a3d52;border-radius:10px;min-width:0}#replay_blackbox h3{margin-top:0}#replay_blackbox td,#replay_blackbox th{padding:6px;font-size:12px}#drive.replay-focused>.grid{grid-template-columns:minmax(0,1fr)}#drive.replay-focused>.grid>section:first-child{display:none}#drive.replay-focused .replay-workbench{grid-template-columns:minmax(0,1.35fr) minmax(320px,1fr)}#drive.replay-focused .map{position:sticky;top:70px}#drive.replay-focused .map canvas{max-height:500px}@media(max-width:850px){#drive.replay-focused .replay-workbench{grid-template-columns:minmax(0,1fr)}#drive.replay-focused .map{position:static}}
11
+
12
+ /* Compact release workspace. All scroll targets use the measured wrapped tab height. */
13
+ :root{--lab-nav-height:76px;--lab-scroll-offset:calc(var(--lab-nav-height) + 14px)}
14
+ .pane h2,#editor_status,#agent-interface,#replayControls,#replay_transport,#replay_blackbox,#replay_workspace,button,a,summary,input,select,textarea,[tabindex="-1"]{scroll-margin-top:var(--lab-scroll-offset)}
15
+ .lab-utilities{display:flex;gap:8px 22px;flex-wrap:wrap;align-items:start;margin:2px 0 6px}.lab-utilities>details{border:0;margin:0;padding:0;font-size:13px}.lab-utilities>details>summary{display:flex;align-items:center;min-height:34px;font-size:13px}.lab-utilities>details[open]{flex-basis:100%;border:1px solid #2a3d52;border-radius:10px;padding:0 14px 12px}.lab-utilities #agent-interface{margin:0}.quick-start{margin:4px 0 0;padding:0 12px}.quick-start>summary{padding:8px 0;min-height:40px;font-size:14px}.quick-steps{gap:12px;padding:6px 0}.quick-steps strong{font-size:14px;margin-bottom:4px}.quick-steps p{font-size:13px;line-height:1.4;margin-bottom:6px}.quick-steps button{font-size:13px;padding:6px 9px;min-height:36px}.quick-footer{padding:8px 0;font-size:13px}.quick-footer button{font-size:13px;min-height:36px;padding:6px 10px}.quick-start>summary .hint{font-size:12px}.lab-header{gap:8px}.lab-header .intro{margin-bottom:8px}.batch-setup{top:var(--lab-scroll-offset)}
16
+ .result-summary{padding:16px 20px}.result-summary h3{font-size:16px;margin:8px 0}.result-summary .pill{margin:0;font-size:12px}.result-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.result-actions button{font-size:14px;min-height:40px;padding:8px 12px}.result-summary details{padding-top:8px;margin-top:10px}.result-summary details>summary{font-size:13px;min-height:32px}.result-summary pre{font-size:13px}.result-summary .settings-summary{margin:5px 0;font-size:13px}.result-summary #result_server{color:#91efc6}.replay-viewer{min-width:0;scroll-margin-top:var(--lab-scroll-offset)}.replay-playback{margin-top:0}.replay-playback #replay_readout{font-size:13px!important;line-height:1.45;margin:8px 0}.replay-playback #replay_status{font-size:12px;color:#a2b4c8;line-height:1.4;margin:6px 0}.replay-options{margin-top:8px;padding-top:5px}.replay-options>summary{font-size:12px;min-height:30px}.replay-options .replay-row{margin-top:8px}.replay-options label{font-size:12px}.recording-identity{font-size:13px;line-height:1.5;padding:8px 10px;background:#14362f;border:1px solid #3d7561;border-radius:7px;overflow-wrap:anywhere}.replay-row input[type=file]{font-size:12px;padding:5px;max-width:260px}.replay-row label[for=replay_import]{font-size:13px;margin:0}.replay-row label[for=replay_import] input{display:block}.replay-row button{min-height:40px}.replay-row input[type=range]{flex-basis:160px}.replay-time{white-space:normal}.replay-bar>details{margin-top:8px;padding-top:5px}.replay-bar>details>summary{font-size:12px;min-height:28px}.replay-bar>p.hint{font-size:12px;line-height:1.45;margin:8px 0}.replay-workbench{gap:16px}#drive.replay-focused .replay-viewer{position:sticky;top:var(--lab-scroll-offset)}#drive.replay-focused .map{position:static;min-height:0}#drive.replay-focused .map canvas{height:min(38vh,300px);max-height:300px}#drive.replay-focused .replay-workbench{grid-template-columns:minmax(0,1.2fr) minmax(320px,1fr)}
17
+ @media(max-width:850px){#drive.replay-focused .replay-workbench{grid-template-columns:minmax(0,1fr)}#drive.replay-focused .replay-viewer{position:static}.result-summary{padding:14px}.result-actions>*{flex:1 1 160px}#drive.replay-focused .map canvas{height:auto;max-height:300px}}@media(max-width:600px){.quick-start>summary .hint{margin-left:0}.quick-steps{gap:10px}.quick-steps li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:0 8px;align-items:center}.quick-steps strong{grid-column:1}.quick-steps p{grid-column:1;margin:0}.quick-steps button{grid-column:2;grid-row:1 / 3;max-width:135px}.header-actions{width:auto}.header-actions>*{flex:initial}.help-link,#quick_start_toggle{font-size:13px;padding:7px 10px;min-height:38px}.lab-header h1{font-size:26px}.lab-header .intro{font-size:14px}.lab-utilities{gap:8px 18px}.replay-row label[for=replay_import]{width:100%}.replay-row label[for=replay_import] input{max-width:100%;width:100%}}
18
  </style>
19
  <dialog id="appConfirm" aria-labelledby="appConfirmTitle" aria-describedby="appConfirmMessage">
20
  <h2 id="appConfirmTitle">Confirm action</h2>
 
43
  })();
44
  </script>
45
  <header class="lab-header"><div><div class="eyebrow">AI LEARNS TO DRIVE</div><h1>Training lab</h1><p class="intro">Build a driver, train it, inspect the result.</p></div><div class="header-actions"><button id="quick_start_toggle" type="button" aria-controls="firstRunGuide" aria-expanded="false">Quick start</button><a class="help-link" href="/help#start-here" target="_blank" rel="noopener">Help &amp; tutorial ↗</a></div></header>
46
+ <div class="lab-utilities"><details class="about"><summary>About &amp; feedback</summary><p>Simulator by <a href="https://github.com/Zorro909/driving_sim_rs">Zorro909</a>; interface and hosting by <a href="https://github.com/seeker-cyber-maker/driving_sim_rs">TiGa</a>. Inspired by the <a href="https://store.steampowered.com/app/3312030/AI_Learns_To_Drive/">game by ActivationFunction</a>. Built-in tracks are reconstructed replicas. For supported conditions, the lab checks the run’s final fitness policy (or frozen input) with a bounded fresh replay. It predicts conditions only and does not award or accumulate original-game stars; opponent races and other unsupported conditions remain unchecked.</p><p>Uploaded files are used for the requested run. Results are held briefly in application memory and deleted after verified download or expiry. Browser recipes contain settings only. <a href="/policy" target="_blank" rel="noopener">Read the data policy</a>.</p><p>The map follows the selected track’s native centerline. Record a short CPU drive to watch an unchanged policy using real vehicle positions. A new recording can differ from an earlier training run. Native control commands and consumed sensor inputs appear in the black box. Ray-hit positions and neural activations are not recorded.</p><p><a id="feedbackLink" href="https://github.com/seeker-cyber-maker/driving_sim_rs/issues/new" target="_blank" rel="noopener">Report a problem</a> · Opens an editable draft for you to review.</p><p id="buildLabel" class="hint">Build: connecting…</p></details>
47
+ <details id="agent-interface" class="agent-utility"><summary>Agent tools</summary><p>Use the same local controls as the lab: read the current state, open a section, create a starter, or inspect the active driver. These tools do not submit jobs or save data. <a href="/agents" target="_blank" rel="noopener">Agent interface guide</a> · <a href="/agent-manifest.json" target="_blank" rel="noopener">Action manifest (JSON)</a></p><p id="agent_tools_status" role="status" aria-live="polite">Checking browser tool support…</p><p>The state below is also available to ordinary browser automation. Each listed action identifies a stable control in this page.</p><pre id="agent_state" aria-label="Current lab state (JSON)"></pre></details></div><details id="firstRunGuide" class="quick-start"><summary id="firstRunTitle">New here? Start a first run <span class="hint">Track → driver → settings → run</span></summary><ol class="quick-steps"><li><strong>Choose a track</strong><p>Built-in replica or your own track.</p><button id="first_run_track" type="button">Choose a track</button></li><li><strong>Create a driver</strong><p>Create a starter or upload a driver.</p><button type="button" data-open-tab="drive" data-focus="starter_create">Set up a network</button></li><li><strong>Review the settings</strong><p>Short first-run defaults are ready. Adjust them or save a recipe.</p><button id="first_run_settings" type="button">Review first-run settings</button></li><li><strong>Run and inspect</strong><p>Train, download and replay. A first run may not finish a lap.</p><button type="button" data-open-tab="drive" data-focus="run">Go to run controls</button></li></ol><div class="quick-footer"><a href="/help#start-here" target="_blank" rel="noopener">Read the full step-by-step tutorial ↗</a><button id="quick_start_close" type="button">Close quick start</button></div></details>
48
+ <nav class="tabs" role="tablist" aria-label="Lab sections"><button type="button" id="tab-drive" role="tab" data-tab="drive" aria-controls="drive" aria-selected="true" tabindex="0">Run &amp; results</button><button type="button" id="tab-training" role="tab" data-tab="training" aria-controls="training" aria-selected="false" tabindex="-1">Training &amp; recipes</button><button type="button" id="tab-batch" role="tab" data-tab="batch" aria-controls="batch" aria-selected="false" tabindex="-1">Batches</button><button type="button" id="tab-editor" role="tab" data-tab="editor" aria-controls="editor" aria-selected="false" tabindex="-1">Network editor</button></nav><div id="drive" class="pane" role="tabpanel" aria-labelledby="tab-drive"><section id="resultSummary" class="card result-summary" hidden aria-labelledby="result_summary_title"><div class="section-heading"><h2 id="result_summary_title" tabindex="-1">Latest run</h2><span id="result_backend" class="pill"></span></div><h3 id="runState" role="status" aria-live="polite">READY — no run started</h3><p id="result_outcome" class="settings-summary"></p><p id="result_driver" class="settings-summary"></p><p id="result_server" class="settings-summary" role="status"></p><div class="result-actions"><button id="download" class="primary" disabled hidden>Download driver</button><button id="saveLocal" class="primary" disabled>Download received driver</button><button id="result_replay" type="button" disabled>Replay selected driver</button><button id="player_report_download" disabled>Download player report (.txt)</button></div><details class="result-exports"><summary>More downloads &amp; server controls</summary><div class="result-actions"><button id="analysis_download" type="button" disabled>Download full analysis archive</button><button id="run_export" disabled>Export run receipt</button><button id="history_csv_download" disabled>Download generation history (.csv)</button><button id="erase" disabled>Delete server-side job data now</button><button id="forgetLocal" disabled>Discard browser result</button></div><p class="privacy-note">The received driver and analysis stay in this page until discarded, replaced, or closed. Save downloads to keep them. Reports describe server-reported measurements; reward weights are configured values, not measured contributions.</p></details><details><summary>Run details</summary><pre id="runSummary" style="white-space:pre-wrap;line-height:1.7" aria-live="polite"></pre></details></section><div class="grid"><section class="card"><h2>Simulation</h2><div class="toolbar" aria-label="Create a starter network"><label for="starter_vehicle">Starter car</label><select id="starter_vehicle"><option value="0" selected>Formula</option><option value="1">Rally</option><option value="2">Snowmobile</option><option value="3">Truck</option></select><button id="starter_create" type="button">Create starter network</button></div><p id="starter_network_status" class="hint" role="status">Creates a small network in this page only. It does not start a run or change saved recipes.</p><label for="run_mode">Action</label><select id="run_mode"><option value="train" selected>Train a population</option><option value="evaluate">Frozen evaluation · one policy, unchanged weights</option></select><p id="run_mode_help" class="hint">Training uses the selected recipe. Frozen evaluation runs the selected policy in a fresh car without changing its weights.</p><button id="run" disabled>Run training</button><p id="starCheckNotice" class="hint">After each run, supported star challenges are checked with unchanged weights. Results are predictions, not game awards. <a href="/help#concepts" target="_blank" rel="noopener">How checks work</a>.</p><p id="setup_next_step" class="hint">Choose or create a driver, then review Training &amp; recipes and start a run.</p><p>Supports Formula, Rally, Snowmobile and Truck networks. Control outputs follow the original vehicle editor rules. Network shape and supported sensors are bounded by the original game contract.</p>
49
  <p id="computeBlock"><label for="backend">Compute</label><select id="backend" disabled><option value="cpu">CPU — no GPU allowance</option><option id="gpuOption" value="gpu" disabled>GPU — uses GPU allowance</option></select></p><p id="backendNote" class="hint">Checking available backends…</p>
50
  <div id="connectionRecovery" hidden><button id="retry" type="button">Reconnect to app</button><p class="hint">Checks server availability. Does not restart training, sign you in, or reset GPU allowance.</p></div>
51
  <div id="gpuAccount" aria-label="Optional GPU account"><p><button id="hfSignIn" type="button" disabled>Sign in with HF for account GPU allowance</button><span id="hfAccount" hidden></span> <button id="hfSignOut" type="button" disabled hidden>Disconnect account</button></p><p id="hfIdentity" role="status" class="hint">CPU needs no sign-in. Checking optional GPU account connection…</p>
 
60
  </details></section><section class="card"><h2>Visual replay &amp; car black box</h2><p id="mapStatus" class="map-status" role="status">Loading selected track…</p><button type="button" id="replay_focus" aria-pressed="false">Expand replay workspace</button><div class="replay-bar" id="replayControls" tabindex="-1" aria-label="Recorded drive controls">
61
  <div class="replay-row"><button type="button" id="replay_record" disabled>Record selected policy</button><label for="replay_duration" style="margin:0">Length</label><select id="replay_duration"><option value="300" selected>5 seconds</option><option value="900">15 seconds</option></select><button type="button" id="replay_download" disabled>Download recording</button><label for="replay_import">Open recording<input type="file" id="replay_import" accept=".json,application/json" disabled></label></div>
62
  <p id="replay_policy" class="hint">Choose or create a network to record a drive.</p>
63
+ <p class="hint">Fresh CPU drive, including for GPU-trained drivers · unchanged weights · 60 Hz decisions. Playback uses measured samples in your browser.</p>
64
+ <p id="replay_loaded" class="recording-identity" role="status" aria-live="polite" hidden></p>
65
+ <details><summary>What does this recording show?</summary><p>A single fresh CPU drive records native state, raw and normalized control commands, and the exact cached sensor inputs every 0.1 seconds. Physics and policy decisions run at 60 Hz. Playback starts paused and shows measured poses by default; optional pose smoothing only interpolates between adjacent measured samples. It does not simulate the car. Black-box values always remain at an actual recorded sample. Ray-hit positions, skid marks and hidden-layer activations are not recorded. This is a selected-policy CPU replay, including for GPU-trained policies; it is not a recording of the original training population.</p><p>The recording remains in this page until replaced, cleared by a changed selection, or the page is closed. Use Download recording to keep its measured samples.</p></details>
66
+ </div><div class="replay-workbench" id="replay_workspace"><div class="replay-viewer"><div class="map"><canvas id="trackMap" width="1000" height="500" role="img" aria-label="Selected track tile layout and terrain"></canvas></div><div class="replay-bar replay-playback"><div class="replay-row" id="replay_transport" role="group" aria-label="Playback controls" tabindex="-1"><button type="button" id="replay_play" disabled>Play</button><button type="button" id="replay_reset" disabled>Restart</button><button type="button" id="replay_prev" disabled>Previous sample</button><button type="button" id="replay_next" disabled>Next sample</button><input type="range" id="replay_scrub" min="1" max="300" value="1" disabled aria-label="Replay tick scrubber"><span id="replay_time_label" class="replay-time">No recording</span></div>
67
+ <details class="replay-options"><summary>Camera, speed &amp; recorded events</summary><div class="replay-row" style="margin-top:8px"><label for="replay_camera" style="margin:0">Camera</label><select id="replay_camera" aria-label="Replay camera mode" disabled><option value="full" selected>Full circuit</option><option value="follow">Follow car</option></select><label for="replay_speed" style="margin:0">Speed</label><select id="replay_speed" aria-label="Replay playback speed" disabled><option value="0.25">0.25×</option><option value="0.5">0.5×</option><option value="1" selected>1×</option><option value="2">2×</option></select></div>
68
  <div class="replay-layers"><label><input type="checkbox" id="replay_layer_outline" checked disabled> Car outline</label><label><input type="checkbox" id="replay_layer_line" checked disabled> Recorded path</label><label><input type="checkbox" id="replay_layer_hud" checked disabled> Recorded telemetry</label></div>
69
  <div class="replay-row"><label><input type="checkbox" id="replay_interpolate" disabled> Smooth pose between measured samples</label><label for="replay_event">Contact / active-state interval</label><select id="replay_event" aria-label="Recorded event interval" disabled><option value="">No recording</option></select></div>
70
 
71
+ </details><p id="replay_readout" style="font-size:16px;font-variant-numeric:tabular-nums" aria-live="off"></p>
72
+ <p id="replay_status" role="status">No replay recorded. The map shows the selected track and its saved starting point.</p></div></div><section id="replay_blackbox" tabindex="-1" hidden aria-label="Recorded car black box"><div class="section-heading"><h3>Car black box</h3><a href="#replay_transport">Back to playback</a></div><p id="blackbox_phase" class="hint"></p>
 
73
  <p class="hint">All values below belong to the selected native sample. Sensor inputs were consumed before physics at the decision tick; vehicle state is captured after physics. Samples are 0.1 seconds apart. Contact events identify intervals, not exact impact ticks; the counter does not count unique crashes.</p>
74
  <details><summary>Recorded vehicle state</summary><div class="table-scroll"><table><thead><tr><th>Vehicle state</th><th>Recorded value</th></tr></thead><tbody id="blackbox_state"></tbody></table></div></details>
75
  <h4>Native control commands</h4><div class="table-scroll"><table><thead><tr><th>Control</th><th>Raw command</th><th>Applied after normalization</th><th>Source</th></tr></thead><tbody id="blackbox_controls"></tbody></table></div>
76
  <details><summary>Inputs consumed by the native network</summary><div class="table-scroll"><table><thead><tr><th>Input index</th><th>Sensor</th><th>Parameters</th><th>Native input value</th></tr></thead><tbody id="blackbox_inputs"></tbody></table></div><p class="hint">These are the input values the policy consumed. Ray inputs are not ray-hit positions. Hidden-layer activations are not recorded.</p></details>
77
+ <details><summary>Recorded cumulative reward metrics</summary><p class="hint">Metrics accumulate at native statistics callbacks; they are not instantaneous forces or per-reward contributions.</p><div class="table-scroll"><table><thead><tr><th>Metric</th><th>Recorded cumulative value</th></tr></thead><tbody id="blackbox_metrics"></tbody></table></div></details><p><a href="#replay_transport">Back to playback</a></p></section></div><div class="map-legend"><span><i class="terrain asphalt"></i> Asphalt</span><span><i class="terrain dirt"></i> Dirt</span><span><i class="terrain ice"></i> Ice</span><span><i class="terrain start"></i> Start</span></div><p id="mapLegend" class="hint"></p><div id="runKpiStrip" class="kpi-strip" hidden><div class="kpi-tile"><div class="kpi-label">Best Fitness</div><div id="kpi_best_score" class="kpi-val">—</div><div id="kpi_best_sub" class="kpi-sub">Final generation</div></div><div class="kpi-tile"><div class="kpi-label">Mean Fitness</div><div id="kpi_mean_score" class="kpi-val" style="color:#58a6ff">—</div><div id="kpi_mean_sub" class="kpi-sub">Population mean</div></div><div class="kpi-tile"><div class="kpi-label">Best Lap Time</div><div id="kpi_best_lap" class="kpi-val" style="color:#ffd073">—</div><div id="kpi_lap_sub" class="kpi-sub">Fastest observed</div></div><div class="kpi-tile"><div class="kpi-label">Lapped Cars</div><div id="kpi_lapped_cars" class="kpi-val" style="color:#c792ea">—</div><div id="kpi_lapped_sub" class="kpi-sub">Completed lap</div></div></div>
 
 
 
 
78
  <div id="historyChartWrap" class="chart-box" hidden><canvas id="historyChartCanvas" width="920" height="185" role="img" aria-label="Generation fitness and lap time progression chart"></canvas></div>
79
+
80
  <section id="analysisPanel" hidden aria-labelledby="analysisTitle">
81
  <h2 id="analysisTitle">Winners and frozen evaluation</h2>
82
  <p id="analysisStatus" role="status" aria-live="polite">No complete analysis is available yet.</p>
 
87
  <div class="toolbar"><button id="compare_winners" type="button" disabled>Compare selected winners</button></div><p class="hint">Comparison starts one frozen evaluation per distinct selected policy, in sequence. It uses the currently selected track, vehicle, tick limit, control rate and collision/idle settings, then holds those conditions fixed for the full comparison.</p>
88
  <details><summary>Inspect complete measurements for an evaluated car</summary><div class="row"><label for="descriptor_generation">Generation<input id="descriptor_generation" type="number" min="1" value="1"></label><label for="descriptor_car">Car index<input id="descriptor_car" type="number" min="0" value="0"></label></div><button id="descriptor_show" type="button" disabled>Show all descriptors</button><p id="descriptor_status" role="status"></p><div id="descriptor_tables"></div></details>
89
  </div>
90
+
91
  <div id="evaluationTools" hidden><p id="evaluationStatus" role="status" aria-live="polite"></p><p class="hint">Frozen evaluation does not select winners or compare population-relative fitness. Each comparison row keeps its network hash and must match the same track, vehicle, horizon, control rate, collision/idle rules, backend and engine build.</p><button id="compare_add" type="button">Add this frozen result to comparison</button><div class="table-scroll"><table><thead><tr><th>Policy</th><th>Vehicle</th><th>Track</th><th>Ticks</th><th>Best lap</th><th>Forward laps</th><th>Physical contacts</th><th>Distance (simulation units)</th><th>Comparison</th></tr></thead><tbody id="evaluationRows"></tbody></table></div><button id="compare_clear" type="button">Clear comparison</button></div>
92
  </section>
93
  <section id="starCheckPanel" class="card" hidden aria-labelledby="starCheckTitle"><h2 id="starCheckTitle">Star challenge check</h2><p id="starCheckStatus" role="status" aria-live="polite"></p><div id="starCheckDetails" hidden><p id="starCheckPolicy" class="hint"></p><p id="starCheckBasis" class="hint"></p><p id="starCheckConditions" class="hint"></p><div class="table-scroll"><table><thead><tr><th>Challenge</th><th>Result</th><th>Reason</th><th>Completion time</th></tr></thead><tbody id="starCheckRows"></tbody></table></div><p class="hint">This is an independent check of the named run policy. It does not grant or accumulate original-game stars; replica challenges may differ from the original game.</p></div></section>
94
  <label class="keep-result"><input id="leaderboard_opt_in" type="checkbox"> After a completed lap, check its position against the public Steam leaderboard</label><p class="privacy-note">Optional, read-only check for supported built-in tracks. It sends the track ID and best lap to this app server; Steam receives only a public leaderboard read. No score is submitted. The result is an estimate, not an official rank.</p><p id="leaderboard_status" role="status" aria-live="polite"></p>
95
+
96
  <details id="portableBundle"><summary>Portable bucket bundle</summary><p>Build, export, or restore one validated bundle. Bucket storage is optional. When connected, automatic save is on by default; turn it off here to keep saves manual. Disconnected runs never call bucket storage.</p><label><input id="bundle_track" type="checkbox"> Include the selected track</label><label><input id="bundle_recipe" type="checkbox"> Include the recipe in the selected slot, if saved</label><label><input id="bundle_receipt" type="checkbox"> Include a sanitized receipt for the selected completed result</label><div class="toolbar"><button id="bundle_export" type="button">Export portable bundle</button><label for="bundle_import">Restore portable bundle (.json)<input id="bundle_import" type="file" accept=".json,application/json"></label><button id="bundle_receipt_download" type="button" disabled>Download staged receipt</button></div><p id="bundle_status" role="status" aria-live="polite">No bucket action has been requested.</p>
97
  <details id="bucketStorage"><summary>Connect to an existing private bucket</summary><p>Bucket storage is optional. Authorizing storage also identifies your account for GPU requests; ordinary GPU sign-in remains available with the basic identity permission. Then enter your existing private bucket ID. Connected runs automatically save by default; uncheck this option to keep run saves manual. No personal token is requested or stored.</p><p id="bucket_scope_notice" class="privacy-note">HF describes the requested <code>write-repos</code> permission as read/write access to your personal repositories. That grant is broader than one selected bucket and is not a bucket-only permission; the app limits its own operations to the bucket you enter. The minimum current scope for bucket read/write has not been confirmed by HF.</p><button id="bucket_authorize" type="button" hidden disabled>Authorize bucket access with HF</button><p id="bucket_auth_unavailable" class="hint" hidden>HF bucket authorization is unavailable for this app. Local bundle export remains available.</p><p id="bucket_auth_status" role="status" aria-live="polite">Checking optional HF bucket authorization…</p><div id="bucketConnectForm" hidden><label for="bucket_id">Private bucket ID</label><input id="bucket_id" type="text" autocomplete="off" placeholder="username/bucket-name" disabled><button id="bucket_connect" type="button" disabled>Connect bucket</button></div><div id="bucketConnected" hidden><p id="bucket_account"></p><label class="keep-result"><input id="bucket_autosave" type="checkbox" checked> Automatically save every verified completed network and run log to this bucket</label><p class="hint">For each completed run, the browser fetches and verifies the network, then confirms server deletion before sending the network and sanitized log to your bucket. If deletion is not confirmed, no save occurs. If bucket saving is not confirmed, the verified local candidate stays available to download or retry; the remote save may have completed. Each batch step saves before the next starts, and a failed save stops the batch.</p><button id="bucket_disconnect" type="button">Disconnect bucket and clear HF authorization</button><div class="toolbar"><button id="bucket_save" type="button" disabled>Save selected bundle</button><button id="bucket_refresh" type="button" disabled>Refresh saved bundles</button></div><label for="bucket_items">Saved bundles from this app</label><select id="bucket_items" size="4" disabled><option value="">Refresh to list saved bundles</option></select><button id="bucket_restore" type="button" disabled>Restore selected bundle</button><hr><h3 id="bucketRecipeTitle" tabindex="-1">Recipe library (settings only)</h3><p class="hint">Save and import the validated recipes and batch definitions from this browser. This 32 KB library contains settings only; networks, tracks, run results and access tokens are never included. Import asks before replacing the local recipe library and never starts a run.</p><div class="toolbar"><button id="bucket_recipe_save" type="button" disabled>Save recipe library</button><button id="bucket_recipe_refresh" type="button" disabled>Refresh saved recipe libraries</button></div><label for="bucket_recipe_items">Saved recipe libraries</label><select id="bucket_recipe_items" size="4" disabled><option value="">Refresh to list saved recipe libraries</option></select><button id="bucket_recipe_import" type="button" disabled>Import selected recipe library</button><p id="bucket_recipe_status" role="status" aria-live="polite">No recipe library bucket action requested.</p></div><p id="bucket_status" role="status" aria-live="polite">Not connected. Bucket storage is optional.</p></details></details>
98
  <p id="deadline"></p><p id="status" role="status">Ready. Inputs remain on this device until you run the test.</p>
 
808
  // Navigation and help preference only; these controls never start a run.
809
  (() => {
810
  const tabs=[...document.querySelectorAll('.tabs [data-tab]')];
811
+ const navigation=document.querySelector('.tabs');
812
+ function syncNavigationOffset(){
813
+ const height=Math.ceil(navigation.getBoundingClientRect().height);
814
+ document.documentElement.style.setProperty('--lab-nav-height',height+'px');
815
+ }
816
+ syncNavigationOffset();
817
+ if(typeof ResizeObserver==='function')new ResizeObserver(syncNavigationOffset).observe(navigation);
818
+ window.addEventListener('resize',syncNavigationOffset);
819
+ document.fonts?.ready?.then(syncNavigationOffset);
820
  function activate(button,{focusPanel=true}={}){
821
+ syncNavigationOffset();
822
  document.querySelectorAll('.pane').forEach(pane=>pane.hidden=pane.id!==button.dataset.tab);
823
  tabs.forEach(tab=>{const active=tab===button;tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;});
824
+ const heading=[...(document.getElementById(button.dataset.tab)?.querySelectorAll('h2')??[])].find(node=>node.getClientRects().length);
825
  if(heading){heading.tabIndex=-1;heading.scrollIntoView({block:'start',behavior:'instant'});if(focusPanel)heading.focus({preventScroll:true});}
826
  window.dispatchEvent(new CustomEvent('ald:section-change'));
827
  }
 
844
  const target=requested&&!requested.matches(':disabled')&&!requested.closest('[hidden]')?requested:tab;
845
  target.scrollIntoView({block:'center'});target.focus();
846
  }));
847
+ const guide=document.getElementById('firstRunGuide'),toggle=document.getElementById('quick_start_toggle'),key='tiga.ald.quick-start.v2';
848
  try{guide.open=window.localStorage.getItem(key)==='open';}catch{}
849
  const syncHelp=()=>toggle.setAttribute('aria-expanded',String(guide.open));syncHelp();
850
  guide.addEventListener('toggle',()=>{syncHelp();try{window.localStorage.setItem(key,guide.open?'open':'closed');}catch{}});
track-map.js CHANGED
@@ -423,7 +423,7 @@
423
  if(state){
424
  const frame=state.frame;blackbox(frame);const speed=Math.hypot(frame.vx,frame.vy)/15*3.6;
425
  const contacts=frame.collision_count===undefined?'unavailable':String(frame.collision_count);
426
- setText('replay_time_label',`${(cursor/60).toFixed(2)}s / ${(recording.requested_ticks/60).toFixed(2)}s · Tick ${Math.round(cursor)} · Recorded sample ${frame.tick}`);
427
  setText('replay_readout',`Recorded sample tick ${frame.tick} (${(frame.tick/60).toFixed(2)}s). Speed ${speed.toFixed(1)} km/h, derived from recorded velocity. Position (${frame.x.toFixed(2)}, ${frame.y.toFixed(2)}). Contacts ${contacts}. ${frame.active?'Active':'Inactive'} at this sample. ${frame.decision?'Native controls and inputs captured at tick '+frame.decision.tick+'.':'No captured decision at this sample.'}`);
428
  if(layers.hud){
429
  ctx.save();ctx.fillStyle='rgba(9,17,27,0.88)';ctx.beginPath();ctx.roundRect(12,12,310,80,8);ctx.fill();
 
423
  if(state){
424
  const frame=state.frame;blackbox(frame);const speed=Math.hypot(frame.vx,frame.vy)/15*3.6;
425
  const contacts=frame.collision_count===undefined?'unavailable':String(frame.collision_count);
426
+ setText('replay_time_label',`${(cursor/60).toFixed(2)}s / ${(recording.requested_ticks/60).toFixed(2)}s · Tick ${Math.round(cursor)} · Measured tick ${frame.tick}`);
427
  setText('replay_readout',`Recorded sample tick ${frame.tick} (${(frame.tick/60).toFixed(2)}s). Speed ${speed.toFixed(1)} km/h, derived from recorded velocity. Position (${frame.x.toFixed(2)}, ${frame.y.toFixed(2)}). Contacts ${contacts}. ${frame.active?'Active':'Inactive'} at this sample. ${frame.decision?'Native controls and inputs captured at tick '+frame.decision.tick+'.':'No captured decision at this sample.'}`);
428
  if(layers.hud){
429
  ctx.save();ctx.fillStyle='rgba(9,17,27,0.88)';ctx.beginPath();ctx.roundRect(12,12,310,80,8);ctx.fill();