infinicode 2.8.93 → 2.8.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -162,6 +162,8 @@ section h2 .count{color:var(--muted);}
162
162
  .fleet-attention{display:grid;gap:.36rem;max-height:154px;overflow:auto}.fleet-alert-row{display:grid;grid-template-columns:7px 1fr auto;gap:.48rem;align-items:start;padding:.36rem .42rem;border-radius:8px;background:rgba(255,255,255,.018);font-size:.61rem}.fleet-alert-row i{width:7px;height:7px;border-radius:50%;margin-top:.28rem;background:var(--warn)}.fleet-alert-row.bad i{background:var(--bad)}.fleet-alert-row.ok i{background:var(--ok)}.fleet-alert-row b{display:block;color:var(--ink);font-size:.62rem}.fleet-alert-row span{color:var(--muted);font-size:.56rem}.fleet-alert-row em{font-family:var(--mono);font-style:normal;font-size:.5rem;color:var(--muted);white-space:nowrap}
163
163
  .fleet-release{grid-column:1/-1;display:flex;align-items:center;gap:.55rem;flex-wrap:wrap;padding:.55rem .7rem}.fleet-release .version-pill{font-family:var(--mono);font-size:.56rem;border:1px solid var(--glass-border);border-radius:100px;padding:.18rem .5rem;color:var(--muted)}.fleet-release .version-pill.current{color:var(--ok);border-color:rgba(111,224,166,.28)}
164
164
  .node .fleet-card-actions{display:flex;gap:.35rem;flex-wrap:wrap;margin-top:.65rem;padding-top:.55rem;border-top:1px solid var(--glass-border)}
