infinicode 2.8.73 → 2.8.76

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.
@@ -691,6 +691,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
691
691
  @media(max-width:900px){.rp-config-columns{grid-template-columns:1fr}.rp-config-hero{align-items:flex-start;flex-direction:column}.rp-config-cards{max-height:430px}}
692
692
  @media(max-width:620px){.rp-config-fields{grid-template-columns:1fr}.rp-config-fields .wide{grid-column:auto}.rp-config-pipeline{grid-template-columns:1fr}.rp-config-pipeline i{display:none}.rp-config-hero-actions{width:100%}.rp-config-hero-actions .rp-btn{flex:1}}
693
693
 
694
+ /* command sheet */
695
+ .rp-command-shell{display:grid;gap:1rem}.rp-command-env{display:grid;grid-template-columns:repeat(3,minmax(170px,1fr));gap:.65rem;padding:.85rem;border:1px solid var(--glass-border);border-radius:16px;background:var(--panel-2)}
696
+ .rp-command-toolbar{display:flex;gap:.55rem;align-items:center;flex-wrap:wrap}.rp-command-toolbar input{flex:1;min-width:220px}.rp-command-toolbar select{width:auto;min-width:170px}
697
+ .rp-command-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:.75rem}.rp-command-card{display:flex;flex-direction:column;gap:.55rem;min-width:0;padding:.85rem;border:1px solid var(--glass-border);border-radius:16px;background:linear-gradient(145deg,rgba(31,26,18,.66),rgba(9,8,6,.76));box-shadow:inset 0 1px 0 var(--glass-hi),0 12px 32px rgba(0,0,0,.22)}
698
+ .rp-command-head{display:flex;align-items:flex-start;justify-content:space-between;gap:.6rem}.rp-command-head b{font-family:var(--mono);font-size:.74rem;color:var(--gold-pale)}.rp-command-tag{font-family:var(--mono);font-size:.52rem;text-transform:uppercase;letter-spacing:.06em;color:var(--gold-bright);border:1px solid var(--border-strong);border-radius:100px;padding:.15rem .42rem;white-space:nowrap}.rp-command-card p{font-size:.64rem;line-height:1.5;color:var(--muted);min-height:2.9em}
699
+ .rp-command-code{position:relative;min-height:86px;padding:.68rem .72rem;border:1px solid rgba(255,224,151,.1);border-radius:11px;background:#050504;overflow:auto}.rp-command-code code{display:block;font-family:var(--mono);font-size:.59rem;line-height:1.55;color:#f2dfab;white-space:pre-wrap;overflow-wrap:anywhere}.rp-command-actions{display:flex;gap:.4rem;align-items:center;margin-top:auto}.rp-command-actions .rp-btn:first-child{flex:1}.rp-command-empty{grid-column:1/-1;padding:2rem;text-align:center;color:var(--muted);border:1px dashed var(--glass-border);border-radius:16px}
700
+ .rp-command-custom{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;padding:.9rem;border:1px solid var(--glass-border);border-radius:16px;background:var(--panel-2)}.rp-command-custom h4,.rp-command-custom .wide{grid-column:1/-1}.rp-command-custom h4{font-family:var(--mono);font-size:.72rem;color:var(--gold-bright)}.rp-command-custom textarea{min-height:92px;resize:vertical}.rp-command-note{font-size:.61rem;color:var(--muted)}
701
+ @media(max-width:800px){.rp-command-env{grid-template-columns:1fr 1fr}.rp-command-grid{grid-template-columns:1fr}}@media(max-width:540px){.rp-command-env,.rp-command-custom{grid-template-columns:1fr}.rp-command-custom h4,.rp-command-custom .wide{grid-column:auto}}
702
+
694
703
  .rp-fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:0.7rem;padding:0;}
695
704
  .rp-fleet-card{position:relative;}
696
705
  .rp-fleet-card .spark{margin-top:0.5rem;height:36px;}
@@ -831,6 +840,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
831
840
  <button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
832
841
  <button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
833
842
  <button class="rp-subtab" data-sub="config" id="rpsbtn-config" role="tab" aria-selected="false">Characters &amp; Voice</button>
843
+ <button class="rp-subtab" data-sub="commands" id="rpsbtn-commands" role="tab" aria-selected="false">Commands</button>
834
844
  <button class="rp-subtab" data-sub="setup" id="rpsbtn-setup" role="tab" aria-selected="false">Setup</button>
835
845
  </div>
836
846
 
@@ -1072,7 +1082,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1072
1082
  </div>
1073
1083
 
1074
1084
  <!-- ═══ Setup ═══ -->
1075
- <div class="rp-sub" id="rp-sub-setup">
1085
+ <div class="rp-sub" id="rp-sub-setup">
1076
1086
  <!-- Add LiveKit server -->
1077
1087
  <div class="rp-form" id="rp-addserver">
1078
1088
  <h3>Add LiveKit server</h3>
@@ -1132,6 +1142,38 @@ button.act:disabled{opacity:0.5;cursor:default;}
1132
1142
 
1133
1143
  </div>
1134
1144
 
1145
+ <!-- Command sheet -->
1146
+ <div class="rp-sub" id="rp-sub-commands">
1147
+ <div class="rp-command-shell">
1148
+ <div class="rp-config-hero">
1149
+ <div><h3>RoboPark Command Sheet</h3><p>Production one-liners for the hub, LAN and Tailscale robots, health checks, recovery, Docker, LiveKit, and package operations. Edit the environment once; every generated command below updates immediately.</p></div>
1150
+ <div class="rp-config-hero-actions"><button class="rp-btn" id="rp-command-reset">Reset environment</button><button class="rp-btn primary" id="rp-command-copy-all">Copy visible commands</button></div>
1151
+ </div>
1152
+ <div class="rp-command-env">
1153
+ <div class="rp-field"><label>Hub LAN IP</label><input id="rp-command-lan" value="192.168.1.12" spellcheck="false"></div>
1154
+ <div class="rp-field"><label>Hub Tailscale IP / DNS</label><input id="rp-command-tail" value="100.84.147.24" spellcheck="false"></div>
1155
+ <div class="rp-field"><label>Site</label><input id="rp-command-site" value="Tel Aviv"></div>
1156
+ <div class="rp-field"><label>Robot name</label><input id="rp-command-robot" value="bmw" spellcheck="false"></div>
1157
+ <div class="rp-field"><label>Enrollment token</label><input id="rp-command-enrollment" placeholder="paste robot enrollment token" spellcheck="false"></div>
1158
+ <div class="rp-field"><label>Mesh token</label><input id="rp-command-token" spellcheck="false"></div>
1159
+ </div>
1160
+ <div class="rp-command-toolbar">
1161
+ <input type="search" id="rp-command-search" placeholder="Search setup, logs, camera, Docker, LiveKit, npm...">
1162
+ <select id="rp-command-category"><option value="">All categories</option><option>Provision</option><option>Health</option><option>Recovery</option><option>Media</option><option>Packages</option><option>Custom</option></select>
1163
+ <span class="rp-command-note" id="rp-command-count"></span>
1164
+ </div>
1165
+ <div class="rp-command-grid" id="rp-command-grid"></div>
1166
+ <form class="rp-command-custom" id="rp-command-custom-form">
1167
+ <h4>Add a persistent command</h4>
1168
+ <div class="rp-field"><label>Title</label><input id="rp-command-custom-title" required placeholder="Restart camera service"></div>
1169
+ <div class="rp-field"><label>Category</label><input id="rp-command-custom-category" value="Custom" placeholder="Custom"></div>
1170
+ <div class="rp-field wide"><label>Simple explanation</label><input id="rp-command-custom-description" placeholder="When to use this and what success looks like"></div>
1171
+ <div class="rp-field wide"><label>Command</label><textarea id="rp-command-custom-command" required spellcheck="false" placeholder="Paste a safe command or one-liner"></textarea></div>
1172
+ <div class="wide"><button class="rp-btn primary" type="submit">Save command to this browser</button> <span class="rp-command-note">Custom commands persist in localStorage on this operator browser.</span></div>
1173
+ </form>
1174
+ </div>
1175
+ </div>
1176
+
1135
1177
  <!-- Character + voice management -->
