infinicode 2.8.72 → 2.8.75
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.
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +240 -61
- package/dist/kernel/federation/transport-http.js +14 -1
- package/dist/robopark/robot-runtime.js +3 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +103 -20
- package/packages/robopark/scheduler/preview_agent.py +80 -24
- package/packages/robopark/scheduler/robot_supervisor.py +41 -16
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
export const DASHBOARD_HTML = `<!doctype html>
|
|
25
25
|
<html lang="en">
|
|
26
26
|
<head>
|
|
27
|
-
<meta charset="utf-8">
|
|
28
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
29
|
-
<
|
|
27
|
+
<meta charset="utf-8">
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
29
|
+
<meta name="robopark-ui-build" content="voice-media-20260716-3">
|
|
30
|
+
<title>RoboPark Control</title>
|
|
30
31
|
<style>
|
|
31
32
|
/* ── premium tech glass theme ── dark is the committed default; light is a frosted toggle ── */
|
|
32
33
|
:root{
|
|
@@ -690,6 +691,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
690
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}}
|
|
691
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}}
|
|
692
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
|
+
|
|
693
703
|
.rp-fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:0.7rem;padding:0;}
|
|
694
704
|
.rp-fleet-card{position:relative;}
|
|
695
705
|
.rp-fleet-card .spark{margin-top:0.5rem;height:36px;}
|
|
@@ -830,6 +840,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
830
840
|
<button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
|
|
831
841
|
<button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
|
|
832
842
|
<button class="rp-subtab" data-sub="config" id="rpsbtn-config" role="tab" aria-selected="false">Characters & Voice</button>
|
|
843
|
+
<button class="rp-subtab" data-sub="commands" id="rpsbtn-commands" role="tab" aria-selected="false">Commands</button>
|
|
833
844
|
<button class="rp-subtab" data-sub="setup" id="rpsbtn-setup" role="tab" aria-selected="false">Setup</button>
|
|
834
845
|
</div>
|
|
835
846
|
|
|
@@ -896,7 +907,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
896
907
|
<div class="rp-call-device"><label>Camera</label><select id="rp-call-cam-device"><option value="">system default</option></select></div>
|
|
897
908
|
<div class="rp-call-device"><label>Speaker output</label><select id="rp-call-speaker-device"><option value="">system default</option></select></div>
|
|
898
909
|
<div class="rp-call-device-note" id="rp-call-device-note">Enable media access to reveal the actual microphone, camera, and speaker names available to this browser.</div>
|
|
899
|
-
<div class="rp-call-device-actions"><button class="rp-btn primary" id="rp-call-refresh-devices">Enable
|
|
910
|
+
<div class="rp-call-device-actions"><button class="rp-btn primary" id="rp-call-refresh-devices">Enable media access</button><button class="rp-btn" id="rp-call-test-mic">Test microphone</button></div>
|
|
900
911
|
</div>
|
|
901
912
|
</aside>
|
|
902
913
|
<main class="rp-call-stage">
|
|
@@ -1071,7 +1082,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1071
1082
|
</div>
|
|
1072
1083
|
|
|
1073
1084
|
<!-- ═══ Setup ═══ -->
|
|
1074
|
-
<div class="rp-sub" id="rp-sub-setup">
|
|
1085
|
+
<div class="rp-sub" id="rp-sub-setup">
|
|
1075
1086
|
<!-- Add LiveKit server -->
|
|
1076
1087
|
<div class="rp-form" id="rp-addserver">
|
|
1077
1088
|
<h3>Add LiveKit server</h3>
|
|
@@ -1131,6 +1142,38 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1131
1142
|
|
|
1132
1143
|
</div>
|
|
1133
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
|
+
|
|
1134
1177
|
<!-- Character + voice management -->
|
|
1135
1178
|
<div class="rp-sub" id="rp-sub-config">
|
|
1136
1179
|
<div class="rp-config-workspace" id="rp-templates">
|
|
@@ -1178,8 +1221,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1178
1221
|
|
|
1179
1222
|
<script>
|
|
1180
1223
|
(function(){
|
|
1181
|
-
var TOKEN = new URLSearchParams(location.search).get('token') || '';
|
|
1182
|
-
var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';
|
|
1224
|
+
var TOKEN = new URLSearchParams(location.search).get('token') || '';
|
|
1225
|
+
var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';
|
|
1226
|
+
var UI_BUILD=(document.querySelector('meta[name="robopark-ui-build"]')||{}).content||'unknown';
|
|
1227
|
+
fetch('/fed/ui-build'+QS,{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(info){
|
|
1228
|
+
if(!info||!info.build||info.build===UI_BUILD)return;
|
|
1229
|
+
var key='rp-ui-reload-'+info.build;if(sessionStorage.getItem(key))return;sessionStorage.setItem(key,'1');
|
|
1230
|
+
var url=new URL(location.href);url.searchParams.set('ui',info.build);location.replace(url.toString());
|
|
1231
|
+
}).catch(function(){});
|
|
1183
1232
|
var selfId = null, selected = null, lastSeq = -1, everOk = false;
|
|
1184
1233
|
var rpReadOnly = false, pmBusy = false;
|
|
1185
1234
|
var lastSchedulerPollAt = 0; // updated by pollRobopark() on every successful /api/telemetry fetch
|
|
@@ -1775,7 +1824,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1775
1824
|
$('pane-fleet').classList.toggle('active', name==='fleet');
|
|
1776
1825
|
$('pane-park').classList.toggle('active', name==='park');
|
|
1777
1826
|
$('pane-robopark').classList.toggle('active', name==='robopark');
|
|
1778
|
-
|
|
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){} }
|
|
1779
1830
|
if (name==='robopark'){
|
|
1780
1831
|
pollRobopark();
|
|
1781
1832
|
initRpSubTabs();
|
|
@@ -1786,11 +1837,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1786
1837
|
var fleetSessionsBtn=$('fleet-open-sessions'); if(fleetSessionsBtn) fleetSessionsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('live');});
|
|
1787
1838
|
var fleetIncidentsBtn=$('fleet-open-incidents'); if(fleetIncidentsBtn) fleetIncidentsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('incidents');});
|
|
1788
1839
|
var fleetRefreshBtn=$('fleet-refresh'); if(fleetRefreshBtn) fleetRefreshBtn.addEventListener('click',function(){poll();pollRobopark();});
|
|
1789
|
-
var initHash = location.hash.replace('#','');
|
|
1790
|
-
// Defer tab init until Park module is defined (same outer IIFE, later in source).
|
|
1791
|
-
function initTab(){
|
|
1792
|
-
if (initHash==='robopark'||initHash
|
|
1793
|
-
|
|
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
|
+
}
|
|
1794
1846
|
// initTab() is called at the very bottom of this IIFE, after Park is assigned.
|
|
1795
1847
|
|
|
1796
1848
|
function robotsFrom(d){
|
|
@@ -2542,7 +2594,74 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2542
2594
|
}
|
|
2543
2595
|
|
|
2544
2596
|
// ── Sub-tab routing ──
|
|
2545
|
-
|
|
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';
|
|
2546
2665
|
var rpIntervals = {}; // sub-tab name -> setInterval handle
|
|
2547
2666
|
var rpHistoryAll = []; // full recent-session list (we paginate client-side)
|
|
2548
2667
|
function setRpSubTab(name){
|
|
@@ -2561,7 +2680,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2561
2680
|
s.classList.toggle('active', s.id==='rp-sub-'+name);
|
|
2562
2681
|
}
|
|
2563
2682
|
// stop every sub-tab interval, then start only the active one
|
|
2564
|
-
var names = ['overview','incidents','live','voice','history','audit','fleet','config','setup'];
|
|
2683
|
+
var names = ['overview','incidents','live','voice','history','audit','fleet','config','commands','setup'];
|
|
2565
2684
|
for(var k=0;k<names.length;k++){
|
|
2566
2685
|
var n=names[k];
|
|
2567
2686
|
if(rpIntervals[n]){ clearInterval(rpIntervals[n]); rpIntervals[n]=null; }
|
|
@@ -2572,7 +2691,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2572
2691
|
if(name==='voice'){ refreshVoiceCallOptions(); }
|
|
2573
2692
|
if(name==='history'){ rpHistoryPage=0; pollHistory(); }
|
|
2574
2693
|
if(name==='audit'){ pollAuditLog(); rpIntervals.audit = setInterval(pollAuditLog, 6000); }
|
|
2575
|
-
if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
|
|
2694
|
+
if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
|
|
2695
|
+
if(name==='commands'){ renderCommandSheet(); }
|
|
2576
2696
|
if(name==='setup'){ pollSettings(); loadCharacterPresets(); loadVoiceStacks(); pollRuntimeHealth(); }
|
|
2577
2697
|
// remember last sub-tab in URL hash
|
|
2578
2698
|
try{ history.replaceState(null,'','#robopark/'+name); }catch(e){}
|
|
@@ -2588,7 +2708,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2588
2708
|
var h=String(location.hash||'').replace('#','');
|
|
2589
2709
|
var m = h.match(/^robopark\\/(.+)$/);
|
|
2590
2710
|
var initial = (m && m[1]) ? m[1] : 'overview';
|
|
2591
|
-
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';
|
|
2592
2712
|
setRpSubTab(initial);
|
|
2593
2713
|
}
|
|
2594
2714
|
|
|
@@ -3862,7 +3982,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
3862
3982
|
var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null, pipelineTimer=null, deviceSelectorsReady=false;
|
|
3863
3983
|
var previewOn=false, previewTimer=null; // on-demand operator camera preview
|
|
3864
3984
|
var visionOn=false; // RoboVisionAI_PI overlay feed (direct MJPEG, not LiveKit)
|
|
3865
|
-
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;
|
|
3866
3986
|
var imgs={}; // preloaded robot concept avatars
|
|
3867
3987
|
var TW=44, TH=22, PARK_W=15, PARK_D=8;
|
|
3868
3988
|
// Plaza footprint and parking lot (upper-right corner of the plaza)
|
|
@@ -5951,8 +6071,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5951
6071
|
if(camAbort){ try{camAbort.abort();}catch(e){} camAbort=null; }
|
|
5952
6072
|
if(drawerTimer){ clearInterval(drawerTimer); drawerTimer=null; }
|
|
5953
6073
|
if(pipelineTimer){ clearInterval(pipelineTimer); pipelineTimer=null; }
|
|
5954
|
-
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
5955
|
-
|
|
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;
|
|
5956
6078
|
}
|
|
5957
6079
|
|
|
5958
6080
|
// ── setup flow for unjoined park pads ──
|
|
@@ -6132,9 +6254,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6132
6254
|
}
|
|
6133
6255
|
fill('pk-video-device', video, device.video_device||'auto');
|
|
6134
6256
|
fill('pk-audio-input', input, device.audio_device||'default');
|
|
6135
|
-
fill('pk-audio-output', output, device.audio_output_device||'default');
|
|
6136
|
-
var
|
|
6137
|
-
if(
|
|
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];
|
|
6138
6263
|
var note=$('pk-device-note');
|
|
6139
6264
|
if(note){
|
|
6140
6265
|
if(!device.id && n && n.connected){
|
|
@@ -6522,13 +6647,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6522
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>'
|
|
6523
6648
|
+ '<div class="pk-note">Starts the camera, sends the same motion trigger used in production, and follows every reported stage remotely.</div>'
|
|
6524
6649
|
+ '<div class="pk-pipeline" id="pk-pipeline"></div>')
|
|
6525
|
-
+ 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>')
|
|
6526
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>'
|
|
6527
|
-
+ '<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);">'
|
|
6528
|
-
+ '<div class="rp-field"><label>
|
|
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>'
|
|
6529
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>'
|
|
6530
6658
|
+ '<div style="grid-column:1/-1;display:flex;gap:0.4rem;align-items:center;">'
|
|
6531
|
-
+ '<button class="rp-btn primary" id="pk-spk-runtest" data-robot="'+esc(schedId(n))+'"
|
|
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>'
|
|
6532
6661
|
+ '<span class="pk-note" id="pk-spk-test-stateline" style="margin:0">awaiting test</span>'
|
|
6533
6662
|
+ '</div>'
|
|
6534
6663
|
+ '<div style="grid-column:1/-1;" id="pk-spk-test-result"></div>'
|
|
@@ -6540,7 +6669,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6540
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>')
|
|
6541
6670
|
+ buildShellSection(schedId(n))
|
|
6542
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>');
|
|
6543
|
-
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>');
|
|
6544
6673
|
// Put live hardware selection beside the camera and audio tests,
|
|
6545
6674
|
// ahead of persona and service administration in the Park drawer.
|
|
6546
6675
|
var deviceInput=$('pk-device-search'), deviceSection=deviceInput&&deviceInput.closest('.pk-sect');
|
|
@@ -6549,11 +6678,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6549
6678
|
refreshDeviceSelectors(n);
|
|
6550
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>';
|
|
6551
6680
|
var deviceSearch=$('pk-device-search'); if(deviceSearch) deviceSearch.addEventListener('input', function(){ refreshDeviceSelectors(n); });
|
|
6552
|
-
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;});});
|
|
6553
6685
|
startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
|
|
6554
6686
|
_bindShellSection(n);
|
|
6555
6687
|
_spkTestRenderHistory(schedId(n));
|
|
6556
|
-
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'; });
|
|
6557
6692
|
var gl=$('pk-golive');
|
|
6558
6693
|
if(gl){
|
|
6559
6694
|
gl.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } goLive(n); });
|
|
@@ -6716,6 +6851,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6716
6851
|
function startCam(n){
|
|
6717
6852
|
idleCam('connecting');
|
|
6718
6853
|
tryVisionOverlay(n, true);
|
|
6854
|
+
startMicMonitor(n);
|
|
6719
6855
|
return;
|
|
6720
6856
|
var note=$('pk-camnote');
|
|
6721
6857
|
camAbort = ('AbortController' in window)?new AbortController():null;
|
|
@@ -6748,17 +6884,51 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6748
6884
|
var res=$('pk-camres'); if(res) res.textContent = reason==='idle'?'idle':'—';
|
|
6749
6885
|
var note=$('pk-camnote'); if(note) note.textContent = reason==='idle'?'the feed goes live automatically when the robot opens a session':msg;
|
|
6750
6886
|
}
|
|
6751
|
-
function
|
|
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){
|
|
6752
6915
|
var note=$('pk-camnote'), res=$('pk-camres');
|
|
6753
6916
|
try{
|
|
6754
6917
|
var LK=window.LivekitClient; lkRoom=new LK.Room();
|
|
6755
6918
|
lkRoom.on(LK.RoomEvent.TrackSubscribed, function(track){
|
|
6756
|
-
if(track.kind==='video'){
|
|
6757
|
-
|
|
6919
|
+
if(track.kind==='video'){
|
|
6920
|
+
if(audioOnly)return;
|
|
6921
|
+
var el=track.attach(); el.setAttribute('playsinline',''); el.muted=true; el.autoplay=true;
|
|
6758
6922
|
var cv=$('pk-camcv'); if(cv&&cv.parentNode) cv.parentNode.replaceChild(el, cv);
|
|
6759
6923
|
if(res) res.textContent='LIVE'; if(note) note.textContent='live camera via LiveKit · '+esc(info.room);
|
|
6760
|
-
} else if(track.kind==='audio'){
|
|
6761
|
-
|
|
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';
|
|
6762
6932
|
// Muted by default: this is the camera-preview panel, running on
|
|
6763
6933
|
// the SAME physical device as the robot in local testing. Auto-
|
|
6764
6934
|
// playing the subscribed track (robot mic, or later the agent's
|
|
@@ -6767,16 +6937,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6767
6937
|
// that drowns out the actual conversation. The VU meter above
|
|
6768
6938
|
// (hookMicAnalyser) taps the raw track directly and works fine
|
|
6769
6939
|
// muted.
|
|
6770
|
-
var audio=track.attach(); audio.autoplay=true; audio.controls=false; audio.muted
|
|
6771
|
-
|
|
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(){});
|
|
6772
6943
|
}
|
|
6773
6944
|
});
|
|
6774
|
-
lkRoom.on(LK.RoomEvent.Disconnected, function(){
|
|
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&¬e)note.textContent='session ended';if(!audioOnly&&res)res.textContent='—';});
|
|
6775
6946
|
var livekitUrl=info.url;
|
|
6776
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');
|
|
6777
|
-
lkRoom.connect(livekitUrl, info.token).then(function(){ if(
|
|
6778
|
-
.catch(function(e){
|
|
6779
|
-
}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&¬e)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'); }
|
|
6780
6951
|
}
|
|
6781
6952
|
// Real mic level: tap the robot's subscribed 'mic' audio track with a Web
|
|
6782
6953
|
// Audio analyser and read RMS in startMeters (no invented levels).
|
|
@@ -6817,7 +6988,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6817
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); }
|
|
6818
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)'; }
|
|
6819
6990
|
set('pk-micval', micAnalyser?Math.round(lvl*100)+'%':'—');
|
|
6820
|
-
set('pk-micstate', micAnalyser?(lvl>0.12?'live
|
|
6991
|
+
set('pk-micstate', micAnalyser?('selected '+selectedRobotMic()+' · '+(lvl>0.12?'live, hearing audio':'live, quiet')):('selected '+selectedRobotMic()+' · '+micMonitorState));
|
|
6821
6992
|
// SPEAKER — the Pi plays TTS out its own speaker; we don't get a
|
|
6822
6993
|
// published track, so the meter stays at "—" unless a speaker
|
|
6823
6994
|
// round-trip test is currently running. While a test plays, the
|
|
@@ -6893,7 +7064,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6893
7064
|
var mark = t.pass===true ? '✓' : (t.pass===false ? '✗' : '·');
|
|
6894
7065
|
var when = new Date(t.t).toTimeString().slice(0,8);
|
|
6895
7066
|
var peak = t.peak_db != null ? (t.peak_db+' dB') : '—';
|
|
6896
|
-
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') : '—');
|
|
6897
7068
|
return '<div class="pk-spk-hist-row"><span class="pk-spk-mark" style="color:'+cls+'">'+mark+'</span>'
|
|
6898
7069
|
+ '<span class="pk-spk-when">'+when+'</span>'
|
|
6899
7070
|
+ '<span class="pk-spk-freq">'+freq+'</span>'
|
|
@@ -6916,14 +7087,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6916
7087
|
+ '</div>';
|
|
6917
7088
|
return;
|
|
6918
7089
|
}
|
|
6919
|
-
var
|
|
6920
|
-
|
|
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>'
|
|
6921
7093
|
: '<div class="pk-spk-banner bad">✗ no signal detected</div>';
|
|
6922
7094
|
var meta = '<div class="pk-spk-meta">'
|
|
6923
7095
|
+ '<div><span class="l">peak</span><span class="v">'+esc(result.recorded_peak_db+' dB')+'</span></div>'
|
|
6924
7096
|
+ '<div><span class="l">RMS</span><span class="v">'+esc(result.recorded_rms_db+' dB')+'</span></div>'
|
|
6925
7097
|
+ '<div><span class="l">threshold</span><span class="v">'+esc(result.threshold_db+' dB')+'</span></div>'
|
|
6926
|
-
+ '<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>')
|
|
6927
7099
|
+ '<div><span class="l">duration</span><span class="v">'+esc(result.duration+' s')+'</span></div>'
|
|
6928
7100
|
+ '<div><span class="l">latency</span><span class="v">'+esc(result.duration_ms+' ms')+'</span></div>'
|
|
6929
7101
|
+ '<div><span class="l">output</span><span class="v">'+esc(result.output_device)+'</span></div>'
|
|
@@ -6933,29 +7105,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6933
7105
|
host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
|
|
6934
7106
|
+ banner + meta + diag + '</div>';
|
|
6935
7107
|
}
|
|
6936
|
-
function runSpeakerTest(n){
|
|
6937
|
-
var robotId = schedId(n);
|
|
6938
|
-
var
|
|
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…'; }
|
|
6939
7112
|
var status = $('pk-spk-test-stateline');
|
|
6940
|
-
if(status) status.textContent = 'queued — waiting for robot…';
|
|
6941
|
-
var freq = parseFloat(($('pk-spk-freq')||{}).value || '1000');
|
|
6942
|
-
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';
|
|
6943
7119
|
// Animate the meter with a synthetic "during-test" peak so the
|
|
6944
7120
|
// operator sees something happen immediately while waiting for
|
|
6945
7121
|
// the real recording.
|
|
6946
7122
|
_spkTestSetLive(robotId, {live:true, rms: 0.45, pass: undefined, waveform: null});
|
|
6947
7123
|
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',
|
|
6948
|
-
{frequency:
|
|
7124
|
+
{mode:mode, text:phrase, frequency:freq, duration:dur, amplitude:0.6, threshold_db:-30, output:output, input:input})
|
|
6949
7125
|
.then(function(q){
|
|
6950
7126
|
// long-poll up to 8s, then fall back to 12s of result polling
|
|
6951
|
-
if(status) status.textContent = 'playing tone + recording…';
|
|
7127
|
+
if(status) status.textContent = mode==='tts'?'playing cached voice on '+selectedRobotOutput()+'…':'playing tone + recording…';
|
|
6952
7128
|
return _shellWaitForResult(robotId, q.request_id, q.device_id || robotId, '', 'speaker-test');
|
|
6953
7129
|
})
|
|
6954
7130
|
.catch(function(e){
|
|
6955
7131
|
if(status) status.textContent = '✗ test failed: '+e.message;
|
|
6956
|
-
_spkTestSetLive(robotId, {live:false});
|
|
6957
|
-
_spkTestRenderResult(robotId, {ok:false, error:e.message});
|
|
6958
|
-
if(
|
|
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'; }
|
|
6959
7136
|
});
|
|
6960
7137
|
}
|
|
6961
7138
|
// Render a synthetic "during-test" waveform so the meter animates
|
|
@@ -6968,7 +7145,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6968
7145
|
t: Date.now(),
|
|
6969
7146
|
pass: result && result.ok ? (result.pass===true) : null,
|
|
6970
7147
|
peak_db: result && result.recorded_peak_db != null ? result.recorded_peak_db : null,
|
|
6971
|
-
frequency: result && result.frequency != null ? result.frequency : null,
|
|
7148
|
+
frequency: result && result.frequency != null ? result.frequency : null,
|
|
7149
|
+
mode: result && result.mode || 'tone',
|
|
6972
7150
|
diagnostic: result && result.diagnostic || (result && !result.ok ? result.error : null),
|
|
6973
7151
|
});
|
|
6974
7152
|
while(list.length > 5) list.pop();
|
|
@@ -6983,15 +7161,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6983
7161
|
_origRenderShellResult(robotId, result, label);
|
|
6984
7162
|
if(label === 'speaker-test'){
|
|
6985
7163
|
_spkTestSetLive(robotId, {live:false});
|
|
6986
|
-
_spkTestMark(robotId, result);
|
|
6987
|
-
_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;});
|
|
6988
7167
|
var status = $('pk-spk-test-stateline');
|
|
6989
7168
|
if(status){
|
|
6990
7169
|
if(!result || !result.ok){ status.textContent = '✗ test failed'; }
|
|
6991
7170
|
else if(result.pass){ status.textContent = '✓ pass · '+result.recorded_peak_db+' dB peak'; }
|
|
6992
7171
|
else { status.textContent = '✗ no signal — '+result.recorded_peak_db+' dB peak'; }
|
|
6993
7172
|
}
|
|
6994
|
-
var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = '
|
|
7173
|
+
var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = (($('pk-spk-mode')||{}).value==='tone'?'Run tone test':'Play cached voice'); }
|
|
6995
7174
|
}
|
|
6996
7175
|
};
|
|
6997
7176
|
|