165
+ .fleet-live-camera{position:relative;aspect-ratio:16/9;margin:.55rem 0;border:1px solid var(--glass-border);border-radius:12px;overflow:hidden;background:radial-gradient(circle at 50% 35%,rgba(246,215,123,.08),transparent 44%),#050504;box-shadow:inset 0 1px 0 var(--glass-hi)}
166
+ .fleet-live-camera img{width:100%;height:100%;display:block;object-fit:cover;background:#050504}.fleet-live-camera .state{position:absolute;left:.5rem;bottom:.45rem;padding:.18rem .4rem;border-radius:100px;background:rgba(3,3,2,.78);border:1px solid var(--glass-border);font-family:var(--mono);font-size:.5rem;color:var(--warn);backdrop-filter:blur(8px)}.fleet-live-camera.live .state{color:var(--ok)}.fleet-live-camera.failed .state{color:var(--bad)}
165
167
  @media(max-width:1150px){.fleet-kpis{grid-template-columns:repeat(3,1fr)}.fleet-insights{grid-template-columns:1fr 1fr}.fleet-insights .fleet-panel:last-of-type{grid-column:1/-1}}
166
168
  @media(max-width:720px){.fleet-hero{align-items:flex-start;flex-direction:column}.fleet-hero-actions{justify-content:flex-start}.fleet-kpis{grid-template-columns:repeat(2,1fr)}.fleet-insights{display:block}.fleet-panel{margin-bottom:.65rem}.fleet-readiness{grid-template-columns:90px 1fr}.fleet-donut{width:86px;height:86px}}
167
169
 
@@ -1484,6 +1486,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
1484
1486
  }
1485
1487
  function renderFleet(nodes){
1486
1488
  var el = $('fleet');
1489
+ // Keep existing MJPEG elements alive while telemetry cards are rebuilt.
1490
+ // Replacing an <img> tears down its long-lived stream and made cameras
1491
+ // appear to reconnect on every fleet poll.
1492
+ var preservedCameras={};
1493
+ if(el){Array.prototype.forEach.call(el.querySelectorAll('[data-fleet-camera]'),function(img){var key=img.getAttribute('data-fleet-camera');if(key)preservedCameras[key]=img;});}
1487
1494
  renderFleetOperations(nodes||[],'fleet');
1488
1495
  if(!nodes || !nodes.length){ el.innerHTML = '<div class="empty">No nodes reporting yet. Start peers with <span class="mono">infinicode serve</span> or <span class="mono">robopark setup</span>.</div>'; return; }
1489
1496
  // connected first, then robots/satellites above hubs so the fleet reads robot-first
@@ -1510,13 +1517,18 @@ button.act:disabled{opacity:0.5;cursor:default;}
1510
1517
  var avatar=rpAvatarFor(tele&&(tele.character_name||tele.character_id)||n.displayName);
1511
1518
  var readiness=tele&&(tele.readiness_code||tele.status)||(!up?'offline':'mesh online');
1512
1519
  var hb=tele&&tele.heartbeat_age_seconds!=null?tele.heartbeat_age_seconds:null;
1513
- var site = n.site?(' · '+esc(n.site)):'';
1520
+ var site = n.site?(' · '+esc(n.site)):'';
1521
+ var cameraRef=n.displayName||n.name||n.nodeId||n.id;
1514
1522
 
1515
1523
  html += '<div class="node '+(up?'up':'down')+(selected===n.nodeId?' sel':'')+'" tabindex="0" role="button" data-id="'+esc(n.nodeId)+'" data-name="'+esc(n.displayName)+'">'
1516
1524
  + '<div class="top"><span class="name"><span class="statdot '+(up?'on':'off')+'"></span>'+esc(n.displayName)+(isSelf?' <span class="selfbadge">this</span>':'')+'</span><span class="role">'+esc(n.role)+'</span></div>'
1517
1525
  + '<div class="meta">'+esc(n.platform||'')+' '+esc((hw&&hw.cpu&&hw.cpu.model)?hw.cpu.model.replace(/\\s+/g,' ').slice(0,22):'')+site+' <span class="mono">v'+esc(n.version||'?')+'</span></div>'
1518
1526
  + (avatar?('<div class="node-hero"><img class="node-avatar" alt="'+esc(n.displayName)+'" src="'+esc(avatar+QS)+'"><div class="node-health"><b>'+esc(String(readiness).replace(/_/g,' '))+'</b><br>'
1519
1527
  +(tele?('sessions '+esc(tele.sessions_total||0)+' / triggers '+esc(tele.trigger_count||0)+'<br>latency '+esc(tele.avg_latency_ms!=null?Math.round(tele.avg_latency_ms)+'ms':'n/a')+' / drops '+esc(tele.drop_rate!=null?Math.round(tele.drop_rate*100)+'%':'n/a')+'<br>heartbeat '+esc(hb!=null?hb+'s ago':'unavailable')):'scheduler telemetry pending')+'</div></div>'):'');
1528
+
1529
+ if((tele||String(n.role||'').toLowerCase()==='satellite'||String(n.role||'').toLowerCase()==='robot')&&cameraRef){
1530
+ html += '<div class="fleet-live-camera" data-fleet-camera-host="'+esc(cameraRef)+'"><img alt="Live camera for '+esc(n.displayName)+'" data-fleet-camera="'+esc(cameraRef)+'"><span class="state">'+(up?'connecting camera':'robot offline')+'</span></div>';
1531
+ }
1520
1532
 
1521
1533
  if(up){
1522
1534
  html += mrow('CPU', cpu, cpu+'%'+(cores?(' /'+cores+'c'):''), loadClass(cpu))
@@ -1536,7 +1548,20 @@ button.act:disabled{opacity:0.5;cursor:default;}
1536
1548
  html += (capHtml?('<div class="caps">'+capHtml+'</div>'):'')
1537
1549
  + ((tele||String(n.role||'').toLowerCase()==='satellite')?('<div class="fleet-card-actions"><button class="rp-btn primary" data-fleet-action="camera" data-robot="'+esc(n.displayName)+'">Live camera</button><button class="rp-btn" data-fleet-action="sessions" data-robot="'+esc(n.displayName)+'">Sessions</button><button class="rp-btn" data-fleet-action="incidents" data-robot="'+esc(n.displayName)+'">Health</button>'+(tele&&(tele.character_id||tele.character_preset_id)?'<button class="rp-btn" data-config-edit-character="'+esc(tele.character_id||tele.character_preset_id)+'">Edit character</button>':'')+(tele&&tele.voice_stack_id?'<button class="rp-btn" data-config-edit-stack="'+esc(tele.voice_stack_id)+'">Edit voice</button>':'')+'</div>'):'')+'</div>';
1538
1550
  }
1539
- el.innerHTML = html;
1551
+ el.innerHTML = html;
1552
+ Array.prototype.forEach.call(el.querySelectorAll('[data-fleet-camera]'),function(placeholder){
1553
+ var key=placeholder.getAttribute('data-fleet-camera'),old=preservedCameras[key];
1554
+ if(old){placeholder.parentNode.replaceChild(old,placeholder);placeholder=old;}
1555
+ var host=placeholder.closest('[data-fleet-camera-host]'),state=host&&host.querySelector('.state');
1556
+ function connect(){
1557
+ if(!placeholder.isConnected)return;
1558
+ placeholder.src='/fed/media/robots/'+encodeURIComponent(key)+'/video_feed'+QS+(QS?'&':'?')+'_='+Date.now();
1559
+ }
1560
+ placeholder.onload=function(){if(host){host.classList.add('live');host.classList.remove('failed');}if(state)state.textContent='always-on live';};
1561
+ placeholder.onerror=function(){if(host){host.classList.remove('live');host.classList.add('failed');}if(state)state.textContent='reconnecting camera';placeholder.removeAttribute('src');setTimeout(connect,4000);};
1562
+ if(!placeholder.getAttribute('src'))connect();
1563
+ else if(host){host.classList.add('live');if(state)state.textContent='always-on live';}
1564
+ });
1540
1565
  var cards = el.querySelectorAll('.node');
1541
1566
  for(var j=0;j<cards.length;j++){
1542
1567
  (function(card){
@@ -6161,8 +6186,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
6161
6186
  }).catch(function(e){ var pm=$('pk-prodmode'); if(pm) pm.checked=!on; showOut('production mode ('+id+'): '+e.message); });
6162
6187
  }
6163
6188
  // ── on-demand operator preview: open the robot's cam without a scene-detection session ──
6164
- function updateGoLive(){ var b=$('pk-golive'); if(b){ b.textContent = previewOn?' Stop live':'▶ Go live'; b.classList.toggle('danger', previewOn); } }
6165
- function goLive(n){ if(previewOn) stopPreview(n); else startPreview(n); }
6189
+ function updateGoLive(){ var b=$('pk-golive'); if(b){ b.textContent=' Reconnect camera'; b.classList.remove('danger'); } }
6190
+ function goLive(n){ stopVision(); tryVisionOverlay(n,true); }
6166
6191
  function startPreview(n){
6167
6192
  previewOn=true; updateGoLive();
6168
6193
  if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
@@ -6189,9 +6214,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
6189
6214
  previewOn=false; updateGoLive();
6190
6215
  if(previewTimer){ clearInterval(previewTimer); previewTimer=null; }
6191
6216
  if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
6192
- stopVision();
6193
6217
  var directNote=$('pk-golivenote'); if(directNote) directNote.textContent='';
6194
- idleCam('idle');
6218
+ tryVisionOverlay(n,true);
6195
6219
  return;
6196
6220
  previewOn=false; updateGoLive();
6197
6221
  if(previewTimer){ clearInterval(previewTimer); previewTimer=null; }
@@ -6582,39 +6606,42 @@ button.act:disabled{opacity:0.5;cursor:default;}
6582
6606
  // Long-poll the result endpoint. The scheduler blocks up to 8s
6583
6607
  // before returning timed_out: true; we just respect that and
6584
6608
  // give the operator a clear "still waiting" line in the output.
6585
- function _shellWaitForResult(robotId, requestId, deviceId, service, label){
6609
+ function _shellWaitForResult(robotId, requestId, deviceId, service, label){
6586
6610
  var url = '/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/wait/'+encodeURIComponent(requestId)+'?timeout=8';
6587
6611
  var pollOnce = function(){
6588
6612
  return getJson(url).then(function(r){
6589
- if(r && r.completed){
6590
- var result = r.result || {};
6591
- _renderShellResult(robotId, result, label);
6592
- return;
6613
+ if(r && r.completed){
6614
+ var result = r.result || {};
6615
+ _renderShellResult(robotId, result, label);
6616
+ return result;
6593
6617
  }
6594
6618
  // Not done yet — fall through to manual polling
6595
6619
  throw new Error('not_ready');
6596
6620
  });
6597
6621
  };
6598
- return pollOnce().catch(function(){
6622
+ return pollOnce().catch(function(){
6599
6623
  // Manual poll loop (covers the case where /wait timed out but
6600
6624
  // the robot is still chewing). Cap at 12s of fallback polling.
6601
6625
  var deadline = Date.now() + 12000;
6602
- var tick = function(){
6603
- if(Date.now() > deadline){
6604
- _setShellStatus(robotId, '⚠ '+label+' did not complete in 20s — robot may be offline', 'error');
6605
- _shellAppend(robotId, 'status', '✗ '+label+' did not complete in 20s');
6606
- return;
6607
- }
6608
- getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/result/'+encodeURIComponent(requestId))
6609
- .then(function(r){
6610
- if(r && r.completed){ _renderShellResult(robotId, r.result || {}, label); return; }
6611
- setTimeout(tick, 500);
6612
- })
6613
- .catch(function(){ setTimeout(tick, 800); });
6614
- };
6615
- tick();
6616
- });
6617
- }
6626
+ return new Promise(function(resolve,reject){
6627
+ var tick = function(){
6628
+ if(Date.now() > deadline){
6629
+ _setShellStatus(robotId, ' '+label+' did not complete in 20s — robot may be offline', 'error');
6630
+ _shellAppend(robotId, 'status', '✗ '+label+' did not complete in 20s');
6631
+ reject(new Error(label+' did not complete in 20s'));
6632
+ return;
6633
+ }
6634
+ getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/result/'+encodeURIComponent(requestId))
6635
+ .then(function(r){
6636
+ if(r && r.completed){var result=r.result||{};_renderShellResult(robotId,result,label);resolve(result);return;}
6637
+ setTimeout(tick,500);
6638
+ })
6639
+ .catch(function(){setTimeout(tick,800);});
6640
+ };
6641
+ tick();
6642
+ });
6643
+ });
6644
+ }
6618
6645
  function _renderShellResult(robotId, result, label){
6619
6646
  if(!result || !result.ok){
6620
6647
  var err = (result && result.error) || '(no response)';
@@ -6688,11 +6715,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
6688
6715
  +'<img id="pk-vision-img" style="width:100%;height:100%;object-fit:cover;position:absolute;inset:0;display:none;border-radius:inherit" alt="vision feed"/>'
6689
6716
  +'<div class="hud"><div class="rec"><i></i>REC · '+esc(n.name)+'</div><div class="tr" id="pk-camres">— · —</div><div class="br" id="pk-camts">--:--:--</div></div></div>'
6690
6717
  +'<div class="pk-note" id="pk-camnote">seeking live feed…</div>'
6691
- +'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-golive" data-robot="'+esc(n.name)+'">▶ Go live</button><button class="rp-btn primary" id="pk-camera-fullscreen">Fullscreen operations view</button><span class="pk-note" id="pk-golivenote" style="margin:0"></span></div>'
6718
+ +'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-golive" data-robot="'+esc(n.name)+'">↻ Reconnect camera</button><button class="rp-btn primary" id="pk-camera-fullscreen">Fullscreen operations view</button><span class="pk-note" id="pk-golivenote" style="margin:0">always-on RoboVision relay</span></div>'
6692
6719
  +'<label class="pk-prodtoggle"><input type="checkbox" id="pk-prodmode"'+(devForPm.production_mode?' checked':'')+'><span>Production mode — continuous motion-triggered loop, camera always live</span></label>')
6693
6720
  + sect('Remote production test','<div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
6694
- + '<div class="pk-ctl" style="margin-top:0.55rem"><button class="rp-btn primary" id="pk-pipeline-start">Start full test</button><button class="rp-btn danger" id="pk-pipeline-stop">Stop conversation</button></div>'
6695
- + '<div class="pk-note">Starts the camera, sends the same motion trigger used in production, and follows every reported stage remotely.</div>'
6721
+ + '<div class="pk-ctl" style="margin-top:0.55rem"><button class="rp-btn primary" id="pk-unattended-test">Run unattended readiness</button><button class="rp-btn" id="pk-pipeline-start">Start full test</button><button class="rp-btn danger" id="pk-pipeline-stop">Stop conversation</button></div>'
6722
+ + '<div class="pk-note" id="pk-unattended-result">Unattended readiness runs an acoustic speaker/mic round-trip, triggers the production motion path, and requires greeting playback on the robot.</div>'
6696
6723
  + '<div class="pk-pipeline" id="pk-pipeline"></div>')
6697
6724
  + sect('🎙 microphone','<div class="pk-meter"><span class="lab">input</span><div class="pk-bars" id="pk-micbars"></div><span class="pk-val" id="pk-micval">—</span></div><div class="pk-note" id="pk-micstate">awaiting robot audio plugin</div><div class="pk-ctl" style="margin-top:.5rem"><button class="rp-btn" id="pk-mic-listen">Listen in browser</button><label class="pk-note" style="display:flex;align-items:center;gap:.35rem;margin:0">volume <input id="pk-mic-volume" type="range" min="0" max="100" value="80" style="width:90px"></label></div><div class="pk-note" id="pk-mic-listen-note">Muted by default. Use headphones to prevent acoustic feedback.</div>')
6698
6725
  + sect('🔊 speaker','<canvas class="pk-wave" id="pk-spkwave" width="360" height="32"></canvas><div class="pk-meter"><span class="lab">output</span><div class="pk-bars" id="pk-spkbars"></div><span class="pk-val" id="pk-spkval">—</span></div><div class="pk-note" id="pk-spkstate">TTS plays on the robot speaker — use “Test speaker” below to round-trip a tone and confirm the audio path</div>'
@@ -6748,9 +6775,6 @@ button.act:disabled{opacity:0.5;cursor:default;}
6748
6775
  var gl=$('pk-golive');
6749
6776
  if(gl){
6750
6777
  gl.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } goLive(n); });
6751
- var vl=document.createElement('button'); vl.className='rp-btn'; vl.id='pk-vision-live'; vl.textContent='Open RoboVision feed';
6752
- vl.addEventListener('click', function(){ tryVisionOverlay(n, true); if(!visionOn) showOut('RoboVision feed unavailable: robot has no reachable LAN/Tailscale address'); });
6753
- gl.after(vl);
6754
6778
  }
6755
6779
  var fullCam=$('pk-camera-fullscreen'); if(fullCam) fullCam.addEventListener('click',function(){ openCameraOperations(n); });
6756
6780
  var editCharacter=$('pk-edit-character'); if(editCharacter) editCharacter.addEventListener('click',function(){var id=$('pk-eff-char').value;if(id)openCharacterManager(id);else showOut('Choose a character to edit');});
@@ -6766,6 +6790,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6766
6790
  var eb=$('pk-end'); if(eb) eb.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } endSession(schedId(n)); });