1136
1178
  <div class="rp-sub" id="rp-sub-config">
1137
1179
  <div class="rp-config-workspace" id="rp-templates">
@@ -1782,7 +1824,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
1782
1824
  $('pane-fleet').classList.toggle('active', name==='fleet');
1783
1825
  $('pane-park').classList.toggle('active', name==='park');
1784
1826
  $('pane-robopark').classList.toggle('active', name==='robopark');
1785
- if (location.hash.replace('#','') !== name) { try{ history.replaceState(null,'','#'+name); }catch(e){} }
1827
+ var currentHash=location.hash.replace('#','');
1828
+ var keepRpSubroute=name==='robopark' && currentHash.indexOf('robopark/')===0;
1829
+ if (!keepRpSubroute && currentHash !== name) { try{ history.replaceState(null,'','#'+name); }catch(e){} }
1786
1830
  if (name==='robopark'){
1787
1831
  pollRobopark();
1788
1832
  initRpSubTabs();
@@ -1793,11 +1837,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
1793
1837
  var fleetSessionsBtn=$('fleet-open-sessions'); if(fleetSessionsBtn) fleetSessionsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('live');});
1794
1838
  var fleetIncidentsBtn=$('fleet-open-incidents'); if(fleetIncidentsBtn) fleetIncidentsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('incidents');});
1795
1839
  var fleetRefreshBtn=$('fleet-refresh'); if(fleetRefreshBtn) fleetRefreshBtn.addEventListener('click',function(){poll();pollRobopark();});
1796
- var initHash = location.hash.replace('#','');
1797
- // Defer tab init until Park module is defined (same outer IIFE, later in source).
1798
- function initTab(){
1799
- if (initHash==='robopark'||initHash==='park') selectTab(initHash);
1800
- }
1840
+ var initHash = location.hash.replace('#','');
1841
+ // Defer tab init until Park module is defined (same outer IIFE, later in source).
1842
+ function initTab(){
1843
+ if (initHash==='robopark'||initHash.indexOf('robopark/')===0) selectTab('robopark');
1844
+ else if(initHash==='park') selectTab('park');
1845
+ }
1801
1846
  // initTab() is called at the very bottom of this IIFE, after Park is assigned.
1802
1847
 
1803
1848
  function robotsFrom(d){
@@ -2549,7 +2594,74 @@ button.act:disabled{opacity:0.5;cursor:default;}
2549
2594
  }
2550
2595
 
2551
2596
  // ── Sub-tab routing ──
