infinicode 2.8.102 → 2.8.103

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.
@@ -26,7 +26,7 @@ export const DASHBOARD_HTML = `<!doctype html>
26
26
  <head>
27
27
  <meta charset="utf-8">
28
28
  <meta name="viewport" content="width=device-width, initial-scale=1">
29
- <meta name="robopark-ui-build" content="voice-media-20260716-3">
29
+ <meta name="robopark-ui-build" content="provisioning-token-20260717-1">
30
30
  <title>RoboPark Control</title>
31
31
  <style>
32
32
  /* ── premium tech glass theme ── dark is the committed default; light is a frosted toggle ── */
@@ -363,20 +363,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
363
363
  .pk-ctl{display:flex;gap:0.45rem;flex-wrap:wrap;}
364
364
  .pk-note{font-family:var(--mono);font-size:0.58rem;color:var(--gold);opacity:0.85;margin-top:0.4rem;}
365
365
  .pk-pipeline{display:grid;gap:0.42rem;margin-top:0.7rem;position:relative;}
366
+ .pk-live-stage-focus{--stage-color:var(--gold-bright);position:relative;overflow:hidden;margin-top:.58rem;padding:.78rem .85rem .78rem 1rem;border:1px solid var(--stage-color);border-radius:13px;background:linear-gradient(105deg,color-mix(in srgb,var(--stage-color) 24%,transparent),rgba(8,8,8,.68));box-shadow:0 10px 30px rgba(0,0,0,.28),inset 4px 0 0 var(--stage-color);}
367
+ .pk-live-stage-focus:before{content:'';position:absolute;width:150px;height:150px;right:-65px;top:-95px;border-radius:50%;background:var(--stage-color);opacity:.12;filter:blur(5px);animation:pulse 1.3s infinite;}.pk-live-stage-focus .eyebrow{display:block;font-family:var(--mono);font-size:.49rem;letter-spacing:.13em;text-transform:uppercase;color:var(--stage-color);}.pk-live-stage-focus b{display:block;margin-top:.2rem;font-size:1rem;letter-spacing:.015em;color:var(--ink);}.pk-live-stage-focus span:last-child{display:block;margin-top:.18rem;font-family:var(--mono);font-size:.57rem;color:var(--muted);}.pk-live-stage-focus.blocked,.pk-live-stage-focus.failed{--stage-color:var(--bad)}.pk-live-stage-focus.waiting,.pk-live-stage-focus.paused{--stage-color:var(--muted)}.pk-live-stage-focus.active,.pk-live-stage-focus.running{--stage-color:var(--warn)}
366
368
  .pk-pipe-step{--phase:var(--gold);display:grid;grid-template-columns:2rem minmax(7.2rem,.72fr) minmax(9rem,1.35fr);gap:0.55rem;align-items:center;padding:0.55rem 0.62rem;border-radius:11px;border:1px solid rgba(255,255,255,.06);background:linear-gradient(110deg,color-mix(in srgb,var(--phase) 7%,transparent),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.58rem;color:var(--muted);transition:transform .18s,border-color .18s,background .18s;position:relative;overflow:hidden;}
367
369
  .pk-pipe-step.phase-hardware{--phase:#63d5c6}.pk-pipe-step.phase-trigger{--phase:#efb94c}.pk-pipe-step.phase-session{--phase:#66a8ff}.pk-pipe-step.phase-media{--phase:#68d48f}.pk-pipe-step.phase-intelligence{--phase:#e38bff}.pk-pipe-step.phase-playback{--phase:#ff9b62}.pk-pipe-step.phase-close{--phase:#a8b0bd}
368
370
  .pk-pipe-step .step-index{display:grid;place-items:center;width:1.65rem;height:1.65rem;border-radius:8px;border:1px solid color-mix(in srgb,var(--phase) 35%,transparent);color:var(--phase);background:color-mix(in srgb,var(--phase) 10%,transparent);font-weight:700;}
369
371
  .pk-pipe-step .stage{color:var(--text);text-transform:uppercase;letter-spacing:.055em;font-weight:650;display:block;}.pk-pipe-step .phase{display:block;color:var(--phase);font-size:.49rem;letter-spacing:.08em;margin-top:.14rem;}
370
- .pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}
372
+ .pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}.pk-pipe-step .step-state{display:inline-block;margin-left:.35rem;padding:.1rem .3rem;border:1px solid currentColor;border-radius:999px;font-size:.43rem;letter-spacing:.07em;vertical-align:1px;color:var(--muted)}
371
373
  .pk-pipe-step.ok{border-color:color-mix(in srgb,var(--ok) 22%,transparent)}
374
+ .pk-pipe-step.ok .step-state{color:var(--ok)}.pk-pipe-step.pending{opacity:.52}.pk-pipe-step.pending .step-index{filter:saturate(.3)}
372
375
  .pk-pipe-step.failed,.pk-pipe-step.blocked{border-color:rgba(255,90,90,.48);background:linear-gradient(110deg,rgba(255,70,70,.14),rgba(255,255,255,.02));}
373
- .pk-pipe-step.current{transform:translateX(4px);border-color:var(--phase);background:linear-gradient(105deg,color-mix(in srgb,var(--phase) 19%,transparent),rgba(255,255,255,.035));box-shadow:0 0 0 1px color-mix(in srgb,var(--phase) 18%,transparent),0 8px 28px rgba(0,0,0,.22);}
376
+ .pk-pipe-step.current{transform:translateX(5px) scale(1.012);border-color:var(--phase);background:linear-gradient(105deg,color-mix(in srgb,var(--phase) 24%,transparent),rgba(255,255,255,.045));box-shadow:0 0 0 1px color-mix(in srgb,var(--phase) 28%,transparent),0 10px 32px rgba(0,0,0,.3);opacity:1;z-index:2;}
374
377
  .pk-pipe-step.current:after{content:'ACTIVE';position:absolute;right:-1.7rem;top:.45rem;transform:rotate(38deg);padding:.12rem 2rem;background:var(--phase);color:#080808;font-size:.42rem;font-weight:800;letter-spacing:.08em;}
375
378
  .pk-pipe-step.current .step-index{box-shadow:0 0 14px color-mix(in srgb,var(--phase) 55%,transparent);animation:pulse 1.15s infinite;}
376
379
  .pk-pipeline-summary{padding:0.7rem 0.75rem;border:1px solid var(--glass-border);border-radius:11px;background:linear-gradient(120deg,rgba(212,170,72,.08),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.62rem;color:var(--muted);line-height:1.45;}
377
380
  .pk-pipeline-summary.ready{color:var(--ok);border-color:rgba(70,220,150,.35);}
378
381
  .pk-pipeline-summary.running{color:var(--warn);border-color:rgba(255,196,70,.35);}
379
382
  .pk-pipeline-summary.blocked{color:var(--bad);border-color:rgba(255,90,90,.4);}
383
+ .fleet-stage-focus{margin-top:.48rem;padding:.48rem .58rem;border:1px solid color-mix(in srgb,var(--warn) 48%,transparent);border-radius:9px;background:linear-gradient(100deg,rgba(255,184,62,.14),rgba(255,255,255,.02));font-family:var(--mono)}.fleet-stage-focus span{display:block;color:var(--warn);font-size:.45rem;letter-spacing:.1em;text-transform:uppercase}.fleet-stage-focus b{display:block;margin-top:.12rem;color:var(--ink);font-size:.66rem}.fleet-stage-focus.blocked,.fleet-stage-focus.failed{border-color:color-mix(in srgb,var(--bad) 55%,transparent)}.fleet-stage-focus.blocked span,.fleet-stage-focus.failed span{color:var(--bad)}.fleet-stage-focus.waiting,.fleet-stage-focus.paused{border-color:var(--glass-border)}.fleet-stage-focus.waiting span,.fleet-stage-focus.paused span{color:var(--muted)}
380
384
  .pk-pipe-history{margin-top:.75rem;border-top:1px solid var(--glass-border);padding-top:.7rem}.pk-pipe-history-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;margin-bottom:.5rem}.pk-pipe-history-head b{font-family:var(--mono);font-size:.65rem;color:var(--ink);text-transform:uppercase;letter-spacing:.06em}.pk-pipe-history-list{display:grid;gap:.45rem}.pk-run-card{border:1px solid var(--glass-border);border-radius:10px;background:var(--panel-2);padding:.55rem .62rem;cursor:pointer}.pk-run-card:hover{border-color:var(--gold)}.pk-run-top{display:grid;grid-template-columns:1fr auto;gap:.5rem;align-items:start}.pk-run-title{font-family:var(--mono);font-size:.61rem;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pk-run-meta{font-family:var(--mono);font-size:.53rem;color:var(--muted);margin-top:.17rem}.pk-run-state{font-family:var(--mono);font-size:.5rem;text-transform:uppercase;border:1px solid var(--glass-border);border-radius:999px;padding:.18rem .4rem}.pk-run-state.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent)}.pk-run-state.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-run-state.running{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}.pk-run-stages{display:flex;gap:3px;margin-top:.45rem}.pk-run-stages i{height:4px;flex:1;border-radius:3px;background:rgba(255,255,255,.07)}.pk-run-stages i.ok{background:var(--ok)}.pk-run-stages i.failed,.pk-run-stages i.blocked{background:var(--bad)}.pk-history-pager{display:flex;align-items:center;gap:.35rem}.pk-history-pager span{font-family:var(--mono);font-size:.54rem;color:var(--muted)}
381
385
  .pk-mic-suite{margin-top:.6rem;display:grid;gap:.55rem}.pk-mic-health{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem}.pk-mic-check{padding:.48rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-mic-check .k{font-family:var(--mono);font-size:.49rem;color:var(--muted);text-transform:uppercase}.pk-mic-check .v{font-family:var(--mono);font-size:.62rem;color:var(--ink);margin-top:.16rem;overflow-wrap:anywhere}.pk-mic-check.ok{border-color:color-mix(in srgb,var(--ok) 30%,transparent)}.pk-mic-check.bad{border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-mic-result{border:1px solid var(--glass-border);border-radius:10px;padding:.58rem;background:var(--panel-2);font-family:var(--mono);font-size:.58rem}.pk-mic-result.ok{border-color:var(--ok);background:rgba(60,180,110,.08)}.pk-mic-result.bad{border-color:var(--bad);background:rgba(220,80,80,.08)}.pk-mic-readings{display:grid;grid-template-columns:repeat(4,1fr);gap:.35rem;margin-top:.45rem}.pk-mic-reading{padding:.35rem;background:rgba(0,0,0,.16);border-radius:7px}.pk-mic-reading span{display:block;color:var(--muted);font-size:.48rem;text-transform:uppercase}.pk-mic-reading b{display:block;color:var(--ink);font-size:.67rem;margin-top:.1rem}
382
386
  .pk-motor-registry{display:grid;gap:.4rem}.pk-motor-row{display:grid;grid-template-columns:1fr 4.5rem 5.5rem auto;gap:.35rem;align-items:end;padding:.45rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-motor-canvas{display:flex;align-items:stretch;gap:.55rem;overflow-x:auto;padding:.65rem .2rem;min-height:130px}.pk-motor-node{min-width:165px;padding:.55rem;border:1px solid rgba(255,205,93,.35);border-radius:12px;background:linear-gradient(145deg,rgba(201,150,46,.13),rgba(255,255,255,.025));position:relative}.pk-motor-node:not(:last-child):after{content:'';position:absolute;right:-.56rem;top:50%;width:.56rem;border-top:2px solid var(--gold)}.pk-motor-node .node-num{font-family:var(--mono);font-size:.48rem;color:var(--gold);text-transform:uppercase;margin-bottom:.35rem}.pk-motor-node .rp-field{margin-bottom:.3rem}.pk-motor-empty{display:grid;place-items:center;width:100%;min-height:100px;border:1px dashed var(--glass-border);border-radius:10px;color:var(--muted);font-family:var(--mono);font-size:.58rem}.pk-motor-status{font-family:var(--mono);font-size:.58rem;padding:.5rem;border-radius:8px;background:var(--panel-2);border:1px solid var(--glass-border)}.pk-motor-status.ok{color:var(--ok);border-color:var(--ok)}.pk-motor-status.bad{color:var(--bad);border-color:var(--bad)}
@@ -1126,18 +1130,21 @@ button.act:disabled{opacity:0.5;cursor:default;}
1126
1130
  <div class="rp-form" id="rp-addrobot">
1127
1131
  <h3>Add robot (mint enrollment token)</h3>
1128
1132
  <div class="rp-quickrow">
1129
- <div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
1130
- <div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
1131
- <button class="rp-btn primary" id="rb-add" style="height:fit-content">Add robot</button>
1133
+ <div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
1134
+ <div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
1135
+ <div class="rp-field"><label>Robot network</label><select id="rb-network"><option value="auto">Auto from hub address</option><option value="lan">LAN</option><option value="tailscale">Tailscale</option></select></div>
1136
+ <div class="rp-field"><label>Mesh token</label><input id="rb-token" spellcheck="false" autocomplete="off" placeholder="required in generated command"></div>
1137
+ <button class="rp-btn primary" id="rb-add" style="height:fit-content">Add robot</button>
1132
1138
  </div>
1133
1139
  <div class="rp-autonote" id="rb-autonote">the robot self-reports its IP and hardware on first heartbeat — you only need a name and (optionally) a character preset to get started.</div>
1134
1140
  <div class="rp-quickrow" id="rb-clicmd-row" style="display:none;">
1135
- <div class="rp-field" style="flex:1;"><label>run this ON THE ROBOT (no need to click Add — this mints its own token)</label><input id="rb-clicmd" readonly style="cursor:text;"></div>
1141
+ <div class="rp-field" style="flex:1;"><label>complete production one-liner — run this ON THE ROBOT</label><input id="rb-clicmd" readonly style="cursor:text;"></div>
1136
1142
  <button class="rp-btn" id="rb-clicmd-copy" style="height:fit-content">📋 Copy</button>
1137
1143
  </div>
1138
1144
  <details class="rp-advanced">
1139
- <summary>Advanced — pre-set network/voice config before the robot connects</summary>
1140
- <div class="rp-advanced-grid">
1145
+ <summary>Advanced — pre-set network/voice config before the robot connects</summary>
1146
+ <div class="rp-advanced-grid">
1147
+ <div class="rp-field"><label>Site</label><input id="rb-site" placeholder="Tel Aviv"></div>
1141
1148
  <div class="rp-field"><label>Tailscale IP</label><input id="rb-tsip" placeholder="auto-detected once connected"></div>
1142
1149
  <div class="rp-field"><label>LAN IP</label><input id="rb-lanip" placeholder="auto-detected once connected"></div>
1143
1150
  <div class="rp-field"><label>Motor server URL</label><input id="rb-motor" placeholder="http://...:5000"></div>
@@ -1241,6 +1248,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
1241
1248
  (function(){
1242
1249
  var TOKEN = new URLSearchParams(location.search).get('token') || '';
1243
1250
  var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';
1251
+ function dashboardMeshToken(){
1252
+ if(TOKEN)return TOKEN;
1253
+ try{var saved=JSON.parse(localStorage.getItem('rp-command-env-v1')||'null');return saved&&String(saved.token||'').trim()||'';}catch(e){return '';}
1254
+ }
1244
1255
  var UI_BUILD=(document.querySelector('meta[name="robopark-ui-build"]')||{}).content||'unknown';
1245
1256
  fetch('/fed/ui-build'+QS,{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(info){
1246
1257
  if(!info||!info.build||info.build===UI_BUILD)return;
@@ -1532,6 +1543,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1532
1543
  + '<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>'
1533
1544
  + (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>'
1534
1545
  +(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>'):'');
1546
+ if(tele){var fleetStage=tele.current_stage||{},fleetStageStatus=fleetStage.status||tele.current_stage_status||'waiting',fleetStageLabel=fleetStage.label||tele.current_stage_label||'Waiting for pipeline telemetry';html+='<div class="fleet-stage-focus '+esc(fleetStageStatus)+'"><span>Live production stage</span><b>'+esc(fleetStageLabel)+'</b></div>';}
1535
1547
 
1536
1548
  if((tele||String(n.role||'').toLowerCase()==='satellite'||String(n.role||'').toLowerCase()==='robot')&&cameraRef){
1537
1549
  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>';
@@ -2248,16 +2260,20 @@ button.act:disabled{opacity:0.5;cursor:default;}
2248
2260
 
2249
2261
  // ── robust CLI one-liner generation, live as you type — no API call
2250
2262
  // needed to see it; 'robopark add-robot' mints its own token when run ──
2251
- function buildAddRobotCommand(name, characterId){
2252
- if(!name) return '';
2253
- var hubUrl=location.origin;
2254
- var schedulerUrl=(lastStatus && lastStatus.control && lastStatus.control.schedulerUrl) || '';
2255
- var argv=['robopark','add-robot',name];
2256
- if(characterId) argv.push('--character', characterId);
2257
- if(hubUrl) argv.push('--hub-url', hubUrl);
2258
- if(TOKEN) argv.push('--token', TOKEN);
2259
- if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
2260
- argv.push('--start');
2263
+ function buildAddRobotCommand(name, characterId){
2264
+ if(!name) return '';
2265
+ var hubUrl=location.origin.replace(/\\/+$/,'');
2266
+ var schedulerUrl=hubUrl+'/robopark';
2267
+ var requestedNetwork=$('rb-network')&&$('rb-network').value||'auto';
2268
+ var network=requestedNetwork==='auto'?(/^100\\./.test(location.hostname)||/\\.ts\\.net$/i.test(location.hostname)?'tailscale':'lan'):requestedNetwork;
2269
+ var argv=['robopark','add-robot',name];
2270
+ if(characterId) argv.push('--character', characterId);
2271
+ var site=$('rb-site')&&$('rb-site').value.trim();if(site)argv.push('--site',site);
2272
+ if(hubUrl) argv.push('--hub-url', hubUrl);
2273
+ var meshToken=$('rb-token')&&$('rb-token').value.trim()||dashboardMeshToken()||'<mesh-token>';
2274
+ argv.push('--token', meshToken);
2275
+ if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
2276
+ argv.push(network==='tailscale'?'--tailscale':'--lan','--start','--auto-start','--yes');
2261
2277
  return argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' ');
2262
2278
  }
2263
2279
  function refreshRobotCliCommand(){
@@ -2267,8 +2283,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
2267
2283
  inp.value=buildAddRobotCommand(name, cid);
2268
2284
  row.style.display='flex';
2269
2285
  }
2270
- var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
2271
- var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
2286
+ var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
2287
+ var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
2288
+ var rbNetworkEl=$('rb-network'); if(rbNetworkEl) rbNetworkEl.addEventListener('change', refreshRobotCliCommand);
2289
+ var rbSiteEl=$('rb-site'); if(rbSiteEl) rbSiteEl.addEventListener('input', refreshRobotCliCommand);
2290
+ var rbTokenEl=$('rb-token'); if(rbTokenEl){ rbTokenEl.value=dashboardMeshToken(); rbTokenEl.addEventListener('input', refreshRobotCliCommand); }
2272
2291
  var rbCliCopy=$('rb-clicmd-copy'); if(rbCliCopy) rbCliCopy.addEventListener('click', function(){ var i=$('rb-clicmd'); i.select(); try{document.execCommand('copy');}catch(e){} showOut('command copied — run it on the robot itself'); });
2273
2292
 
2274
2293
  function addRobot(){
@@ -2653,7 +2672,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2653
2672
  var RP_COMMAND_ENV_KEY='rp-command-env-v1';
2654
2673
  var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
2655
2674
  var rpVisibleCommands=[];
2656
- function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:TOKEN};}
2675
+ function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:dashboardMeshToken()};}
2657
2676
  function rpLoadStoredJson(key,fallback){try{var value=JSON.parse(localStorage.getItem(key)||'null');return value&&typeof value==='object'?value:fallback;}catch(e){return fallback;}}
2658
2677
  function rpCommandEnv(){
2659
2678
  var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
@@ -2705,7 +2724,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2705
2724
  }
2706
2725
  function initCommandSheet(){
2707
2726
  var fields=['lan','tail','site','robot','enrollment','token'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2708
- fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=saved[name]!=null?saved[name]:defaults[name];input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
2727
+ fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=name==='token'&&TOKEN?TOKEN:(saved[name]!=null?saved[name]:defaults[name]);input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
2709
2728
  var search=$('rp-command-search'),category=$('rp-command-category'),grid=$('rp-command-grid'),reset=$('rp-command-reset'),copyAll=$('rp-command-copy-all'),form=$('rp-command-custom-form');
2710
2729
  if(search)search.addEventListener('input',renderCommandSheet);if(category)category.addEventListener('change',renderCommandSheet);
2711
2730
  if(reset)reset.addEventListener('click',function(){try{localStorage.removeItem(RP_COMMAND_ENV_KEY);}catch(e){}fields.forEach(function(name){var input=$('rp-command-'+name);if(input)input.value=defaults[name];});renderCommandSheet();showOut('command environment reset');});
@@ -6517,13 +6536,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
6517
6536
  // cars that arrived at the boundary together don't land on
6518
6537
  // top of each other.
6519
6538
  if(car.dir > 0 && car.x > PARK_W + 0.5){
6539
+ // Outbound (right-bound) car reached the right edge —
6540
+ // re-enter on the left. We swap to the OTHER lane so two
6541
+ // lanes of traffic are visible at all times; the wrap dir
6542
+ // is explicitly set to +1 so the car continues in its
6543
+ // original direction on the new lane. (Previous version
6544
+ // computed dir from the *post-wrap* y, which always
6545
+ // flipped the car to the opposite direction — cars would
6546
+ // oscillate at the road edge instead of crossing it.)
6520
6547
  car.x = -0.5 - Math.random() * (PARK_W * 0.5);
6521
6548
  car.y = (car.y < -0.9) ? -0.65 : -1.18;
6522
- car.dir = (car.y < -0.9) ? +1 : -1;
6549
+ car.dir = +1;
6523
6550
  } else if(car.dir < 0 && car.x < -0.5){
6551
+ // Inbound (left-bound) car reached the left edge —
6552
+ // re-enter on the right, swap to the other lane, keep
6553
+ // direction at -1. (See note above about the prior bug.)
6524
6554
  car.x = PARK_W + 0.5 + Math.random() * (PARK_W * 0.5);
6525
6555
  car.y = (car.y < -0.9) ? -0.65 : -1.18;
6526
- car.dir = (car.y < -0.9) ? +1 : -1;
6556
+ car.dir = -1;
6527
6557
  }
6528
6558
  }
6529
6559
  }
@@ -7429,7 +7459,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7429
7459
  +'<div class="pk-note" id="pk-camnote">seeking live feed…</div>'
7430
7460
  +'<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>'
7431
7461
  +'<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>')
7432
- + sect('Remote production test','<div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
7462
+ + sect('Remote production test','<div id="pk-live-stage" class="pk-live-stage-focus waiting"><span class="eyebrow">Current production stage</span><b>Checking robot state...</b><span>Waiting for scheduler telemetry</span></div><div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
7433
7463
  + '<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>'
7434
7464
  + '<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>'
7435
7465
  + '<div class="pk-pipeline" id="pk-pipeline"></div>')
@@ -7621,7 +7651,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7621
7651
  return stages[Math.min(stages.length-1,lastOk+1)];
7622
7652
  }
7623
7653
  function renderPipeline(data){
7624
- var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'); if(!host||!summary) return;
7654
+ var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'),focus=$('pk-live-stage'); if(!host||!summary) return;
7625
7655
  var latest={}, prereqs=data.prerequisites||{}, session=data.latest_session||{}, sid=session.id||null;
7626
7656
  (data.events||[]).forEach(function(ev){
7627
7657
  if(ev.session_id && sid && ev.session_id!==sid) return;
@@ -7630,15 +7660,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
7630
7660
  ['robot_online','camera_ready','microphone_ready','speaker_ready'].forEach(function(stage){
7631
7661
  if(!latest[stage]) latest[stage]={stage:stage,status:prereqs[stage]?'ok':'blocked',message:prereqs[stage]?'Ready':'Not reported'};
7632
7662
  });
7633
- var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),current=pipelineCurrentStage(stages,latest,running);
7663
+ var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),authoritative=data.current_stage||{},current=authoritative.stage||pipelineCurrentStage(stages,latest,running);
7634
7664
  host.innerHTML=stages.map(function(stage,index){
7635
- var ev=latest[stage],isCurrent=stage===current,status=ev&&ev.status||(isCurrent?'running':'pending');
7636
- var detail=ev&&(ev.message||ev.timestamp)||'waiting';
7665
+ var ev=latest[stage],isCurrent=stage===current,status=(isCurrent&&authoritative.status)||ev&&ev.status||(isCurrent?'running':'pending');
7666
+ var detail=isCurrent&&(authoritative.message||authoritative.label)||ev&&(ev.message||ev.timestamp)||'waiting';
7637
7667
  var time=ev&&ev.timestamp?new Date(ev.timestamp+'Z').toLocaleString():'';
7638
7668
  var phase=PIPELINE_PHASES[stage]||'session';
7639
- return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'</span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
7669
+ return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"'+(isCurrent?' aria-current="step"':'')+'><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'<em class="step-state">'+esc(isCurrent?'ACTIVE':status)+'</em></span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
7640
7670
  }).join('');
7641
7671
  var blockers=data.blockers||[];
7672
+ if(focus){var focusStatus=authoritative.status||blockers.length?'blocked':running?'active':'waiting';if(authoritative.status)focusStatus=authoritative.status;focus.className='pk-live-stage-focus '+esc(focusStatus);focus.innerHTML='<span class="eyebrow">Current production stage</span><b>'+esc(authoritative.label||PIPELINE_LABELS[current]||current||'Standing by')+'</b><span>'+esc(authoritative.message||blockers[0]||'Robot is armed and waiting for motion')+'</span>';}
7642
7673
  summary.className='pk-pipeline-summary '+esc(data.overall||'blocked');
7643
7674
  summary.textContent=blockers.length?('BLOCKED: '+blockers[0]):(running?('ACTIVE: '+(PIPELINE_LABELS[current]||current||'starting')+' | session '+(sid||'allocating')):'READY: camera-to-speaker remote test can start');
7644
7675
  var micHealth=$('pk-mic-health');if(micHealth){
@@ -7784,11 +7815,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
7784
7815
  function refreshCameraOperations(){
7785
7816
  var n=cameraOpsNode;if(!n)return;var row=schedRow(n)||{},device=schedulerDevice(n)||{},hw=hwByNode[n.nodeId]||{};
7786
7817
  var readiness=String(row.readiness_code||(!n.connected?'offline':'mesh_online')),active=!!(row.active_session||row.in_session||row.session_active||row.current_session_id||row.status==='running'||row.status==='connecting');
7818
+ var currentStage=row.current_stage||{},currentStageLabel=currentStage.label||row.current_stage_label||(active?'Conversation starting':device.production_mode?'Armed - waiting for motion':'Production paused'),currentStageStatus=currentStage.status||row.current_stage_status||(active?'active':'waiting');
7787
7819
  var img=$('pk-camera-ops-img'),cameraLive=!!(img&&img.dataset.live==='true'),clockEl=$('pk-camera-ops-clock');if(clockEl)clockEl.textContent=clock(Date.now());
7788
7820
  var rec=$('pk-camera-ops-rec');if(rec)rec.textContent=(cameraLive?'REC / LIVE':'REC / CONNECTING')+' / '+String(n.displayName||n.name||'robot').toUpperCase();
7789
- var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>readiness</span></div><div class="metric"><b>'+esc(active?'in session':'standing by')+'</b><span>conversation</span></div><div class="metric"><b>'+(row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'--')+'</b><span>average latency</span></div><div class="metric"><b>'+(row.drop_rate!=null?Math.round(row.drop_rate*100)+'%':'--')+'</b><span>session drops</span></div>';
7821
+ var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(currentStageLabel)+'</b><span>active stage</span></div><div class="metric"><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>readiness</span></div><div class="metric"><b>'+(row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'--')+'</b><span>average latency</span></div><div class="metric"><b>'+(row.drop_rate!=null?Math.round(row.drop_rate*100)+'%':'--')+'</b><span>session drops</span></div>';
7790
7822
  var insights=$('pk-camera-ops-insights');if(insights)insights.innerHTML='<span>Camera relay</span><b class="'+(cameraLive?'ok':'bad')+'">'+(cameraLive?'LIVE':'CONNECTING')+'</b><span>Mesh node</span><b class="'+(n.connected?'ok':'bad')+'">'+(n.connected?'connected':'offline')+'</b><span>Production mode</span><b class="'+(device.production_mode?'ok':'')+'">'+(device.production_mode?'armed':'disabled')+'</b><span>Readiness</span><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>Heartbeat</span><b>'+(row.heartbeat_age_seconds!=null?Math.round(row.heartbeat_age_seconds)+'s ago':'--')+'</b><span>Current room</span><b>'+esc(String(row.current_session_id||row.active_session_id||'none').slice(0,24))+'</b><span>Triggers</span><b>'+esc(row.trigger_count!=null?row.trigger_count:0)+'</b><span>Sessions</span><b>'+esc(row.sessions_total!=null?row.sessions_total:0)+'</b>';
7791
- var stages=[['Robot online',!!n.connected],['Camera streaming',cameraLive],['Motion detector armed',!!device.production_mode],['Scheduler session',active],['Voice pipeline',readiness==='in_session'||active]],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s){return '<div><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?'ready':'waiting')+'</b></div>';}).join('');
7823
+ var stages=[[currentStageLabel,true,'ACTIVE'],['Robot online',!!n.connected,'ready'],['Camera streaming',cameraLive,'ready'],['Motion detector armed',!!device.production_mode,'armed'],['Scheduler session',active,'active']],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s,index){return '<div class="'+(index===0?'fleet-stage-focus '+esc(currentStageStatus):'')+'"><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?s[2]:'waiting')+'</b></div>';}).join('');
7792
7824
  var inv=device.device_inventory||{},mh=inv.media_health||{},hardware=$('pk-camera-ops-hardware');if(hardware)hardware.innerHTML='<span>CPU</span><b>'+(hw.cpu&&hw.cpu.percent!=null?Math.round(hw.cpu.percent)+'%':'--')+'</b><span>Memory</span><b>'+(hw.memory&&hw.memory.percent!=null?Math.round(hw.memory.percent)+'%':'--')+'</b><span>Temperature</span><b>'+(hw.temperatureC!=null?Math.round(hw.temperatureC)+'C':'--')+'</b><span>Camera inputs</span><b>'+((inv.video||[]).length||0)+'</b><span>Microphones</span><b>'+((inv.audio_input||[]).length||0)+'</b><span>Speaker outputs</span><b>'+((inv.audio_output||[]).length||0)+'</b><span>Camera permission</span><b class="'+(mh.camera_access===false?'bad':'ok')+'">'+(mh.camera_access===false?'DENIED':mh.camera_access===true?'granted':'--')+'</b><span>Audio permission</span><b class="'+(mh.audio_access===false?'bad':'ok')+'">'+(mh.audio_access===false?'DENIED':mh.audio_access===true?'granted':'--')+'</b><span>Camera worker</span><b class="'+(mh.camera_stalled?'bad':'ok')+'">'+(mh.camera_stalled?'STALLED':mh.camera_worker?'streaming':'--')+'</b><span>Frame age</span><b>'+(mh.camera_frame_age_seconds!=null?Number(mh.camera_frame_age_seconds).toFixed(1)+'s':'--')+'</b><span>Selected camera</span><b>'+esc(device.video_device||'auto')+'</b><span>Selected microphone</span><b>'+esc(device.audio_device||'default')+'</b>';
7793
7825
  var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
7794
7826
  }
@@ -4,6 +4,8 @@ import { type ExplicitContextOpts } from './profile.js';
4
4
  export interface AddRobotOptions extends ExplicitContextOpts {
5
5
  name: string;
6
6
  start?: boolean;
7
+ autoStart?: boolean;
8
+ yes?: boolean;
7
9
  /** Force a specific park-map pad/character preset id instead of the
8
10
  * fuzzy name-based match below — e.g. `--character jaguar`. */
9
11
  character?: string;
@@ -24,8 +24,32 @@
24
24
  import chalk from 'chalk';
25
25
  import { resolveContext } from './profile.js';
26
26
  import { roboparkSetup } from './setup.js';
27
- async function mintEnrollmentToken(schedulerUrl, name, characterId) {
28
- const url = `${schedulerUrl.replace(/\/$/, '')}/api/devices`;
27
+ function robotReachableSchedulerUrl(schedulerUrl, hubUrl) {
28
+ if (!hubUrl)
29
+ return schedulerUrl;
30
+ try {
31
+ // Port 8080 is intentionally private to the hub. Enrollment, character
32
+ // lookup, heartbeats, and robot sessions must all use the authenticated
33
+ // mesh proxy, whether the hidden scheduler is loopback or a 192.168.x.x
34
+ // address that is only meaningful on the hub's own LAN.
35
+ new URL(schedulerUrl);
36
+ new URL(hubUrl);
37
+ return `${hubUrl.replace(/\/+$/, '')}/robopark`;
38
+ }
39
+ catch {
40
+ // Keep the original value so the existing request reports a useful error.
41
+ }
42
+ return schedulerUrl;
43
+ }
44
+ function schedulerApiUrl(schedulerUrl, path, token) {
45
+ const url = new URL(`${schedulerUrl.replace(/\/+$/, '')}${path}`);
46
+ if (token && url.pathname.startsWith('/robopark/')) {
47
+ url.searchParams.set('token', token);
48
+ }
49
+ return url.toString();
50
+ }
51
+ async function mintEnrollmentToken(schedulerUrl, name, characterId, token) {
52
+ const url = schedulerApiUrl(schedulerUrl, '/api/devices', token);
29
53
  const res = await fetch(url, {
30
54
  method: 'POST',
31
55
  headers: { 'content-type': 'application/json' },
@@ -37,10 +61,10 @@ async function mintEnrollmentToken(schedulerUrl, name, characterId) {
37
61
  }
38
62
  return (await res.json());
39
63
  }
40
- async function pickCharacterPreset(schedulerUrl, name) {
64
+ async function pickCharacterPreset(schedulerUrl, name, token) {
41
65
  const lower = name.toLowerCase();
42
66
  try {
43
- const res = await fetch(`${schedulerUrl.replace(/\/$/, '')}/api/character-presets`);
67
+ const res = await fetch(schedulerApiUrl(schedulerUrl, '/api/character-presets', token));
44
68
  if (!res.ok)
45
69
  return undefined;
46
70
  const presets = (await res.json());
@@ -83,11 +107,16 @@ export async function roboparkAddRobot(config, opts) {
83
107
  console.log(chalk.red(' ✗ no scheduler URL resolved. Pass --scheduler-url/--hub-url, run `robopark hub-use` first, or run this on/near the hub.'));
84
108
  process.exit(1);
85
109
  }
86
- console.log(` scheduler: ${chalk.cyan(ctx.schedulerUrl)}`);
110
+ const schedulerUrl = robotReachableSchedulerUrl(ctx.schedulerUrl, ctx.hubUrl);
111
+ if (schedulerUrl.endsWith('/robopark') && !ctx.token) {
112
+ console.log(chalk.red(' ✗ hub mesh token is required to use the scheduler proxy. Pass --token or run `robopark hub-use` first.'));
113
+ process.exit(1);
114
+ }
115
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
87
116
  console.log(` name: ${chalk.cyan(opts.name)}${ctx.site ? chalk.dim(' @ ' + ctx.site) : ''}`);
88
117
  console.log(` network: ${chalk.cyan(network)}`);
89
118
  console.log();
90
- const characterId = opts.character?.trim() || await pickCharacterPreset(ctx.schedulerUrl, opts.name);
119
+ const characterId = opts.character?.trim() || await pickCharacterPreset(schedulerUrl, opts.name, ctx.token);
91
120
  if (characterId) {
92
121
  console.log(` character: ${chalk.cyan(characterId)}${opts.character ? chalk.dim(' (forced via --character)') : chalk.dim(' (auto-matched)')}`);
93
122
  }
@@ -96,7 +125,7 @@ export async function roboparkAddRobot(config, opts) {
96
125
  }
97
126
  let minted;
98
127
  try {
99
- minted = await mintEnrollmentToken(ctx.schedulerUrl, opts.name, characterId);
128
+ minted = await mintEnrollmentToken(schedulerUrl, opts.name, characterId, ctx.token);
100
129
  }
101
130
  catch (e) {
102
131
  console.log(chalk.red(` ✗ failed to mint enrollment token: ${e.message}`));
@@ -120,6 +149,8 @@ export async function roboparkAddRobot(config, opts) {
120
149
  schedulerPort: ctx.schedulerUrl ? String(new URL(ctx.schedulerUrl).port || '8080') : undefined,
121
150
  enrollmentToken: minted.enrollment_token,
122
151
  start: true,
152
+ autoStart: opts.autoStart !== false,
153
+ yes: opts.yes !== false,
123
154
  lan: opts.lan,
124
155
  tailscale: opts.tailscale,
125
156
  });
@@ -140,7 +171,7 @@ export async function roboparkAddRobot(config, opts) {
140
171
  argv.push('--scheduler-port', schedulerPort);
141
172
  }
142
173
  argv.push(network === 'tailscale' ? '--tailscale' : '--lan');
143
- argv.push('--enrollment-token', minted.enrollment_token, '--start', '--auto-start');
174
+ argv.push('--enrollment-token', minted.enrollment_token, '--start', '--auto-start', '--yes');
144
175
  console.log(chalk.dim(` run this ON THE ROBOT${characterId ? ` (binds to pad: ${characterId})` : ''}:`));
145
176
  console.log(' ' + chalk.cyan(commandString(argv)));
146
177
  if (!ctx.hubUrl) {
@@ -146,7 +146,7 @@ export async function roboparkRobotRuntime(opts) {
146
146
  // only while audible robot speech is playing, then hold briefly for
147
147
  // room echo decay so VAD cannot interrupt the robot with its own voice.
148
148
  ROBOPARK_HALF_DUPLEX: 'true',
149
- ROBOPARK_ECHO_TAIL_MS: '700',
149
+ ROBOPARK_ECHO_TAIL_MS: '350',
150
150
  ROBOPARK_ECHO_GATE_PEAK: '96',
151
151
  // Keep ordinary speaker echo muted but reopen the microphone when a
152
152
  // nearby voice rises decisively above the learned acoustic echo floor.
@@ -284,6 +284,8 @@ program
284
284
  .option('--lan', 'generate a robot command for the same local network (default)')
285
285
  .option('--tailscale', 'generate a robot command for a Tailscale-reachable hub')
286
286
  .option('--start', 'start the robot node on this machine now, instead of printing the one-liner')
287
+ .option('--auto-start', 'register the robot runtime to start on boot')
288
+ .option('--yes', 'non-interactive production setup')
287
289
  .action(async (name, opts) => {
288
290
  await roboparkAddRobot(config, { name, ...opts });
289
291
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.102",
3
+ "version": "2.8.103",
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",
@@ -3192,6 +3192,93 @@ PIPELINE_STAGES = (
3192
3192
  )
3193
3193
  PIPELINE_STATUSES = {"pending", "running", "ok", "failed", "blocked", "skipped"}
3194
3194
 
3195
+ PIPELINE_STAGE_LABELS = {
3196
+ "robot_online": "Robot online",
3197
+ "camera_ready": "Camera detected",
3198
+ "microphone_ready": "Microphone detected",
3199
+ "speaker_ready": "Speaker detected",
3200
+ "motion_detected": "Waiting for motion",
3201
+ "scheduler_session": "Allocating session",
3202
+ "livekit_join": "Joining LiveKit",
3203
+ "camera_published": "Publishing camera",
3204
+ "microphone_published": "Publishing microphone",
3205
+ "voice_worker": "Starting voice worker",
3206
+ "stt_listening": "Listening to visitor",
3207
+ "llm_response": "Preparing response",
3208
+ "tts_subscribed": "Preparing robot voice",
3209
+ "playback_started": "Speaking through robot",
3210
+ "motor_sequence": "Running motor sequence",
3211
+ "session_ended": "Closing session",
3212
+ }
3213
+
3214
+
3215
+ def _pipeline_current_state(events: list[dict], *, active: bool,
3216
+ production_mode: bool, online: bool) -> dict:
3217
+ """Derive one operator-facing live stage from persistent pipeline events."""
3218
+ if not online:
3219
+ return {
3220
+ "stage": "robot_online", "status": "blocked",
3221
+ "label": PIPELINE_STAGE_LABELS["robot_online"],
3222
+ "message": "Robot heartbeat is stale or missing", "since": None,
3223
+ }
3224
+ if not active:
3225
+ return {
3226
+ "stage": "motion_detected",
3227
+ "status": "waiting" if production_mode else "paused",
3228
+ "label": "Armed - waiting for motion" if production_mode else "Production paused",
3229
+ "message": "Motion detection is armed" if production_mode else "Enable production mode to arm this robot",
3230
+ "since": None,
3231
+ }
3232
+
3233
+ scoped = [event for event in events if event.get("session_id")]
3234
+ for event in reversed(scoped):
3235
+ if event.get("status") in {"failed", "blocked"}:
3236
+ return {
3237
+ "stage": event["stage"], "status": event["status"],
3238
+ "label": PIPELINE_STAGE_LABELS.get(event["stage"], event["stage"]),
3239
+ "message": event.get("message") or "Pipeline requires attention",
3240
+ "since": event.get("timestamp"),
3241
+ }
3242
+
3243
+ conversational = {
3244
+ "voice_worker", "stt_listening", "llm_response", "tts_subscribed",
3245
+ "playback_started", "motor_sequence",
3246
+ }
3247
+ latest = next((event for event in reversed(scoped) if event.get("stage") in conversational), None)
3248
+ if latest:
3249
+ stage = latest["stage"]
3250
+ status = latest.get("status") or "running"
3251
+ if status in {"ok", "skipped"}:
3252
+ stage = {
3253
+ "voice_worker": "stt_listening",
3254
+ "stt_listening": "llm_response",
3255
+ "llm_response": "tts_subscribed",
3256
+ "tts_subscribed": "playback_started",
3257
+ "playback_started": "stt_listening",
3258
+ "motor_sequence": "stt_listening",
3259
+ }.get(stage, stage)
3260
+ status = "active"
3261
+ return {
3262
+ "stage": stage, "status": status,
3263
+ "label": PIPELINE_STAGE_LABELS.get(stage, stage),
3264
+ "message": latest.get("message") or PIPELINE_STAGE_LABELS.get(stage, stage),
3265
+ "since": latest.get("timestamp"),
3266
+ }
3267
+
3268
+ startup = (
3269
+ "scheduler_session", "livekit_join", "camera_published",
3270
+ "microphone_published", "voice_worker",
3271
+ )
3272
+ completed = {event.get("stage") for event in scoped if event.get("status") in {"ok", "skipped"}}
3273
+ stage = next((candidate for candidate in startup if candidate not in completed), "voice_worker")
3274
+ latest_startup = next((event for event in reversed(scoped) if event.get("stage") in startup), None)
3275
+ return {
3276
+ "stage": stage, "status": "active",
3277
+ "label": PIPELINE_STAGE_LABELS.get(stage, stage),
3278
+ "message": (latest_startup or {}).get("message") or "Production conversation is starting",
3279
+ "since": (latest_startup or {}).get("timestamp"),
3280
+ }
3281
+
3195
3282
  async def _insert_pipeline_event(device_id: str, payload: PipelineEventPayload) -> dict:
3196
3283
  if payload.stage not in PIPELINE_STAGES:
3197
3284
  raise HTTPException(422, f"Unknown pipeline stage: {payload.stage}")
@@ -3286,16 +3373,29 @@ async def robot_pipeline_status(robot_id: str, limit: int = 80):
3286
3373
  blockers.append("Microphone inventory has not been reported")
3287
3374
  if not prereqs["speaker_ready"]:
3288
3375
  blockers.append("Speaker inventory has not been reported")
3376
+ active = bool(latest_session and not latest_session["ended_at"])
3377
+ active_session_id = latest_session["id"] if active else None
3378
+ current_events = [
3379
+ event for event in events
3380
+ if not active_session_id or event.get("session_id") == active_session_id
3381
+ ]
3382
+ current_stage = _pipeline_current_state(
3383
+ current_events,
3384
+ active=active,
3385
+ production_mode=bool(device["production_mode"]),
3386
+ online=prereqs["robot_online"],
3387
+ )
3289
3388
  return {
3290
3389
  "robot_id": device_id,
3291
3390
  "robot_name": device["name"],
3292
- "overall": "blocked" if blockers else ("running" if latest_session and not latest_session["ended_at"] else "ready"),
3391
+ "overall": "blocked" if blockers else ("running" if active else "ready"),
3293
3392
  "blockers": list(dict.fromkeys(blockers)),
3294
3393
  "prerequisites": prereqs,
3295
3394
  "telemetry": telemetry,
3296
3395
  "latest_session": dict(latest_session) if latest_session else None,
3297
3396
  "events": events,
3298
3397
  "stages": list(PIPELINE_STAGES),
3398
+ "current_stage": current_stage,
3299
3399
  }
3300
3400
 
3301
3401
  @app.get("/api/robots/{robot_id}/pipeline-history")
@@ -5170,13 +5270,20 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
5170
5270
 
5171
5271
  device_inventory = parse_json_field(device["device_inventory"], {}) if device else {}
5172
5272
  supervisor_status = parse_json_field(device["supervisor_status"], []) if device else []
5173
- session = None
5174
- if robot["current_session_id"]:
5273
+ session = None
5274
+ current_pipeline_events = []
5275
+ if robot["current_session_id"]:
5175
5276
  async with db.execute(
5176
5277
  "SELECT started_at, last_activity_at, ended_at FROM sessions WHERE id = ?",
5177
5278
  (robot["current_session_id"],),
5178
- ) as c:
5179
- session = await c.fetchone()
5279
+ ) as c:
5280
+ session = await c.fetchone()
5281
+ async with db.execute(
5282
+ "SELECT * FROM pipeline_events WHERE robot_id = ? AND session_id = ? "
5283
+ "ORDER BY timestamp, id",
5284
+ (robot_id, robot["current_session_id"]),
5285
+ ) as c:
5286
+ current_pipeline_events = [dict(row) for row in await c.fetchall()]
5180
5287
 
5181
5288
  now = datetime.utcnow()
5182
5289
  session_age = None
@@ -5259,8 +5366,14 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
5259
5366
  (robot_id,),
5260
5367
  ) as c:
5261
5368
  end_reasons = {r["end_reason"]: r["n"] for r in await c.fetchall()}
5262
- drops = sum(n for reason, n in end_reasons.items() if reason in ABNORMAL_END_REASONS)
5263
- drop_rate = round(drops / sessions_total, 4) if sessions_total else 0.0
5369
+ drops = sum(n for reason, n in end_reasons.items() if reason in ABNORMAL_END_REASONS)
5370
+ drop_rate = round(drops / sessions_total, 4) if sessions_total else 0.0
5371
+ current_stage = _pipeline_current_state(
5372
+ current_pipeline_events,
5373
+ active=bool(robot["current_session_id"] and session and not session["ended_at"]),
5374
+ production_mode=bool(device["production_mode"]) if device else False,
5375
+ online=heartbeat_age is not None and heartbeat_age <= 30,
5376
+ )
5264
5377
 
5265
5378
  return {
5266
5379
  "robot_id": robot_id,
@@ -5291,9 +5404,13 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
5291
5404
  "supervisor_status": supervisor_status,
5292
5405
  "supervisor_status_at": device["supervisor_status_at"] if device else None,
5293
5406
  "production_mode": bool(device["production_mode"]) if device else False,
5294
- "readiness_code": readiness_code,
5295
- "readiness_message": readiness_message,
5296
- }
5407
+ "readiness_code": readiness_code,
5408
+ "readiness_message": readiness_message,
5409
+ "current_stage": current_stage,
5410
+ "current_stage_id": current_stage["stage"],
5411
+ "current_stage_label": current_stage["label"],
5412
+ "current_stage_status": current_stage["status"],
5413
+ }
5297
5414
 
5298
5415
  @app.get("/api/robots/{robot_id}/telemetry")
5299
5416
  async def robot_telemetry(robot_id: str):
@@ -1303,7 +1303,7 @@ class LiveKitPublisher:
1303
1303
  self._echo_mic_floor = 0.0
1304
1304
  self._barge_in_candidate_frames = 0
1305
1305
  self._speaker_echo_tail = max(
1306
- 0.1, min(float(os.getenv("ROBOPARK_ECHO_TAIL_MS", "700")) / 1000.0, 2.0)
1306
+ 0.1, min(float(os.getenv("ROBOPARK_ECHO_TAIL_MS", "350")) / 1000.0, 2.0)
1307
1307
  )
1308
1308
  self._adaptive_barge_in = str(
1309
1309
  os.getenv("ROBOPARK_ADAPTIVE_BARGE_IN", "true")
@@ -1553,7 +1553,7 @@ class LiveKitPublisher:
1553
1553
  import threading
1554
1554
  write_queue: "queue.Queue[Optional[bytes]]" = queue.Queue()
1555
1555
  written_frames = [0]
1556
- PREBUFFER_CHUNKS = 5
1556
+ PREBUFFER_CHUNKS = 3
1557
1557
  stream_failed = threading.Event()
1558
1558
  playback_reported = threading.Event()
1559
1559
  event_loop = asyncio.get_running_loop()