6767
6791
  var rb=$('pk-recover'); if(rb) rb.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } recoverRobot(schedId(n)); });
6768
6792
  var pipelineStart=$('pk-pipeline-start'); if(pipelineStart) pipelineStart.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } startPipelineTest(n); });
6793
+ var unattended=$('pk-unattended-test'); if(unattended) unattended.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runUnattendedReadiness(n); });
6769
6794
  var pipelineStop=$('pk-pipeline-stop'); if(pipelineStop) pipelineStop.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } stopPipelineTest(n); });
6770
6795
  pollPipeline(n);
6771
6796
  if(pipelineTimer) clearInterval(pipelineTimer);
@@ -6842,6 +6867,53 @@ button.act:disabled{opacity:0.5;cursor:default;}
6842
6867
  showOut('conversation stopped for '+(n.label||n.name)); pollPipeline(n); startCam(n);
6843
6868
  }).catch(function(e){ showOut('stop test: '+e.message); });
6844
6869
  }
6870
+ function runUnattendedReadiness(n){
6871
+ var button=$('pk-unattended-test'),resultEl=$('pk-unattended-result'),robotId=schedId(n),device=schedulerDevice(n)||{};
6872
+ if(!device.id){showOut('Robot has no scheduler device identity');return;}
6873
+ if(button){button.disabled=true;button.textContent='Testing hardware...';}
6874
+ function report(text,bad){if(resultEl){resultEl.textContent=text;resultEl.style.color=bad?'var(--bad)':'var(--warn)';}}
6875
+ function delay(ms){return new Promise(function(resolve){setTimeout(resolve,ms);});}
6876
+ function status(){return getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/pipeline-status'+QS);}
6877
+ function waitForGreeting(deadline){
6878
+ return status().then(function(data){
6879
+ renderPipeline(data);
6880
+ var sid=data.latest_session&&data.latest_session.id,seen={};
6881
+ (data.events||[]).forEach(function(ev){if(!ev.session_id||!sid||ev.session_id===sid)seen[ev.stage]=ev;});
6882
+ var required=['scheduler_session','livekit_join','camera_published','microphone_published','tts_subscribed','playback_started'];
6883
+ var missing=required.filter(function(stage){return !seen[stage]||seen[stage].status!=='ok';});
6884
+ if(!missing.length)return {data:data,required:required};
6885
+ if(Date.now()>deadline)throw new Error('production greeting timed out; missing '+missing.join(', '));
6886
+ report('Pipeline running · waiting for '+missing.join(', '));
6887
+ return delay(1500).then(function(){return waitForGreeting(deadline);});
6888
+ });
6889
+ }
6890
+ var inv=device.device_inventory||{},media=inv.media_health||{};
6891
+ var cameraProblems=[];
6892
+ if(media.camera_access===false)cameraProblems.push('camera permission denied');
6893
+ if(media.camera_stalled===true)cameraProblems.push('camera worker stalled');
6894
+ if(media.camera_frame_age_seconds!=null&&Number(media.camera_frame_age_seconds)>15)cameraProblems.push('camera frame '+Math.round(Number(media.camera_frame_age_seconds))+'s old');
6895
+ if(cameraProblems.length){report('FAILED · '+cameraProblems.join(' · '),true);if(button){button.disabled=false;button.textContent='Run unattended readiness';}return;}
6896
+ report('Camera healthy · running physical speaker/mic round-trip');
6897
+ rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',{
6898
+ mode:'tone',playback_only:false,frequency:1000,duration:0.8,amplitude:0.6,threshold_db:-30,
6899
+ output:'1',input:'2',output_name:'USB Audio Device: -',input_name:'Usb Audio Device: USB Audio'
6900
+ }).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','speaker-test');})
6901
+ .then(function(acoustic){
6902
+ if(!acoustic||!acoustic.ok||acoustic.pass!==true)throw new Error((acoustic&&acoustic.error)||(acoustic&&acoustic.diagnostic)||'speaker/mic acoustic round-trip failed');
6903
+ report('Acoustic path passed · starting production motion and greeting');
6904
+ if(button)button.textContent='Testing production loop...';
6905
+ var enable=device.production_mode?Promise.resolve():rpAction('PATCH','/robopark/api/devices/'+encodeURIComponent(device.id),{production_mode:true}).then(function(){device.production_mode=true;});
6906
+ return enable.then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/pipeline-test/start',{});});
6907
+ })
6908
+ .then(function(queued){if(!queued||!queued.queued)throw new Error(queued&&queued.reason||'production trigger was not queued');return waitForGreeting(Date.now()+45000);})
6909
+ .then(function(done){
6910
+ report('PASSED · camera fresh · acoustic speaker/mic path confirmed · motion → scheduler → LiveKit → mic/camera → TTS → robot playback confirmed');
6911
+ if(resultEl)resultEl.style.color='var(--ok)';
6912
+ showOut('unattended production readiness passed for '+(n.label||n.name));
6913
+ })
6914
+ .catch(function(e){report('FAILED · '+e.message,true);showOut('unattended readiness: '+e.message);})
6915
+ .then(function(){if(button){button.disabled=false;button.textContent='Run unattended readiness';}});
6916
+ }
6845
6917
  function refreshDrawer(){
6846
6918
  var n=drawerId?byId[drawerId]:null; if(!n) return;
6847
6919
  if(n.kind==='robot' && !deviceSelectorsReady && $('pk-video-device')){
@@ -142,3 +142,11 @@ Required result: `playback_started ok`.
142
142
  status and recycles media after three consecutive unhealthy checks.
143
143
  - Park media operations display camera/audio permission, camera worker state,
144
144
  and latest frame age from `device_inventory.media_health`.
145
+ - Camera visibility is independent of conversation sessions. Fleet cards and
146
+ the robot drawer use the RoboVision mesh MJPEG relay continuously and keep
147
+ stream elements alive across telemetry rerenders.
148
+ - The dashboard unattended readiness test requires camera health, an acoustic
149
+ speaker-to-microphone round-trip, scheduler allocation, LiveKit join, camera
150
+ and microphone publication, TTS subscription, and first robot playback.
151
+ - An unattended readiness pass does not claim an STT/LLM turn occurred. That
152
+ semantic turn still requires injected speech or a person near the robot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.93",
3
+ "version": "2.8.94",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",