2552
- var rpSubTab = 'overview';
2597
+ // Command sheet: generate production commands from one saved environment.
2598
+ var RP_COMMAND_ENV_KEY='rp-command-env-v1';
2599
+ var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
2600
+ var rpVisibleCommands=[];
2601
+ function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:TOKEN};}
2602
+ function rpLoadStoredJson(key,fallback){try{var value=JSON.parse(localStorage.getItem(key)||'null');return value&&typeof value==='object'?value:fallback;}catch(e){return fallback;}}
2603
+ function rpCommandEnv(){
2604
+ var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2605
+ function value(name){var input=$('rp-command-'+name),raw=input?input.value:(s[name]!=null?s[name]:d[name]);return String(raw==null?'':raw).trim();}
2606
+ return {lan:value('lan'),tail:value('tail'),site:value('site'),robot:value('robot'),enrollment:value('enrollment'),token:value('token')};
2607
+ }
2608
+ function rpSaveCommandEnv(){try{localStorage.setItem(RP_COMMAND_ENV_KEY,JSON.stringify(rpCommandEnv()));}catch(e){}}
2609
+ function rpShellArg(value){var text=String(value||'');return /^[A-Za-z0-9._:\/-]+$/.test(text)?text:'"'+text.replace(/"/g,'\\"')+'"';}
2610
+ function rpCommandCatalog(){
2611
+ var e=rpCommandEnv(),lan='http://'+e.lan,tail='http://'+e.tail,enroll=e.enrollment?' --enrollment-token '+rpShellArg(e.enrollment):'';
2612
+ return [
2613
+ {id:'hub-up',category:'Provision',title:'Start production hub and full dashboard',description:'Run elevated on livekit-1. Starts the mesh hub, full Park control center, scheduler proxy, LAN discovery, supervision, and updates.',command:'infinicode serve --hub --name livekit-1 --port 47913 --token '+rpShellArg(e.token)+' --lan --dashboard --scheduler-url http://127.0.0.1:8080 --auto-update --supervised'},
2614
+ {id:'robot-lan',category:'Provision',title:'Provision LAN robot',description:'One command for a Linux robot on the hub LAN. Installs the supervised mesh, RoboVision, and preview runtime.',command:'sudo robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+lan+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --lan --start --auto-start --yes'},
2615
+ {id:'robot-tail',category:'Provision',title:'Provision Tailscale robot',description:'One command for a Windows or Linux robot reaching the hub through Tailscale.',command:'robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+tail+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --tailscale --start --auto-start --yes'},
2616
+ {id:'operator-tail',category:'Provision',title:'Connect operator MCP over Tailscale',description:'Connects an operator workstation and coding agents to the production mesh.',command:'infinicode mcp --lan --tailscale --seed '+tail+':47913 --name dev-laptop --token '+rpShellArg(e.token)+' --mesh-port 47913'},
2617
+ {id:'hub-ui',category:'Health',title:'Open full production control center',description:'Opens the full Park dashboard through the hub Tailscale address.',command:'Start-Process "'+tail+':47913/?token='+encodeURIComponent(e.token)+'#park"'},
2618
+ {id:'hub-build',category:'Health',title:'Verify current hub UI',description:'Shows the installed version and dashboard build marker after npm update or restart.',command:'infinicode --version; Invoke-RestMethod "http://127.0.0.1:47913/fed/ui-build?token='+encodeURIComponent(e.token)+'"'},
2619
+ {id:'hub-ports',category:'Health',title:'Inspect hub ports on Windows',description:'Lists processes owning the control center, scheduler, and LiveKit ports without changing them.',command:'Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -in 47913,8080,7880,7881,7882,7883 } | Sort-Object LocalPort | Format-Table LocalAddress,LocalPort,OwningProcess -AutoSize'},
2620
+ {id:'scheduler-health',category:'Health',title:'Check scheduler through hub',description:'Confirms the hub proxy can reach the scheduler and return robot inventory.',command:'Invoke-RestMethod "'+tail+':47913/robopark/api/robots?token='+encodeURIComponent(e.token)+'" | ConvertTo-Json -Depth 6'},
2621
+ {id:'mesh-status',category:'Health',title:'Check mesh nodes',description:'Lists connected hub, robot, and operator nodes.',command:'Invoke-RestMethod "'+tail+':47913/fed/status?token='+encodeURIComponent(e.token)+'" | ConvertTo-Json -Depth 6'},
2622
+ {id:'pi-runtime',category:'Health',title:'Follow robot runtime logs',description:'Live systemd logs for the selected robot unified runtime.',command:'sudo journalctl -u robopark-robot-runtime-'+e.robot+'.service -f'},
2623
+ {id:'pi-ports',category:'Health',title:'Inspect robot service ports',description:'Checks mesh, camera, motion webhook, and audio listeners on a Linux robot.',command:"sudo ss -ltnp | grep -E ':(47913|5000|5057|8000)'"},
2624
+ {id:'camera-inventory',category:'Media',title:'Read RoboVision media inventory',description:'Returns the real cameras, microphones, and speakers detected by RoboVision.',command:'curl -s http://127.0.0.1:5000/api/media/inventory'},
2625
+ {id:'audio-inventory',category:'Media',title:'Read RoboVision audio devices',description:'Returns the microphone and speaker inventory used by the robot audio server.',command:'curl -s http://127.0.0.1:8000/devices'},
2626
+ {id:'camera-devices',category:'Media',title:'List valid Linux capture devices',description:'Shows V4L2 devices and capabilities so metadata-only nodes are not selected as cameras.',command:'v4l2-ctl --list-devices; for d in /dev/video*; do echo "--- $d"; v4l2-ctl -d "$d" --all 2>/dev/null | grep -E "Card type|Video Capture|Device Caps"; done'},
2627
+ {id:'camera-preview',category:'Media',title:'Open direct RoboVision camera feed',description:'Opens the robot MJPEG feed locally for camera-only diagnosis.',command:'xdg-open http://127.0.0.1:5000/video_feed'},
2628
+ {id:'robot-restart',category:'Recovery',title:'Clean restart selected robot',description:'Stops stale supervised processes, then recreates the complete LAN runtime.',command:'sudo robopark stop && sudo robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+lan+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --lan --start --auto-start --yes'},
2629
+ {id:'robot-status',category:'Recovery',title:'Inspect robot service failure',description:'Shows service state and the latest 150 log lines after a failed start.',command:'sudo systemctl status robopark-robot-runtime-'+e.robot+'.service --no-pager; sudo journalctl -u robopark-robot-runtime-'+e.robot+'.service -n 150 --no-pager'},
2630
+ {id:'docker-health',category:'Health',title:'Inspect Docker and LiveKit containers',description:'Shows container state, health, ports, and names on the hub.',command:'docker ps -a --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\\t{{.Image}}"'},
2631
+ {id:'docker-restart',category:'Recovery',title:'Restart Docker Desktop service',description:'Run from elevated PowerShell only when Docker Desktop is stuck starting.',command:'Restart-Service com.docker.service; Start-Process -FilePath "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"'},
2632
+ {id:'tailscale-status',category:'Health',title:'Check Tailscale routes and Serve',description:'Confirms the node is connected and shows any active Serve proxy.',command:'tailscale status; tailscale serve status'},
2633
+ {id:'npm-version',category:'Packages',title:'Check installed and published versions',description:'Compares this device with the npm latest tag.',command:'infinicode --version; npm view infinicode version'},
2634
+ {id:'npm-update',category:'Packages',title:'Update InfiniCode and RoboPark',description:'Installs the current published production package globally.',command:'npm install -g infinicode@latest --force'},
2635
+ {id:'npm-cache',category:'Packages',title:'Repair npm metadata cache',description:'Use when a new version returns ETARGET. Verify metadata before clearing anything.',command:'npm cache verify; npm view infinicode dist-tags --json'}
2636
+ ];
2637
+ }
2638
+ function rpCustomCommands(){var list=rpLoadStoredJson(RP_CUSTOM_COMMANDS_KEY,[]);return Array.isArray(list)?list:[];}
2639
+ function rpSaveCustomCommands(list){try{localStorage.setItem(RP_CUSTOM_COMMANDS_KEY,JSON.stringify(list));}catch(e){showOut('Could not save custom command: '+e.message);}}
2640
+ function rpLegacyCopy(value){var area=document.createElement('textarea');area.value=value;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();try{document.execCommand('copy');showOut('command copied');}catch(e){showOut('Copy failed: select the command manually');}area.remove();}
2641
+ function rpCopyCommand(text){var value=String(text||'');if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(value).then(function(){showOut('command copied');}).catch(function(){rpLegacyCopy(value);});}else rpLegacyCopy(value);}
2642
+ function renderCommandSheet(){
2643
+ var grid=$('rp-command-grid');if(!grid)return;
2644
+ var search=String(($('rp-command-search')||{}).value||'').toLowerCase().trim(),category=String(($('rp-command-category')||{}).value||'').toLowerCase();
2645
+ var custom=rpCustomCommands().map(function(x){return {id:String(x.id),category:String(x.category||'Custom'),title:String(x.title||'Custom command'),description:String(x.description||''),command:String(x.command||''),custom:true};});
2646
+ rpVisibleCommands=rpCommandCatalog().concat(custom).filter(function(x){if(category&&x.category.toLowerCase()!==category)return false;return !search||(x.title+' '+x.category+' '+x.description+' '+x.command).toLowerCase().indexOf(search)>=0;});
2647
+ var count=$('rp-command-count');if(count)count.textContent=rpVisibleCommands.length+' command'+(rpVisibleCommands.length===1?'':'s');
2648
+ if(!rpVisibleCommands.length){grid.innerHTML='<div class="rp-command-empty">No commands match this filter.</div>';return;}
2649
+ grid.innerHTML=rpVisibleCommands.map(function(x){return '<article class="rp-command-card"><div class="rp-command-head"><b>'+esc(x.title)+'</b><span class="rp-command-tag">'+esc(x.category)+'</span></div><p>'+esc(x.description)+'</p><pre class="rp-command-code"><code>'+esc(x.command)+'</code></pre><div class="rp-command-actions"><button class="rp-btn primary" data-command-copy="'+esc(x.id)+'">Copy command</button>'+(x.custom?'<button class="rp-btn danger" data-command-delete="'+esc(x.id)+'">Delete</button>':'')+'</div></article>';}).join('');
2650
+ }
2651
+ function initCommandSheet(){
2652
+ var fields=['lan','tail','site','robot','enrollment','token'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2653
+ 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();});});
2654
+ 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');
2655
+ if(search)search.addEventListener('input',renderCommandSheet);if(category)category.addEventListener('change',renderCommandSheet);
2656
+ 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');});
2657
+ if(copyAll)copyAll.addEventListener('click',function(){rpCopyCommand(rpVisibleCommands.map(function(x){return '# '+x.title+'\\n'+x.command;}).join('\\n\\n'));});
2658
+ if(grid)grid.addEventListener('click',function(ev){var copy=ev.target.closest('[data-command-copy]'),del=ev.target.closest('[data-command-delete]');if(copy){var id=copy.getAttribute('data-command-copy'),item=rpVisibleCommands.find(function(x){return x.id===id;});if(item)rpCopyCommand(item.command);}if(del){var removeId=del.getAttribute('data-command-delete');rpSaveCustomCommands(rpCustomCommands().filter(function(x){return String(x.id)!==removeId;}));renderCommandSheet();showOut('custom command deleted');}});
2659
+ if(form)form.addEventListener('submit',function(ev){ev.preventDefault();var title=$('rp-command-custom-title').value.trim(),command=$('rp-command-custom-command').value.trim();if(!title||!command)return;var list=rpCustomCommands();list.push({id:'custom-'+Date.now(),title:title,category:$('rp-command-custom-category').value.trim()||'Custom',description:$('rp-command-custom-description').value.trim(),command:command});rpSaveCustomCommands(list);$('rp-command-custom-title').value='';$('rp-command-custom-description').value='';$('rp-command-custom-command').value='';renderCommandSheet();showOut('custom command saved in this browser');});
2660
+ renderCommandSheet();
2661
+ }
2662
+ initCommandSheet();
2663
+
2664
+ var rpSubTab = 'overview';
2553
2665
  var rpIntervals = {}; // sub-tab name -> setInterval handle
2554
2666
  var rpHistoryAll = []; // full recent-session list (we paginate client-side)
2555
2667
  function setRpSubTab(name){
@@ -2568,7 +2680,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2568
2680
  s.classList.toggle('active', s.id==='rp-sub-'+name);
2569
2681
  }
2570
2682
  // stop every sub-tab interval, then start only the active one
2571
- var names = ['overview','incidents','live','voice','history','audit','fleet','config','setup'];
2683
+ var names = ['overview','incidents','live','voice','history','audit','fleet','config','commands','setup'];
2572
2684
  for(var k=0;k<names.length;k++){
2573
2685
  var n=names[k];
2574
2686
  if(rpIntervals[n]){ clearInterval(rpIntervals[n]); rpIntervals[n]=null; }
@@ -2579,7 +2691,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
2579
2691
  if(name==='voice'){ refreshVoiceCallOptions(); }
2580
2692
  if(name==='history'){ rpHistoryPage=0; pollHistory(); }
2581
2693
  if(name==='audit'){ pollAuditLog(); rpIntervals.audit = setInterval(pollAuditLog, 6000); }
2582
- if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
2694
+ if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
2695
+ if(name==='commands'){ renderCommandSheet(); }
2583
2696
  if(name==='setup'){ pollSettings(); loadCharacterPresets(); loadVoiceStacks(); pollRuntimeHealth(); }
2584
2697
  // remember last sub-tab in URL hash
2585
2698
  try{ history.replaceState(null,'','#robopark/'+name); }catch(e){}
@@ -2595,7 +2708,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2595
2708
  var h=String(location.hash||'').replace('#','');
2596
2709
  var m = h.match(/^robopark\\/(.+)$/);
2597
2710
  var initial = (m && m[1]) ? m[1] : 'overview';
2598
- if(['overview','incidents','live','voice','history','audit','fleet','config','setup'].indexOf(initial)<0) initial='overview';
2711
+ if(['overview','incidents','live','voice','history','audit','fleet','config','commands','setup'].indexOf(initial)<0) initial='overview';
2599
2712
  setRpSubTab(initial);
2600
2713
  }
2601
2714
 
@@ -3869,7 +3982,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
3869
3982
  var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null, pipelineTimer=null, deviceSelectorsReady=false;
3870
3983
  var previewOn=false, previewTimer=null; // on-demand operator camera preview
3871
3984
  var visionOn=false; // RoboVisionAI_PI overlay feed (direct MJPEG, not LiveKit)
3872
- var lkRoom=null, micAnalyser=null, audioCtx=null, lkAudioEls=[];
3985
+ var lkRoom=null, micAnalyser=null, audioCtx=null, lkAudioEls=[], micMonitorConnecting=false, micMonitorState='no active conversation session', micMonitorAudible=false, micMonitorVolume=0.8;
3873
3986
  var imgs={}; // preloaded robot concept avatars
3874
3987
  var TW=44, TH=22, PARK_W=15, PARK_D=8;
3875
3988
  // Plaza footprint and parking lot (upper-right corner of the plaza)
@@ -5958,8 +6071,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
5958
6071
  if(camAbort){ try{camAbort.abort();}catch(e){} camAbort=null; }
5959
6072
  if(drawerTimer){ clearInterval(drawerTimer); drawerTimer=null; }
5960
6073
  if(pipelineTimer){ clearInterval(pipelineTimer); pipelineTimer=null; }
5961
- if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
5962
- micAnalyser=null;
6074
+ if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
6075
+ lkAudioEls.forEach(function(el){try{el.pause();el.remove();}catch(e){}});lkAudioEls=[];
6076
+ micMonitorAudible=false;micMonitorConnecting=false;micMonitorState='no active conversation session';
6077
+ micAnalyser=null;
5963
6078
  }
5964
6079
 
5965
6080
  // ── setup flow for unjoined park pads ──
@@ -6139,9 +6254,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
6139
6254
  }
6140
6255
  fill('pk-video-device', video, device.video_device||'auto');
6141
6256
  fill('pk-audio-input', input, device.audio_device||'default');
6142
- fill('pk-audio-output', output, device.audio_output_device||'default');
6143
- var greetings=$('pk-greeting-phrases');
6144
- if(greetings && document.activeElement!==greetings) greetings.value=(device.greeting_phrases||[]).join(String.fromCharCode(10));
6257
+ fill('pk-audio-output', output, device.audio_output_device||'default');
6258
+ var selectedOutput=$('pk-audio-output'),selectedLabel=$('pk-spk-selected');
6259
+ if(selectedLabel&&selectedOutput){var selectedOption=selectedOutput.options&&selectedOutput.selectedIndex>=0?selectedOutput.options[selectedOutput.selectedIndex]:null;selectedLabel.textContent=selectedOption?String(selectedOption.textContent||selectedOption.value):String(selectedOutput.value||'default');}
6260
+ var greetings=$('pk-greeting-phrases');
6261
+ if(greetings && document.activeElement!==greetings) greetings.value=(device.greeting_phrases||[]).join(String.fromCharCode(10));
6262
+ var testPhrase=$('pk-spk-phrase');if(testPhrase&&document.activeElement!==testPhrase&&device.greeting_phrases&&device.greeting_phrases.length)testPhrase.value=device.greeting_phrases[0];
6145
6263
  var note=$('pk-device-note');
6146
6264
  if(note){
6147
6265
  if(!device.id && n && n.connected){
@@ -6529,13 +6647,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
6529
6647
  + '<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>'
6530
6648
  + '<div class="pk-note">Starts the camera, sends the same motion trigger used in production, and follows every reported stage remotely.</div>'
6531
6649
  + '<div class="pk-pipeline" id="pk-pipeline"></div>')
6532
- + 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>')
6650
+ + 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>')
6533
6651
  + 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>'
6534
- + '<div class="rp-form" id="pk-spk-test" style="grid-template-columns:1fr 1fr;gap:0.4rem;margin-top:0.55rem;padding:0.55rem;border-color:var(--border-strong);">'
6535
- + '<div class="rp-field"><label>Frequency</label><select id="pk-spk-freq"><option value="500">500 Hz (low)</option><option value="1000" selected>1000 Hz (1 kHz reference)</option><option value="2000">2000 Hz (presence)</option><option value="3000">3000 Hz (clarity)</option></select></div>'
6652
+ + '<div class="rp-form" id="pk-spk-test" style="grid-template-columns:1fr 1fr;gap:0.4rem;margin-top:0.55rem;padding:0.55rem;border-color:var(--border-strong);">'
6653
+ + '<div class="rp-field"><label>Test type</label><select id="pk-spk-mode"><option value="tts" selected>Cached character voice</option><option value="tone">Tone + mic round-trip</option></select></div>'
6654
+ + '<div class="rp-field"><label>Selected output</label><div class="pk-note" id="pk-spk-selected" style="margin:.45rem 0 0">loading device...</div></div>'
6655
+ + '<div class="rp-field" style="grid-column:1/-1"><label>Voice test phrase</label><input id="pk-spk-phrase" value="Hello, this is a RoboPark speaker test." maxlength="240"></div>'
6656
+ + '<div class="rp-field"><label>Frequency</label><select id="pk-spk-freq"><option value="500">500 Hz (low)</option><option value="1000" selected>1000 Hz (1 kHz reference)</option><option value="2000">2000 Hz (presence)</option><option value="3000">3000 Hz (clarity)</option></select></div>'
6536
6657
  + '<div class="rp-field"><label>Duration</label><select id="pk-spk-dur"><option value="0.4">0.4 s</option><option value="0.6" selected>0.6 s</option><option value="1.0">1.0 s</option></select></div>'
6537
6658
  + '<div style="grid-column:1/-1;display:flex;gap:0.4rem;align-items:center;">'
6538
- + '<button class="rp-btn primary" id="pk-spk-runtest" data-robot="'+esc(schedId(n))+'">▶ Test speaker</button>'
6659
+ + '<button class="rp-btn primary" id="pk-spk-runtest" data-robot="'+esc(schedId(n))+'">Play cached voice</button>'
6660
+ + '<button class="rp-btn" id="pk-spk-tone">Quick tone</button>'
6539
6661
  + '<span class="pk-note" id="pk-spk-test-stateline" style="margin:0">awaiting test</span>'
6540
6662
  + '</div>'
6541
6663
  + '<div style="grid-column:1/-1;" id="pk-spk-test-result"></div>'
@@ -6547,7 +6669,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6547
6669
  + sect('🩺 robot services','<div id="pk-svc-list" class="pk-svc-list"><div class="pk-note">waiting for supervisor report…</div></div><div class="pk-note" id="pk-svc-age" style="margin-top:0.35rem;"></div><div class="rp-field" style="margin-top:0.55rem;"><label>Quick run (on the robot, from packages/robopark/scheduler)</label><input id="pk-svc-clicmd" readonly value="python robot_supervisor.py"></div><button class="rp-btn" id="pk-svc-clicmd-copy" style="margin-top:0.35rem;">📋 Copy command</button>')
6548
6670
  + buildShellSection(schedId(n))
6549
6671
  + sect('🎭 character / voice','<div class="pk-note" id="pk-eff-note" style="margin-bottom:0.45rem;">loading effective config…</div><div class="rp-field"><label>Character preset</label><select class="rp-char" id="pk-eff-char" data-robot="'+esc(schedId(n))+'">'+effPresetOptions+'</select></div><div class="rp-field"><label>Voice stack</label><select class="rp-voice" id="pk-eff-voice" data-robot="'+esc(schedId(n))+'">'+effVoiceOptions+'</select></div><div class="rp-config-actions" style="margin-top:.45rem"><button class="rp-btn primary" id="pk-eff-save" data-robot="'+esc(schedId(n))+'">Apply to robot</button><button class="rp-btn" id="pk-edit-character">Edit character</button><button class="rp-btn" id="pk-edit-stack">Edit stack</button><button class="rp-btn" id="pk-new-stack">New stack</button></div>');
6550
- b.innerHTML += sect('Camera + audio devices','<div class="rp-field"><label>Search devices</label><input id="pk-device-search" placeholder="camera, USB, microphone, HDMI..." autocomplete="off"></div><div class="rp-field"><label>Camera input</label><select id="pk-video-device"></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input"></select></div><div class="rp-field"><label>Speaker / output inventory</label><select id="pk-audio-output"></select></div><div class="rp-field"><label>Preset greeting phrases <span class="pk-note">one per line, cached per voice</span></label><textarea id="pk-greeting-phrases" rows="3" placeholder="Hi, I am Jaguar! / Welcome to RoboPark."></textarea></div><button class="rp-btn" id="pk-device-save" style="margin-top:0.35rem">Save device selection + greetings</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
6672
+ b.innerHTML += sect('Camera + audio devices','<div class="rp-field"><label>Search devices</label><input id="pk-device-search" placeholder="camera, USB, microphone, HDMI..." autocomplete="off"></div><div class="rp-field"><label>Camera input</label><select id="pk-video-device"></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input"></select></div><div class="rp-field"><label>Speaker / output inventory</label><select id="pk-audio-output"></select></div><button class="rp-btn primary" id="pk-device-test-speaker" style="margin-top:0.35rem">Test selected speaker now</button><div class="rp-field"><label>Preset greeting phrases <span class="pk-note">one per line, cached per voice</span></label><textarea id="pk-greeting-phrases" rows="3" placeholder="Hi, I am Jaguar! / Welcome to RoboPark."></textarea></div><button class="rp-btn" id="pk-device-save" style="margin-top:0.35rem">Save device selection + greetings</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
6551
6673
  // Put live hardware selection beside the camera and audio tests,
6552
6674
  // ahead of persona and service administration in the Park drawer.
6553
6675
  var deviceInput=$('pk-device-search'), deviceSection=deviceInput&&deviceInput.closest('.pk-sect');
@@ -6556,11 +6678,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
6556
6678
  refreshDeviceSelectors(n);
6557
6679
  b.innerHTML += '<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn danger" id="pk-recover" data-robot="'+esc(n.name)+'">Recover robot</button><span class="pk-note" style="margin:0">clears stale sessions and queued triggers for this robot</span></div>';
6558
6680
  var deviceSearch=$('pk-device-search'); if(deviceSearch) deviceSearch.addEventListener('input', function(){ refreshDeviceSelectors(n); });
6559
- var deviceSave=$('pk-device-save'); if(deviceSave) deviceSave.addEventListener('click', function(){ saveDeviceSelectors(n); });
6681
+ var deviceSave=$('pk-device-save'); if(deviceSave) deviceSave.addEventListener('click', function(){ saveDeviceSelectors(n); });
6682
+ var outputSelect=$('pk-audio-output'); if(outputSelect) outputSelect.addEventListener('change', function(){ refreshDeviceSelectors(n); });
6683
+ var micListen=$('pk-mic-listen');if(micListen)micListen.addEventListener('click',function(){setMicMonitorAudible(!micMonitorAudible);});
6684
+ var micVolume=$('pk-mic-volume');if(micVolume)micVolume.addEventListener('input',function(){micMonitorVolume=Math.max(0,Math.min(1,Number(micVolume.value||80)/100));lkAudioEls.forEach(function(el){el.volume=micMonitorVolume;});});
6560
6685
  startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
6561
6686
  _bindShellSection(n);
6562
6687
  _spkTestRenderHistory(schedId(n));
6563
- var runSpkTest = $('pk-spk-runtest'); if(runSpkTest) runSpkTest.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n); });
6688
+ var runSpkTest = $('pk-spk-runtest'); if(runSpkTest) runSpkTest.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n); });
6689
+ var runSpkTone = $('pk-spk-tone'); if(runSpkTone) runSpkTone.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n,'tone'); });
6690
+ var quickSpkTest = $('pk-device-test-speaker'); if(quickSpkTest) quickSpkTest.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n,'tts'); });
6691
+ var spkMode = $('pk-spk-mode'); if(spkMode) spkMode.addEventListener('change', function(){ if(runSpkTest) runSpkTest.textContent=spkMode.value==='tts'?'Play cached voice':'Run tone test'; });
6564
6692
  var gl=$('pk-golive');
6565
6693
  if(gl){
6566
6694
  gl.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } goLive(n); });
@@ -6723,6 +6851,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6723
6851
  function startCam(n){
6724
6852
  idleCam('connecting');
6725
6853
  tryVisionOverlay(n, true);
6854
+ startMicMonitor(n);
6726
6855
  return;
6727
6856
  var note=$('pk-camnote');
6728
6857
  camAbort = ('AbortController' in window)?new AbortController():null;
@@ -6755,17 +6884,51 @@ button.act:disabled{opacity:0.5;cursor:default;}
6755
6884
  var res=$('pk-camres'); if(res) res.textContent = reason==='idle'?'idle':'—';
6756
6885
  var note=$('pk-camnote'); if(note) note.textContent = reason==='idle'?'the feed goes live automatically when the robot opens a session':msg;
6757
6886
  }
6758
- function attachLiveKit(info, n){
6887
+ function selectedRobotMic(){var el=$('pk-audio-input');if(!el)return 'default';var option=el.options&&el.selectedIndex>=0?el.options[el.selectedIndex]:null;return option?String(option.textContent||option.value):String(el.value||'default');}
6888
+ function selectedRobotOutput(){var el=$('pk-audio-output');if(!el)return 'default';var option=el.options&&el.selectedIndex>=0?el.options[el.selectedIndex]:null;return option?String(option.textContent||option.value):String(el.value||'default');}
6889
+ function setMicMonitorAudible(enabled){
6890
+ micMonitorAudible=!!enabled;
6891
+ var btn=$('pk-mic-listen'),note=$('pk-mic-listen-note');
6892
+ if(btn){btn.textContent=micMonitorAudible?'Mute browser monitor':'Listen in browser';btn.classList.toggle('primary',micMonitorAudible);}
6893
+ if(note)note.textContent=micMonitorAudible?(lkAudioEls.length?'Robot mic is playing in this browser. Use headphones.':'Listening armed — waiting for the robot mic track.'):'Browser monitoring muted.';
6894
+ lkAudioEls.forEach(function(el){
6895
+ el.muted=!micMonitorAudible;el.volume=micMonitorVolume;
6896
+ if(micMonitorAudible)el.play().catch(function(e){if(note)note.textContent='Browser blocked playback: '+e.message+' — press Listen again.';});
6897
+ });
6898
+ }
6899
+ function startMicMonitor(n){
6900
+ if(lkRoom||micMonitorConnecting)return;
6901
+ micMonitorConnecting=true;
6902
+ micMonitorState='checking active conversation session';
6903
+ var state=$('pk-micstate');if(state)state.textContent='selected '+selectedRobotMic()+' · checking active session...';
6904
+ fetch('/robopark/api/robots/'+encodeURIComponent(schedId(n))+'/stream'+QS,{cache:'no-store'})
6905
+ .then(function(r){if(!r.ok)throw new Error('scheduler '+r.status);return r.json();})
6906
+ .then(function(info){
6907
+ micMonitorConnecting=false;
6908
+ if(info&&info.active&&info.url&&info.token&&window.LivekitClient){micMonitorState='connecting to '+info.url;attachLiveKit(info,n,true);return;}
6909
+ micMonitorState=info&&info.reason?String(info.reason).replace(/_/g,' '):'waiting for conversation session';
6910
+ if(state)state.textContent='selected '+selectedRobotMic()+' · waiting for conversation session';
6911
+ })
6912
+ .catch(function(e){micMonitorConnecting=false;micMonitorState='monitor unavailable: '+e.message;if(state)state.textContent='selected '+selectedRobotMic()+' · '+micMonitorState;});
6913
+ }
6914
+ function attachLiveKit(info, n, audioOnly){
6759
6915
  var note=$('pk-camnote'), res=$('pk-camres');
6760
6916
  try{
6761
6917
  var LK=window.LivekitClient; lkRoom=new LK.Room();
6762
6918
  lkRoom.on(LK.RoomEvent.TrackSubscribed, function(track){
6763
- if(track.kind==='video'){
6764
- var el=track.attach(); el.setAttribute('playsinline',''); el.muted=true; el.autoplay=true;
6919
+ if(track.kind==='video'){
6920
+ if(audioOnly)return;
6921
+ var el=track.attach(); el.setAttribute('playsinline',''); el.muted=true; el.autoplay=true;
6765
6922
  var cv=$('pk-camcv'); if(cv&&cv.parentNode) cv.parentNode.replaceChild(el, cv);
6766
6923
  if(res) res.textContent='LIVE'; if(note) note.textContent='live camera via LiveKit · '+esc(info.room);
6767
- } else if(track.kind==='audio'){
6768
- hookMicAnalyser(track);
6924
+ } else if(track.kind==='audio'){
6925
+ // Listen only to the robot's published microphone. Agent TTS may
6926
+ // also be present in the room, but hearing that directly would
6927
+ // bypass the physical speaker -> microphone round-trip test.
6928
+ if(track.name&&track.name!=='microphone')return;
6929
+ hookMicAnalyser(track);
6930
+ micMonitorState='live track published';
6931
+ var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · live track published';
6769
6932
  // Muted by default: this is the camera-preview panel, running on
6770
6933
  // the SAME physical device as the robot in local testing. Auto-
6771
6934
  // playing the subscribed track (robot mic, or later the agent's
@@ -6774,16 +6937,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
6774
6937
  // that drowns out the actual conversation. The VU meter above
6775
6938
  // (hookMicAnalyser) taps the raw track directly and works fine
6776
6939
  // muted.
6777
- var audio=track.attach(); audio.autoplay=true; audio.controls=false; audio.muted=true; audio.style.display='none'; document.body.appendChild(audio); lkAudioEls.push(audio);
6778
- audio.play().catch(function(){});
6940
+ var audio=track.attach(); audio.autoplay=true; audio.controls=false; audio.muted=!micMonitorAudible; audio.volume=micMonitorVolume;audio.style.display='none'; document.body.appendChild(audio); lkAudioEls.push(audio);
6941
+ var listenNote=$('pk-mic-listen-note');if(listenNote&&micMonitorAudible)listenNote.textContent='Robot mic is playing in this browser. Use headphones.';
6942
+ audio.play().catch(function(){});
6779
6943
  }
6780
6944
  });
6781
- lkRoom.on(LK.RoomEvent.Disconnected, function(){ micAnalyser=null; if(note) note.textContent='session ended'; if(res) res.textContent='—'; });
6945
+ lkRoom.on(LK.RoomEvent.Disconnected, function(){lkRoom=null;micAnalyser=null;micMonitorConnecting=false;micMonitorState='session ended';var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · session ended';if(!audioOnly&&note)note.textContent='session ended';if(!audioOnly&&res)res.textContent='—';});
6782
6946
  var livekitUrl=info.url;
6783
6947
  if((location.hostname==='localhost'||location.hostname==='127.0.0.1') && /^ws:\\/\\/localhost(?::|\\/)/i.test(livekitUrl)) livekitUrl=livekitUrl.replace(/^ws:\\/\\/localhost/i,'ws://127.0.0.1');
6784
- lkRoom.connect(livekitUrl, info.token).then(function(){ if(note) note.textContent='connected · waiting for camera track…'; })
6785
- .catch(function(e){ idleCam('unreachable'); if(note) note.textContent='LiveKit connection failed'; if(window.console) console.error('RoboPark LiveKit connection failed',e); });
6786
- }catch(e){ idleCam('no-sdk'); }
6948
+ lkRoom.connect(livekitUrl, info.token).then(function(){micMonitorState='connected, waiting for robot mic publication';var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · '+micMonitorState;if(!audioOnly&&note)note.textContent='connected · waiting for camera track…';})
6949
+ .catch(function(e){lkRoom=null;micMonitorConnecting=false;micMonitorState='LiveKit connection failed: '+(e&&e.message?e.message:'unreachable');var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · '+micMonitorState;if(!audioOnly){idleCam('unreachable');if(note)note.textContent='LiveKit connection failed';}if(window.console)console.error('RoboPark LiveKit connection failed',e);});
6950
+ }catch(e){ micMonitorConnecting=false;micMonitorState='LiveKit SDK unavailable';idleCam('no-sdk'); }
6787
6951
  }
6788
6952
  // Real mic level: tap the robot's subscribed 'mic' audio track with a Web
6789
6953
  // Audio analyser and read RMS in startMeters (no invented levels).
@@ -6824,7 +6988,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6824
6988
  if(micAnalyser){ micAnalyser.getByteTimeDomainData(buf); var sum=0; for(var q=0;q<buf.length;q++){ var v=(buf[q]-128)/128; sum+=v*v; } lvl=Math.min(1, Math.sqrt(sum/buf.length)*3.4); }
6825
6989
  for(var i=0;i<mi.length;i++){ var on=i<lvl*mi.length; mi[i].style.height=(3+(on?lvl*13:0))+'px'; mi[i].style.background=on?(i<mi.length*0.7?'var(--ok)':i<mi.length*0.9?'var(--warn)':'var(--bad)'):'var(--glass-border)'; }
6826
6990
  set('pk-micval', micAnalyser?Math.round(lvl*100)+'%':'—');
6827
- set('pk-micstate', micAnalyser?(lvl>0.12?'live · hearing audio':'live · quiet'):'no live mic track');
6991
+ set('pk-micstate', micAnalyser?('selected '+selectedRobotMic()+' · '+(lvl>0.12?'live, hearing audio':'live, quiet')):('selected '+selectedRobotMic()+' · '+micMonitorState));
6828
6992
  // SPEAKER — the Pi plays TTS out its own speaker; we don't get a
6829
6993
  // published track, so the meter stays at "—" unless a speaker
6830
6994
  // round-trip test is currently running. While a test plays, the
@@ -6900,7 +7064,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6900
7064
  var mark = t.pass===true ? '✓' : (t.pass===false ? '✗' : '·');
6901
7065
  var when = new Date(t.t).toTimeString().slice(0,8);
6902
7066
  var peak = t.peak_db != null ? (t.peak_db+' dB') : '—';
6903
- var freq = t.frequency != null ? (Math.round(t.frequency)+' Hz') : '—';
7067
+ var freq = t.mode==='tts' ? 'cached voice' : (t.frequency != null ? (Math.round(t.frequency)+' Hz') : '—');
6904
7068
  return '<div class="pk-spk-hist-row"><span class="pk-spk-mark" style="color:'+cls+'">'+mark+'</span>'
6905
7069
  + '<span class="pk-spk-when">'+when+'</span>'
6906
7070
  + '<span class="pk-spk-freq">'+freq+'</span>'
@@ -6923,14 +7087,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
6923
7087
  + '</div>';
6924
7088
  return;
6925
7089
  }
6926
- var banner = result.pass
6927
- ? '<div class="pk-spk-banner ok">✓ speaker round-trip OK</div>'
7090
+ var isTts=result.mode==='tts';
7091
+ var banner = result.pass
7092
+ ? '<div class="pk-spk-banner ok">✓ '+(isTts?'cached voice playback OK':'speaker round-trip OK')+'</div>'
6928
7093
  : '<div class="pk-spk-banner bad">✗ no signal detected</div>';
6929
7094
  var meta = '<div class="pk-spk-meta">'
6930
7095
  + '<div><span class="l">peak</span><span class="v">'+esc(result.recorded_peak_db+' dB')+'</span></div>'
6931
7096
  + '<div><span class="l">RMS</span><span class="v">'+esc(result.recorded_rms_db+' dB')+'</span></div>'
6932
7097
  + '<div><span class="l">threshold</span><span class="v">'+esc(result.threshold_db+' dB')+'</span></div>'
6933
- + '<div><span class="l">frequency</span><span class="v">'+esc(Math.round(result.frequency)+' Hz')+'</span></div>'
7098
+ + (isTts?'<div><span class="l">voice</span><span class="v">'+esc((result.tts_provider||'tts')+' / '+(result.tts_voice||'default'))+'</span></div><div><span class="l">cache</span><span class="v">'+(result.cache_hit?'hit':'generated')+'</span></div>':'<div><span class="l">frequency</span><span class="v">'+esc(Math.round(result.frequency)+' Hz')+'</span></div>')
6934
7099
  + '<div><span class="l">duration</span><span class="v">'+esc(result.duration+' s')+'</span></div>'
6935
7100
  + '<div><span class="l">latency</span><span class="v">'+esc(result.duration_ms+' ms')+'</span></div>'
6936
7101
  + '<div><span class="l">output</span><span class="v">'+esc(result.output_device)+'</span></div>'
@@ -6940,29 +7105,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
6940
7105
  host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
6941
7106
  + banner + meta + diag + '</div>';
6942
7107
  }
6943
- function runSpeakerTest(n){
6944
- var robotId = schedId(n);
6945
- var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = true; btn.textContent = 'testing…'; }
7108
+ function runSpeakerTest(n, forcedMode){
7109
+ var robotId = schedId(n), mode=forcedMode||(($('pk-spk-mode')||{}).value||'tts');
7110
+ var buttons=[$('pk-spk-runtest'),$('pk-spk-tone'),$('pk-device-test-speaker')];buttons.forEach(function(b){if(b)b.disabled=true;});
7111
+ var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = true; btn.textContent = mode==='tts'?'loading voice…':'testing…'; }
6946
7112
  var status = $('pk-spk-test-stateline');
6947
- if(status) status.textContent = 'queued — waiting for robot…';
6948
- var freq = parseFloat(($('pk-spk-freq')||{}).value || '1000');
6949
- var dur = parseFloat(($('pk-spk-dur')||{}).value || '0.6');
7113
+ if(status) status.textContent = mode==='tts'?'loading cached character voice…':'queued — waiting for robot…';
7114
+ var freq = parseFloat(($('pk-spk-freq')||{}).value || '1000');
7115
+ var dur = parseFloat(($('pk-spk-dur')||{}).value || '0.6');
7116
+ var phrase=String((($('pk-spk-phrase')||{}).value)||'Hello, I am ready to talk.').trim();
7117
+ var output=(($('pk-audio-output')||{}).value)||'default';
7118
+ var input=(($('pk-audio-input')||{}).value)||'default';
6950
7119
  // Animate the meter with a synthetic "during-test" peak so the
6951
7120
  // operator sees something happen immediately while waiting for
6952
7121
  // the real recording.
6953
7122
  _spkTestSetLive(robotId, {live:true, rms: 0.45, pass: undefined, waveform: null});
6954
7123
  rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',
6955
- {frequency: freq, duration: dur, amplitude: 0.6, threshold_db: -30})
7124
+ {mode:mode, text:phrase, frequency:freq, duration:dur, amplitude:0.6, threshold_db:-30, output:output, input:input})
6956
7125
  .then(function(q){
6957
7126
  // long-poll up to 8s, then fall back to 12s of result polling
6958
- if(status) status.textContent = 'playing tone + recording…';
7127
+ if(status) status.textContent = mode==='tts'?'playing cached voice on '+selectedRobotOutput()+'…':'playing tone + recording…';
6959
7128
  return _shellWaitForResult(robotId, q.request_id, q.device_id || robotId, '', 'speaker-test');
6960
7129
  })
6961
7130
  .catch(function(e){
6962
7131
  if(status) status.textContent = '✗ test failed: '+e.message;
6963
- _spkTestSetLive(robotId, {live:false});
6964
- _spkTestRenderResult(robotId, {ok:false, error:e.message});
6965
- if(btn){ btn.disabled = false; btn.textContent = '▶ Test speaker'; }
7132
+ _spkTestSetLive(robotId, {live:false});
7133
+ _spkTestRenderResult(robotId, {ok:false, error:e.message});
7134
+ buttons.forEach(function(b){if(b)b.disabled=false;});
7135
+ if(btn){ btn.disabled = false; btn.textContent = mode==='tts'?'Play cached voice':'Run tone test'; }
6966
7136
  });
6967
7137
  }
6968
7138
  // Render a synthetic "during-test" waveform so the meter animates
@@ -6975,7 +7145,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
6975
7145
  t: Date.now(),
6976
7146
  pass: result && result.ok ? (result.pass===true) : null,
6977
7147
  peak_db: result && result.recorded_peak_db != null ? result.recorded_peak_db : null,
6978
- frequency: result && result.frequency != null ? result.frequency : null,
7148
+ frequency: result && result.frequency != null ? result.frequency : null,
7149
+ mode: result && result.mode || 'tone',
6979
7150
  diagnostic: result && result.diagnostic || (result && !result.ok ? result.error : null),
6980
7151
  });
6981
7152
  while(list.length > 5) list.pop();
@@ -6990,15 +7161,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
6990
7161
  _origRenderShellResult(robotId, result, label);
6991
7162
  if(label === 'speaker-test'){
6992
7163
  _spkTestSetLive(robotId, {live:false});
6993
- _spkTestMark(robotId, result);
6994
- _spkTestRenderResult(robotId, result);
7164
+ _spkTestMark(robotId, result);
7165
+ _spkTestRenderResult(robotId, result);
7166
+ [$('pk-spk-runtest'),$('pk-spk-tone'),$('pk-device-test-speaker')].forEach(function(b){if(b)b.disabled=false;});
6995
7167
  var status = $('pk-spk-test-stateline');
6996
7168
  if(status){
6997
7169
  if(!result || !result.ok){ status.textContent = '✗ test failed'; }
6998
7170
  else if(result.pass){ status.textContent = '✓ pass · '+result.recorded_peak_db+' dB peak'; }
6999
7171
  else { status.textContent = '✗ no signal — '+result.recorded_peak_db+' dB peak'; }
7000
7172
  }
7001
- var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = ' Test speaker'; }
7173
+ var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = (($('pk-spk-mode')||{}).value==='tone'?'Run tone test':'Play cached voice'); }
7002
7174
  }
7003
7175
  };
7004
7176
 
@@ -76,6 +76,8 @@ export async function roboparkRobotRuntime(opts) {
76
76
  const network = opts.network ?? 'lan';
77
77
  const foreground = opts.foreground === true;
78
78
  const cli = roboparkCliPath();
79
+ const hub = new URL(opts.hubUrl);
80
+ const livekitUrl = `${hub.protocol === 'https:' ? 'wss:' : 'ws:'}//${hub.hostname}:7880`;
79
81
  const children = [];
80
82
  let stopping = false;
81
83
  let recycling = false;
@@ -111,6 +113,7 @@ export async function roboparkRobotRuntime(opts) {
111
113
  // Keep this in the child environment, never in the persisted robot config.
112
114
  env: {
113
115
  ROBOPARK_MESH_TOKEN: opts.token,
116
+ ROBOPARK_LIVEKIT_URL: livekitUrl,
114
117
  // Keep internal service wiring out of the Commander argument boundary.
115
118
  // This also lets preview start while RoboVision is still warming up.
116
119
  ROBOVISION_URL: opts.vision !== false ? 'http://127.0.0.1:5000' : '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.73",
3
+ "version": "2.8.76",
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",