infinicode 2.8.100 → 2.8.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +387 -90
- package/dist/robopark/add-robot.d.ts +2 -0
- package/dist/robopark/add-robot.js +39 -8
- package/dist/robopark/agent-ctl.js +42 -26
- package/dist/robopark/robot-runtime.js +10 -1
- package/dist/robopark-cli.js +2 -0
- package/docs/ROBOPARK_PRODUCTION_MEMORY.md +26 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +174 -10
- package/packages/robopark/scheduler/preview_agent.py +72 -19
|
@@ -26,7 +26,7 @@ export const DASHBOARD_HTML = `<!doctype html>
|
|
|
26
26
|
<head>
|
|
27
27
|
<meta charset="utf-8">
|
|
28
28
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
29
|
-
<meta name="robopark-ui-build" content="
|
|
29
|
+
<meta name="robopark-ui-build" content="provisioning-token-20260717-1">
|
|
30
30
|
<title>RoboPark Control</title>
|
|
31
31
|
<style>
|
|
32
32
|
/* ── premium tech glass theme ── dark is the committed default; light is a frosted toggle ── */
|
|
@@ -363,20 +363,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
363
363
|
.pk-ctl{display:flex;gap:0.45rem;flex-wrap:wrap;}
|
|
364
364
|
.pk-note{font-family:var(--mono);font-size:0.58rem;color:var(--gold);opacity:0.85;margin-top:0.4rem;}
|
|
365
365
|
.pk-pipeline{display:grid;gap:0.42rem;margin-top:0.7rem;position:relative;}
|
|
366
|
+
.pk-live-stage-focus{--stage-color:var(--gold-bright);position:relative;overflow:hidden;margin-top:.58rem;padding:.78rem .85rem .78rem 1rem;border:1px solid var(--stage-color);border-radius:13px;background:linear-gradient(105deg,color-mix(in srgb,var(--stage-color) 24%,transparent),rgba(8,8,8,.68));box-shadow:0 10px 30px rgba(0,0,0,.28),inset 4px 0 0 var(--stage-color);}
|
|
367
|
+
.pk-live-stage-focus:before{content:'';position:absolute;width:150px;height:150px;right:-65px;top:-95px;border-radius:50%;background:var(--stage-color);opacity:.12;filter:blur(5px);animation:pulse 1.3s infinite;}.pk-live-stage-focus .eyebrow{display:block;font-family:var(--mono);font-size:.49rem;letter-spacing:.13em;text-transform:uppercase;color:var(--stage-color);}.pk-live-stage-focus b{display:block;margin-top:.2rem;font-size:1rem;letter-spacing:.015em;color:var(--ink);}.pk-live-stage-focus span:last-child{display:block;margin-top:.18rem;font-family:var(--mono);font-size:.57rem;color:var(--muted);}.pk-live-stage-focus.blocked,.pk-live-stage-focus.failed{--stage-color:var(--bad)}.pk-live-stage-focus.waiting,.pk-live-stage-focus.paused{--stage-color:var(--muted)}.pk-live-stage-focus.active,.pk-live-stage-focus.running{--stage-color:var(--warn)}
|
|
366
368
|
.pk-pipe-step{--phase:var(--gold);display:grid;grid-template-columns:2rem minmax(7.2rem,.72fr) minmax(9rem,1.35fr);gap:0.55rem;align-items:center;padding:0.55rem 0.62rem;border-radius:11px;border:1px solid rgba(255,255,255,.06);background:linear-gradient(110deg,color-mix(in srgb,var(--phase) 7%,transparent),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.58rem;color:var(--muted);transition:transform .18s,border-color .18s,background .18s;position:relative;overflow:hidden;}
|
|
367
369
|
.pk-pipe-step.phase-hardware{--phase:#63d5c6}.pk-pipe-step.phase-trigger{--phase:#efb94c}.pk-pipe-step.phase-session{--phase:#66a8ff}.pk-pipe-step.phase-media{--phase:#68d48f}.pk-pipe-step.phase-intelligence{--phase:#e38bff}.pk-pipe-step.phase-playback{--phase:#ff9b62}.pk-pipe-step.phase-close{--phase:#a8b0bd}
|
|
368
370
|
.pk-pipe-step .step-index{display:grid;place-items:center;width:1.65rem;height:1.65rem;border-radius:8px;border:1px solid color-mix(in srgb,var(--phase) 35%,transparent);color:var(--phase);background:color-mix(in srgb,var(--phase) 10%,transparent);font-weight:700;}
|
|
369
371
|
.pk-pipe-step .stage{color:var(--text);text-transform:uppercase;letter-spacing:.055em;font-weight:650;display:block;}.pk-pipe-step .phase{display:block;color:var(--phase);font-size:.49rem;letter-spacing:.08em;margin-top:.14rem;}
|
|
370
|
-
.pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}
|
|
372
|
+
.pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}.pk-pipe-step .step-state{display:inline-block;margin-left:.35rem;padding:.1rem .3rem;border:1px solid currentColor;border-radius:999px;font-size:.43rem;letter-spacing:.07em;vertical-align:1px;color:var(--muted)}
|
|
371
373
|
.pk-pipe-step.ok{border-color:color-mix(in srgb,var(--ok) 22%,transparent)}
|
|
374
|
+
.pk-pipe-step.ok .step-state{color:var(--ok)}.pk-pipe-step.pending{opacity:.52}.pk-pipe-step.pending .step-index{filter:saturate(.3)}
|
|
372
375
|
.pk-pipe-step.failed,.pk-pipe-step.blocked{border-color:rgba(255,90,90,.48);background:linear-gradient(110deg,rgba(255,70,70,.14),rgba(255,255,255,.02));}
|
|
373
|
-
.pk-pipe-step.current{transform:translateX(
|
|
376
|
+
.pk-pipe-step.current{transform:translateX(5px) scale(1.012);border-color:var(--phase);background:linear-gradient(105deg,color-mix(in srgb,var(--phase) 24%,transparent),rgba(255,255,255,.045));box-shadow:0 0 0 1px color-mix(in srgb,var(--phase) 28%,transparent),0 10px 32px rgba(0,0,0,.3);opacity:1;z-index:2;}
|
|
374
377
|
.pk-pipe-step.current:after{content:'ACTIVE';position:absolute;right:-1.7rem;top:.45rem;transform:rotate(38deg);padding:.12rem 2rem;background:var(--phase);color:#080808;font-size:.42rem;font-weight:800;letter-spacing:.08em;}
|
|
375
378
|
.pk-pipe-step.current .step-index{box-shadow:0 0 14px color-mix(in srgb,var(--phase) 55%,transparent);animation:pulse 1.15s infinite;}
|
|
376
379
|
.pk-pipeline-summary{padding:0.7rem 0.75rem;border:1px solid var(--glass-border);border-radius:11px;background:linear-gradient(120deg,rgba(212,170,72,.08),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.62rem;color:var(--muted);line-height:1.45;}
|
|
377
380
|
.pk-pipeline-summary.ready{color:var(--ok);border-color:rgba(70,220,150,.35);}
|
|
378
381
|
.pk-pipeline-summary.running{color:var(--warn);border-color:rgba(255,196,70,.35);}
|
|
379
382
|
.pk-pipeline-summary.blocked{color:var(--bad);border-color:rgba(255,90,90,.4);}
|
|
383
|
+
.fleet-stage-focus{margin-top:.48rem;padding:.48rem .58rem;border:1px solid color-mix(in srgb,var(--warn) 48%,transparent);border-radius:9px;background:linear-gradient(100deg,rgba(255,184,62,.14),rgba(255,255,255,.02));font-family:var(--mono)}.fleet-stage-focus span{display:block;color:var(--warn);font-size:.45rem;letter-spacing:.1em;text-transform:uppercase}.fleet-stage-focus b{display:block;margin-top:.12rem;color:var(--ink);font-size:.66rem}.fleet-stage-focus.blocked,.fleet-stage-focus.failed{border-color:color-mix(in srgb,var(--bad) 55%,transparent)}.fleet-stage-focus.blocked span,.fleet-stage-focus.failed span{color:var(--bad)}.fleet-stage-focus.waiting,.fleet-stage-focus.paused{border-color:var(--glass-border)}.fleet-stage-focus.waiting span,.fleet-stage-focus.paused span{color:var(--muted)}
|
|
380
384
|
.pk-pipe-history{margin-top:.75rem;border-top:1px solid var(--glass-border);padding-top:.7rem}.pk-pipe-history-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;margin-bottom:.5rem}.pk-pipe-history-head b{font-family:var(--mono);font-size:.65rem;color:var(--ink);text-transform:uppercase;letter-spacing:.06em}.pk-pipe-history-list{display:grid;gap:.45rem}.pk-run-card{border:1px solid var(--glass-border);border-radius:10px;background:var(--panel-2);padding:.55rem .62rem;cursor:pointer}.pk-run-card:hover{border-color:var(--gold)}.pk-run-top{display:grid;grid-template-columns:1fr auto;gap:.5rem;align-items:start}.pk-run-title{font-family:var(--mono);font-size:.61rem;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pk-run-meta{font-family:var(--mono);font-size:.53rem;color:var(--muted);margin-top:.17rem}.pk-run-state{font-family:var(--mono);font-size:.5rem;text-transform:uppercase;border:1px solid var(--glass-border);border-radius:999px;padding:.18rem .4rem}.pk-run-state.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent)}.pk-run-state.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-run-state.running{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}.pk-run-stages{display:flex;gap:3px;margin-top:.45rem}.pk-run-stages i{height:4px;flex:1;border-radius:3px;background:rgba(255,255,255,.07)}.pk-run-stages i.ok{background:var(--ok)}.pk-run-stages i.failed,.pk-run-stages i.blocked{background:var(--bad)}.pk-history-pager{display:flex;align-items:center;gap:.35rem}.pk-history-pager span{font-family:var(--mono);font-size:.54rem;color:var(--muted)}
|
|
381
385
|
.pk-mic-suite{margin-top:.6rem;display:grid;gap:.55rem}.pk-mic-health{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem}.pk-mic-check{padding:.48rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-mic-check .k{font-family:var(--mono);font-size:.49rem;color:var(--muted);text-transform:uppercase}.pk-mic-check .v{font-family:var(--mono);font-size:.62rem;color:var(--ink);margin-top:.16rem;overflow-wrap:anywhere}.pk-mic-check.ok{border-color:color-mix(in srgb,var(--ok) 30%,transparent)}.pk-mic-check.bad{border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-mic-result{border:1px solid var(--glass-border);border-radius:10px;padding:.58rem;background:var(--panel-2);font-family:var(--mono);font-size:.58rem}.pk-mic-result.ok{border-color:var(--ok);background:rgba(60,180,110,.08)}.pk-mic-result.bad{border-color:var(--bad);background:rgba(220,80,80,.08)}.pk-mic-readings{display:grid;grid-template-columns:repeat(4,1fr);gap:.35rem;margin-top:.45rem}.pk-mic-reading{padding:.35rem;background:rgba(0,0,0,.16);border-radius:7px}.pk-mic-reading span{display:block;color:var(--muted);font-size:.48rem;text-transform:uppercase}.pk-mic-reading b{display:block;color:var(--ink);font-size:.67rem;margin-top:.1rem}
|
|
382
386
|
.pk-motor-registry{display:grid;gap:.4rem}.pk-motor-row{display:grid;grid-template-columns:1fr 4.5rem 5.5rem auto;gap:.35rem;align-items:end;padding:.45rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-motor-canvas{display:flex;align-items:stretch;gap:.55rem;overflow-x:auto;padding:.65rem .2rem;min-height:130px}.pk-motor-node{min-width:165px;padding:.55rem;border:1px solid rgba(255,205,93,.35);border-radius:12px;background:linear-gradient(145deg,rgba(201,150,46,.13),rgba(255,255,255,.025));position:relative}.pk-motor-node:not(:last-child):after{content:'';position:absolute;right:-.56rem;top:50%;width:.56rem;border-top:2px solid var(--gold)}.pk-motor-node .node-num{font-family:var(--mono);font-size:.48rem;color:var(--gold);text-transform:uppercase;margin-bottom:.35rem}.pk-motor-node .rp-field{margin-bottom:.3rem}.pk-motor-empty{display:grid;place-items:center;width:100%;min-height:100px;border:1px dashed var(--glass-border);border-radius:10px;color:var(--muted);font-family:var(--mono);font-size:.58rem}.pk-motor-status{font-family:var(--mono);font-size:.58rem;padding:.5rem;border-radius:8px;background:var(--panel-2);border:1px solid var(--glass-border)}.pk-motor-status.ok{color:var(--ok);border-color:var(--ok)}.pk-motor-status.bad{color:var(--bad);border-color:var(--bad)}
|
|
@@ -790,7 +794,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
790
794
|
<h3><span>Live activity</span><span style="color:var(--muted)" id="pk-src">/fed/stream</span></h3>
|
|
791
795
|
<div class="rows" id="pk-rows"></div>
|
|
792
796
|
</aside>
|
|
793
|
-
<div class="pk-hint">drag to pan · scroll to zoom · <b>click a robot</b> for cam / mic / speaker</div>
|
|
797
|
+
<div class="pk-hint">drag to pan · scroll to zoom · <b>click a robot</b> for cam / mic / speaker · <b>ctrl+drag a robot</b> to reposition (persists)</div>
|
|
794
798
|
</div>
|
|
795
799
|
<div class="pk-drawer" id="pk-drawer">
|
|
796
800
|
<div class="pk-dh">
|
|
@@ -1126,18 +1130,21 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1126
1130
|
<div class="rp-form" id="rp-addrobot">
|
|
1127
1131
|
<h3>Add robot (mint enrollment token)</h3>
|
|
1128
1132
|
<div class="rp-quickrow">
|
|
1129
|
-
<div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
|
|
1130
|
-
<div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
|
|
1131
|
-
<
|
|
1133
|
+
<div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
|
|
1134
|
+
<div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
|
|
1135
|
+
<div class="rp-field"><label>Robot network</label><select id="rb-network"><option value="auto">Auto from hub address</option><option value="lan">LAN</option><option value="tailscale">Tailscale</option></select></div>
|
|
1136
|
+
<div class="rp-field"><label>Mesh token</label><input id="rb-token" spellcheck="false" autocomplete="off" placeholder="required in generated command"></div>
|
|
1137
|
+
<button class="rp-btn primary" id="rb-add" style="height:fit-content">Add robot</button>
|
|
1132
1138
|
</div>
|
|
1133
1139
|
<div class="rp-autonote" id="rb-autonote">the robot self-reports its IP and hardware on first heartbeat — you only need a name and (optionally) a character preset to get started.</div>
|
|
1134
1140
|
<div class="rp-quickrow" id="rb-clicmd-row" style="display:none;">
|
|
1135
|
-
<div class="rp-field" style="flex:1;"><label>run this ON THE ROBOT
|
|
1141
|
+
<div class="rp-field" style="flex:1;"><label>complete production one-liner — run this ON THE ROBOT</label><input id="rb-clicmd" readonly style="cursor:text;"></div>
|
|
1136
1142
|
<button class="rp-btn" id="rb-clicmd-copy" style="height:fit-content">📋 Copy</button>
|
|
1137
1143
|
</div>
|
|
1138
1144
|
<details class="rp-advanced">
|
|
1139
|
-
<summary>Advanced — pre-set network/voice config before the robot connects</summary>
|
|
1140
|
-
<div class="rp-advanced-grid">
|
|
1145
|
+
<summary>Advanced — pre-set network/voice config before the robot connects</summary>
|
|
1146
|
+
<div class="rp-advanced-grid">
|
|
1147
|
+
<div class="rp-field"><label>Site</label><input id="rb-site" placeholder="Tel Aviv"></div>
|
|
1141
1148
|
<div class="rp-field"><label>Tailscale IP</label><input id="rb-tsip" placeholder="auto-detected once connected"></div>
|
|
1142
1149
|
<div class="rp-field"><label>LAN IP</label><input id="rb-lanip" placeholder="auto-detected once connected"></div>
|
|
1143
1150
|
<div class="rp-field"><label>Motor server URL</label><input id="rb-motor" placeholder="http://...:5000"></div>
|
|
@@ -1241,6 +1248,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1241
1248
|
(function(){
|
|
1242
1249
|
var TOKEN = new URLSearchParams(location.search).get('token') || '';
|
|
1243
1250
|
var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';
|
|
1251
|
+
function dashboardMeshToken(){
|
|
1252
|
+
if(TOKEN)return TOKEN;
|
|
1253
|
+
try{var saved=JSON.parse(localStorage.getItem('rp-command-env-v1')||'null');return saved&&String(saved.token||'').trim()||'';}catch(e){return '';}
|
|
1254
|
+
}
|
|
1244
1255
|
var UI_BUILD=(document.querySelector('meta[name="robopark-ui-build"]')||{}).content||'unknown';
|
|
1245
1256
|
fetch('/fed/ui-build'+QS,{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(info){
|
|
1246
1257
|
if(!info||!info.build||info.build===UI_BUILD)return;
|
|
@@ -1532,6 +1543,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1532
1543
|
+ '<div class="meta">'+esc(n.platform||'')+' '+esc((hw&&hw.cpu&&hw.cpu.model)?hw.cpu.model.replace(/\\s+/g,' ').slice(0,22):'')+site+' <span class="mono">v'+esc(n.version||'?')+'</span></div>'
|
|
1533
1544
|
+ (avatar?('<div class="node-hero"><img class="node-avatar" alt="'+esc(n.displayName)+'" src="'+esc(avatar+QS)+'"><div class="node-health"><b>'+esc(String(readiness).replace(/_/g,' '))+'</b><br>'
|
|
1534
1545
|
+(tele?('sessions '+esc(tele.sessions_total||0)+' / triggers '+esc(tele.trigger_count||0)+'<br>latency '+esc(tele.avg_latency_ms!=null?Math.round(tele.avg_latency_ms)+'ms':'n/a')+' / drops '+esc(tele.drop_rate!=null?Math.round(tele.drop_rate*100)+'%':'n/a')+'<br>heartbeat '+esc(hb!=null?hb+'s ago':'unavailable')):'scheduler telemetry pending')+'</div></div>'):'');
|
|
1546
|
+
if(tele){var fleetStage=tele.current_stage||{},fleetStageStatus=fleetStage.status||tele.current_stage_status||'waiting',fleetStageLabel=fleetStage.label||tele.current_stage_label||'Waiting for pipeline telemetry';html+='<div class="fleet-stage-focus '+esc(fleetStageStatus)+'"><span>Live production stage</span><b>'+esc(fleetStageLabel)+'</b></div>';}
|
|
1535
1547
|
|
|
1536
1548
|
if((tele||String(n.role||'').toLowerCase()==='satellite'||String(n.role||'').toLowerCase()==='robot')&&cameraRef){
|
|
1537
1549
|
html += '<div class="fleet-live-camera" data-fleet-camera-host="'+esc(cameraRef)+'"><img alt="Live camera for '+esc(n.displayName)+'" data-fleet-camera="'+esc(cameraRef)+'"><span class="state">'+(up?'connecting camera':'robot offline')+'</span></div>';
|
|
@@ -2248,16 +2260,20 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2248
2260
|
|
|
2249
2261
|
// ── robust CLI one-liner generation, live as you type — no API call
|
|
2250
2262
|
// needed to see it; 'robopark add-robot' mints its own token when run ──
|
|
2251
|
-
function buildAddRobotCommand(name, characterId){
|
|
2252
|
-
if(!name) return '';
|
|
2253
|
-
var hubUrl=location.origin;
|
|
2254
|
-
var schedulerUrl=
|
|
2255
|
-
var
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
if(
|
|
2259
|
-
if(
|
|
2260
|
-
argv.push('--
|
|
2263
|
+
function buildAddRobotCommand(name, characterId){
|
|
2264
|
+
if(!name) return '';
|
|
2265
|
+
var hubUrl=location.origin.replace(/\\/+$/,'');
|
|
2266
|
+
var schedulerUrl=hubUrl+'/robopark';
|
|
2267
|
+
var requestedNetwork=$('rb-network')&&$('rb-network').value||'auto';
|
|
2268
|
+
var network=requestedNetwork==='auto'?(/^100\\./.test(location.hostname)||/\\.ts\\.net$/i.test(location.hostname)?'tailscale':'lan'):requestedNetwork;
|
|
2269
|
+
var argv=['robopark','add-robot',name];
|
|
2270
|
+
if(characterId) argv.push('--character', characterId);
|
|
2271
|
+
var site=$('rb-site')&&$('rb-site').value.trim();if(site)argv.push('--site',site);
|
|
2272
|
+
if(hubUrl) argv.push('--hub-url', hubUrl);
|
|
2273
|
+
var meshToken=$('rb-token')&&$('rb-token').value.trim()||dashboardMeshToken()||'<mesh-token>';
|
|
2274
|
+
argv.push('--token', meshToken);
|
|
2275
|
+
if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
|
|
2276
|
+
argv.push(network==='tailscale'?'--tailscale':'--lan','--start','--auto-start','--yes');
|
|
2261
2277
|
return argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' ');
|
|
2262
2278
|
}
|
|
2263
2279
|
function refreshRobotCliCommand(){
|
|
@@ -2267,8 +2283,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2267
2283
|
inp.value=buildAddRobotCommand(name, cid);
|
|
2268
2284
|
row.style.display='flex';
|
|
2269
2285
|
}
|
|
2270
|
-
var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
|
|
2271
|
-
var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
|
|
2286
|
+
var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
|
|
2287
|
+
var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
|
|
2288
|
+
var rbNetworkEl=$('rb-network'); if(rbNetworkEl) rbNetworkEl.addEventListener('change', refreshRobotCliCommand);
|
|
2289
|
+
var rbSiteEl=$('rb-site'); if(rbSiteEl) rbSiteEl.addEventListener('input', refreshRobotCliCommand);
|
|
2290
|
+
var rbTokenEl=$('rb-token'); if(rbTokenEl){ rbTokenEl.value=dashboardMeshToken(); rbTokenEl.addEventListener('input', refreshRobotCliCommand); }
|
|
2272
2291
|
var rbCliCopy=$('rb-clicmd-copy'); if(rbCliCopy) rbCliCopy.addEventListener('click', function(){ var i=$('rb-clicmd'); i.select(); try{document.execCommand('copy');}catch(e){} showOut('command copied — run it on the robot itself'); });
|
|
2273
2292
|
|
|
2274
2293
|
function addRobot(){
|
|
@@ -2653,7 +2672,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2653
2672
|
var RP_COMMAND_ENV_KEY='rp-command-env-v1';
|
|
2654
2673
|
var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
|
|
2655
2674
|
var rpVisibleCommands=[];
|
|
2656
|
-
function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:
|
|
2675
|
+
function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:dashboardMeshToken()};}
|
|
2657
2676
|
function rpLoadStoredJson(key,fallback){try{var value=JSON.parse(localStorage.getItem(key)||'null');return value&&typeof value==='object'?value:fallback;}catch(e){return fallback;}}
|
|
2658
2677
|
function rpCommandEnv(){
|
|
2659
2678
|
var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
|
|
@@ -2705,7 +2724,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2705
2724
|
}
|
|
2706
2725
|
function initCommandSheet(){
|
|
2707
2726
|
var fields=['lan','tail','site','robot','enrollment','token'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
|
|
2708
|
-
fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=saved[name]!=null?saved[name]:defaults[name];input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
|
|
2727
|
+
fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=name==='token'&&TOKEN?TOKEN:(saved[name]!=null?saved[name]:defaults[name]);input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
|
|
2709
2728
|
var search=$('rp-command-search'),category=$('rp-command-category'),grid=$('rp-command-grid'),reset=$('rp-command-reset'),copyAll=$('rp-command-copy-all'),form=$('rp-command-custom-form');
|
|
2710
2729
|
if(search)search.addEventListener('input',renderCommandSheet);if(category)category.addEventListener('change',renderCommandSheet);
|
|
2711
2730
|
if(reset)reset.addEventListener('click',function(){try{localStorage.removeItem(RP_COMMAND_ENV_KEY);}catch(e){}fields.forEach(function(name){var input=$('rp-command-'+name);if(input)input.value=defaults[name];});renderCommandSheet();showOut('command environment reset');});
|
|
@@ -4078,21 +4097,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4078
4097
|
// Pond / curved water feature at the lower-left of the plaza.
|
|
4079
4098
|
var POND={cx:0.18*15, cy:0.84*8, rx:2.4, ry:1.1};
|
|
4080
4099
|
// Street traffic along the top road. Each car has x, y (world), color,
|
|
4081
|
-
// a per-car speed (world units / second), a direction (+1 = right,
|
|
4082
|
-
// left)
|
|
4083
|
-
//
|
|
4084
|
-
//
|
|
4085
|
-
//
|
|
4100
|
+
// a per-car base speed (world units / second), a direction (+1 = right,
|
|
4101
|
+
// -1 = left), and a per-frame jitter range so cars never settle into
|
|
4102
|
+
// a stable cluster. y is one of two lane bands (≈ -0.65 for outbound,
|
|
4103
|
+
// ≈ -1.18 for inbound) so cars don't overlap.
|
|
4104
|
+
//
|
|
4105
|
+
// Stability fix: the previous version always re-spawned a wrapped car
|
|
4106
|
+
// at x = -0.5 (or PARK_W + 0.5) — every car in the same lane teleported
|
|
4107
|
+
// to the same spot, so the slowest car instantly caught the fastest
|
|
4108
|
+
// one and the whole lane collapsed into a clump. We now (a) spawn
|
|
4109
|
+
// each car at a random x within the visible road, and (b) add a
|
|
4110
|
+
// ±5% per-frame speed jitter so cars gradually drift apart. Speeds
|
|
4111
|
+
// are also more spread (2.2 → 4.6) so the natural ordering stays
|
|
4112
|
+
// stable long enough to look like real traffic.
|
|
4086
4113
|
var ROAD_Y0=-1.5, ROAD_Y1=-0.35;
|
|
4087
4114
|
var CARS=[
|
|
4088
|
-
{x:1.5, y:-0.65, color:'#f2c14e', speed:
|
|
4089
|
-
{x:
|
|
4090
|
-
{x:
|
|
4091
|
-
{x:
|
|
4092
|
-
{x:
|
|
4093
|
-
{x:0.5, y:-1.18, color:'#5b8bd6', speed:
|
|
4094
|
-
{x:13.0, y:-0.65, color:'#c87ed4', speed:2.6, dir:+1},
|
|
4095
|
-
{x:7.0, y:-1.18, color:'#f2c14e', speed:3.8, dir:-1},
|
|
4115
|
+
{x:1.5, y:-0.65, color:'#f2c14e', speed:2.4, dir:+1},
|
|
4116
|
+
{x:6.5, y:-0.65, color:'#e0a23c', speed:3.6, dir:+1},
|
|
4117
|
+
{x:11.5, y:-0.65, color:'#d8d8e0', speed:4.4, dir:+1},
|
|
4118
|
+
{x:4.0, y:-1.18, color:'#7fb4ff', speed:2.6, dir:-1},
|
|
4119
|
+
{x:9.0, y:-1.18, color:'#ff7e6e', speed:3.4, dir:-1},
|
|
4120
|
+
{x:0.5, y:-1.18, color:'#5b8bd6', speed:4.2, dir:-1},
|
|
4096
4121
|
];
|
|
4097
4122
|
// Per-car phase offsets so headlights / taillights / wheels don't all
|
|
4098
4123
|
// blink in lockstep — gives a natural "different cars, different
|
|
@@ -4369,12 +4394,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4369
4394
|
connected: match?!!match.connected:false,
|
|
4370
4395
|
load: (match&&match.load!=null)?match.load:0,
|
|
4371
4396
|
platform: match?(match.platform||''):'', version: match?(match.version||'?'):'—',
|
|
4372
|
-
site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
|
|
4373
|
-
schedulerRobotId: match?(match.schedulerRobotId||match.schedulerDeviceId||''):'',
|
|
4374
|
-
schedulerDeviceId: match?(match.schedulerDeviceId||match.schedulerRobotId||''):'',
|
|
4397
|
+
site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
|
|
4398
|
+
schedulerRobotId: match?(match.schedulerRobotId||match.schedulerDeviceId||''):'',
|
|
4399
|
+
schedulerDeviceId: match?(match.schedulerDeviceId||match.schedulerRobotId||''):'',
|
|
4375
4400
|
telemKeys: [slot.label.toLowerCase()].concat(slot.keys||[]).concat(match?[(match.displayName||'').toLowerCase()]:[]),
|
|
4401
|
+
// slotId is the stable handle we use to persist operator-
|
|
4402
|
+
// chosen positions. slotNx/slotNy are the preset's default
|
|
4403
|
+
// coordinates so the user can always reset back.
|
|
4404
|
+
slotId: slot.id, slotNx: slot.nx, slotNy: slot.ny,
|
|
4376
4405
|
wx:wx, wy:wy, _hit:prevHit(id)
|
|
4377
|
-
};
|
|
4406
|
+
};
|
|
4378
4407
|
// Attach a status derived from the latest scheduler telemetry
|
|
4379
4408
|
// (push-poll, so it can lag up to 3s behind /fed/status).
|
|
4380
4409
|
if(kind === 'robot'){
|
|
@@ -4406,6 +4435,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4406
4435
|
if(!s) return;
|
|
4407
4436
|
nodes = layout(s.nodes||[]);
|
|
4408
4437
|
byId={}; nodes.forEach(function(n){ byId[n.id]=n; });
|
|
4438
|
+
// re-apply any persisted Ctrl+drag overrides on top of the
|
|
4439
|
+
// fresh layout (the server may have also returned updated
|
|
4440
|
+
// slot nx/ny — those go through the override map too)
|
|
4441
|
+
_applyPosOverrides();
|
|
4409
4442
|
// if not streaming, animate from the polled activity tail
|
|
4410
4443
|
if(!es || es.readyState!==1){ (s.activity||[]).forEach(function(e){ ingest(e.f||e, true); }); }
|
|
4411
4444
|
}
|
|
@@ -4450,16 +4483,187 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4450
4483
|
}
|
|
4451
4484
|
|
|
4452
4485
|
// ── canvas + projection ──
|
|
4486
|
+
// State for Ctrl+drag robot repositioning on the park map. When
|
|
4487
|
+
// the user holds Ctrl and mousedown's a robot, the canvas stops
|
|
4488
|
+
// panning and instead drags that node around. On mouseup, the
|
|
4489
|
+
// new (nx, ny) is written to _parkPosOverrides and pushed to
|
|
4490
|
+
// the scheduler via a debounced PUT. Survives reloads because
|
|
4491
|
+
// the server stores nx/ny in the character_presets table.
|
|
4492
|
+
var _parkPosOverrides = {}; // presetId -> {nx, ny} (normalized 0..1)
|
|
4493
|
+
var _dragNode = null; // the robot node currently being dragged
|
|
4494
|
+
var _dragOffset = {x:0, y:0}; // screen-space offset from node center
|
|
4495
|
+
var _posSaveTimer = null; // debounce timer for the server PUT
|
|
4496
|
+
var _posSaveQueue = {}; // presetId -> {nx, ny} pending save
|
|
4497
|
+
function _isoToWorld(sx, sy){
|
|
4498
|
+
// Inverse of iso() in the same coordinate system. Given a
|
|
4499
|
+
// screen point, return the (wx, wy) world point. Used by the
|
|
4500
|
+
// Ctrl+drag handler to translate a mouse position back into
|
|
4501
|
+
// the iso coordinate space so the robot follows the cursor.
|
|
4502
|
+
var dx = (sx - (W-300)/2 - cam.x) / (TW*cam.z);
|
|
4503
|
+
var dy = (sy - H*0.42 - cam.y) / (TH*cam.z);
|
|
4504
|
+
// from iso(): dx = (wx-PARK_W/2) - (wy-PARK_D/2); dy = (wx-PARK_W/2) + (wy-PARK_D/2)
|
|
4505
|
+
// so wx = (dx+dy)/2 + PARK_W/2; wy = (dy-dx)/2 + PARK_D/2
|
|
4506
|
+
return { x: (dx+dy)*0.5 + PARK_W*0.5, y: (dy-dx)*0.5 + PARK_D*0.5 };
|
|
4507
|
+
}
|
|
4508
|
+
function _applyPosOverrides(){
|
|
4509
|
+
// Walk nodes and override their wx/wy from _parkPosOverrides if
|
|
4510
|
+
// we have a stored position. The slot's default nx/ny stays in
|
|
4511
|
+
// node.slotNx/slotNy so the user can "reset" back to defaults
|
|
4512
|
+
// if we ever expose that control.
|
|
4513
|
+
for(var i=0; i<nodes.length; i++){
|
|
4514
|
+
var n = nodes[i];
|
|
4515
|
+
if(!n.slotId) continue;
|
|
4516
|
+
var ov = _parkPosOverrides[n.slotId];
|
|
4517
|
+
if(ov){
|
|
4518
|
+
n.wx = Math.max(0, Math.min(PARK_W, ov.nx * PARK_W));
|
|
4519
|
+
n.wy = Math.max(0, Math.min(PARK_D, ov.ny * PARK_D));
|
|
4520
|
+
n._posOverridden = true;
|
|
4521
|
+
} else if(n._posOverridden){
|
|
4522
|
+
// The server just gave us a fresh layout and we don't have
|
|
4523
|
+
// a local override — fall back to the slot's default position.
|
|
4524
|
+
n.wx = n.slotNx * PARK_W;
|
|
4525
|
+
n.wy = n.slotNy * PARK_D;
|
|
4526
|
+
n._posOverridden = false;
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
function _schedulePosSave(){
|
|
4531
|
+
// Debounce: collapse a flurry of drag-end events into one
|
|
4532
|
+
// PUT after 350ms of quiet. Saves queue is merged so the final
|
|
4533
|
+
// PUT always reflects the latest position for each preset.
|
|
4534
|
+
if(_posSaveTimer) clearTimeout(_posSaveTimer);
|
|
4535
|
+
_posSaveTimer = setTimeout(_flushPosSaves, 350);
|
|
4536
|
+
}
|
|
4537
|
+
function _flushPosSaves(){
|
|
4538
|
+
var entries = [];
|
|
4539
|
+
for(var id in _posSaveQueue){
|
|
4540
|
+
if(Object.prototype.hasOwnProperty.call(_posSaveQueue, id)){
|
|
4541
|
+
entries.push({ id: id, nx: _posSaveQueue[id].nx, ny: _posSaveQueue[id].ny });
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
_posSaveQueue = {};
|
|
4545
|
+
_posSaveTimer = null;
|
|
4546
|
+
if(!entries.length) return;
|
|
4547
|
+
// Persist to the scheduler. We POST to the dedicated
|
|
4548
|
+
// /api/character-presets/positions batch endpoint which clamps
|
|
4549
|
+
// nx/ny to [0.04, 0.96] so a stray drag can't park a robot
|
|
4550
|
+
// off the island.
|
|
4551
|
+
var body = JSON.stringify({ positions: entries });
|
|
4552
|
+
try{
|
|
4553
|
+
fetch('/api/character-presets/positions'+QS, {
|
|
4554
|
+
method: 'PUT',
|
|
4555
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4556
|
+
body: body,
|
|
4557
|
+
keepalive: true
|
|
4558
|
+
}).then(function(r){
|
|
4559
|
+
if(r.ok) return r.json();
|
|
4560
|
+
throw new Error('HTTP '+r.status);
|
|
4561
|
+
}).then(function(j){
|
|
4562
|
+
if(typeof showOut === 'function'){
|
|
4563
|
+
showOut('park position saved · '+(j.updated||entries.length)+' preset'+(entries.length===1?'':'s'));
|
|
4564
|
+
}
|
|
4565
|
+
}).catch(function(err){
|
|
4566
|
+
if(typeof showOut === 'function') showOut('park position save failed: '+err.message);
|
|
4567
|
+
});
|
|
4568
|
+
}catch(e){ /* offline — keep override in memory */ }
|
|
4569
|
+
}
|
|
4570
|
+
function _hitTestNode(mx, my){
|
|
4571
|
+
// Walk all visible nodes and return the first whose hit-box
|
|
4572
|
+
// contains (mx, my). Mirrors the existing click hit-test
|
|
4573
|
+
// but returns the node directly (and skips early if a drag
|
|
4574
|
+
// is already in progress).
|
|
4575
|
+
for(var i=0; i<nodes.length; i++){
|
|
4576
|
+
var n = nodes[i];
|
|
4577
|
+
var hb = n._hit;
|
|
4578
|
+
if(hb && mx>=hb.x && mx<=hb.x+hb.w && my>=hb.y && my<=hb.y+hb.h){
|
|
4579
|
+
return n;
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
return null;
|
|
4583
|
+
}
|
|
4453
4584
|
function ensure(){
|
|
4454
4585
|
if(C) return;
|
|
4455
4586
|
C=$('pk-canvas'); X=C.getContext('2d'); t0=performance.now();
|
|
4456
4587
|
DPR=Math.max(1,Math.min(2,window.devicePixelRatio||1));
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4588
|
+
// Panning: drag on the canvas (no Ctrl) pans the iso camera.
|
|
4589
|
+
// Ctrl+drag on a robot reposition the robot on the park map.
|
|
4590
|
+
// Each gesture is mutually exclusive — we keep one _isPanning
|
|
4591
|
+
// flag and one _dragNode at a time.
|
|
4592
|
+
var _isPanning = false;
|
|
4593
|
+
var _px = 0, _py = 0;
|
|
4594
|
+
C.addEventListener('mousedown',function(e){
|
|
4595
|
+
var r = C.getBoundingClientRect();
|
|
4596
|
+
var mx = e.clientX - r.left;
|
|
4597
|
+
var my = e.clientY - r.top;
|
|
4598
|
+
// Ctrl+drag: if a robot is under the cursor, pick it up.
|
|
4599
|
+
// Only ROBOT nodes are draggable — the hub, parked cars in the
|
|
4600
|
+
// lot, and ghosts for offline nodes stay locked to their slot.
|
|
4601
|
+
if(e.ctrlKey){
|
|
4602
|
+
var hit = _hitTestNode(mx, my);
|
|
4603
|
+
if(hit && hit.slotId && hit.kind === 'robot'){
|
|
4604
|
+
_dragNode = hit;
|
|
4605
|
+
var p = iso(hit.wx, hit.wy);
|
|
4606
|
+
_dragOffset.x = mx - p.x;
|
|
4607
|
+
_dragOffset.y = my - p.y;
|
|
4608
|
+
// prevent text selection while dragging
|
|
4609
|
+
e.preventDefault();
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
// otherwise: start a pan
|
|
4614
|
+
_isPanning = true;
|
|
4615
|
+
_px = e.clientX; _py = e.clientY;
|
|
4616
|
+
});
|
|
4617
|
+
window.addEventListener('mouseup',function(){
|
|
4618
|
+
if(_dragNode){
|
|
4619
|
+
// commit the drag: record override, schedule server save
|
|
4620
|
+
var nx = _dragNode.wx / PARK_W;
|
|
4621
|
+
var ny = _dragNode.wy / PARK_D;
|
|
4622
|
+
var id = _dragNode.slotId;
|
|
4623
|
+
if(id){
|
|
4624
|
+
_parkPosOverrides[id] = { nx: nx, ny: ny };
|
|
4625
|
+
_posSaveQueue[id] = { nx: nx, ny: ny };
|
|
4626
|
+
_schedulePosSave();
|
|
4627
|
+
}
|
|
4628
|
+
_dragNode = null;
|
|
4629
|
+
}
|
|
4630
|
+
_isPanning = false;
|
|
4631
|
+
});
|
|
4632
|
+
window.addEventListener('mousemove',function(e){
|
|
4633
|
+
if(_dragNode){
|
|
4634
|
+
var r = C.getBoundingClientRect();
|
|
4635
|
+
var mx = e.clientX - r.left;
|
|
4636
|
+
var my = e.clientY - r.top;
|
|
4637
|
+
// screen point = mouse minus the original grab offset, so
|
|
4638
|
+
// the robot doesn't snap its center to the cursor
|
|
4639
|
+
var worldPt = _isoToWorld(mx - _dragOffset.x, my - _dragOffset.y);
|
|
4640
|
+
// clamp to the visible grass rectangle (with a small
|
|
4641
|
+
// inner margin so the avatar doesn't get clipped by the
|
|
4642
|
+
// gold wall or the hedge)
|
|
4643
|
+
var MARGIN = 0.4;
|
|
4644
|
+
_dragNode.wx = Math.max(MARGIN, Math.min(PARK_W - MARGIN, worldPt.x));
|
|
4645
|
+
_dragNode.wy = Math.max(MARGIN, Math.min(PARK_D - MARGIN, worldPt.y));
|
|
4646
|
+
return;
|
|
4647
|
+
}
|
|
4648
|
+
if(!_isPanning) return;
|
|
4649
|
+
cam.x += e.clientX - _px;
|
|
4650
|
+
cam.y += e.clientY - _py;
|
|
4651
|
+
_px = e.clientX; _py = e.clientY;
|
|
4652
|
+
});
|
|
4653
|
+
// Escape cancels an in-progress drag without saving
|
|
4654
|
+
window.addEventListener('keydown',function(e){
|
|
4655
|
+
if((e.key === 'Escape' || e.key === 'Esc') && _dragNode){
|
|
4656
|
+
// restore to the original override (or slot default)
|
|
4657
|
+
_dragNode.wx = _dragNode.slotNx * PARK_W;
|
|
4658
|
+
_dragNode.wy = _dragNode.slotNy * PARK_D;
|
|
4659
|
+
_dragNode = null;
|
|
4660
|
+
}
|
|
4661
|
+
});
|
|
4461
4662
|
C.addEventListener('wheel',function(e){ e.preventDefault(); var f=e.deltaY<0?1.08:0.925; cam.z=Math.max(.5,Math.min(2.4,cam.z*f)); },{passive:false});
|
|
4462
4663
|
C.addEventListener('click',function(e){
|
|
4664
|
+
// Suppress the click that fires at the end of a drag — we
|
|
4665
|
+
// don't want a single drag-release to also open the drawer.
|
|
4666
|
+
if(_dragNode) return;
|
|
4463
4667
|
var r=C.getBoundingClientRect(), mx=e.clientX-r.left, my=e.clientY-r.top, hit=null;
|
|
4464
4668
|
for(var i=0;i<nodes.length;i++){ var hb=nodes[i]._hit; if(hb&&mx>=hb.x&&mx<=hb.x+hb.w&&my>=hb.y&&my<=hb.y+hb.h) hit=nodes[i]; }
|
|
4465
4669
|
if(hit){ openDrawer(hit.id); if(String(hit.id).indexOf('pad:')!==0) selectNode(hit.id, hit.name); }
|
|
@@ -4605,9 +4809,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4605
4809
|
// the warm sun")
|
|
4606
4810
|
// The underside and drop shadow give the park real depth so it
|
|
4607
4811
|
// doesn't look like a flat sticker on the sky.
|
|
4608
|
-
// Build the grass perimeter polygon once (
|
|
4609
|
-
// so the gold
|
|
4610
|
-
|
|
4812
|
+
// Build the grass perimeter polygon once (the same corners the
|
|
4813
|
+
// grass field uses, so the gold wall's top edge sits flush
|
|
4814
|
+
// against the grass). We use grassPerim(0) — no offset — for
|
|
4815
|
+
// the walls, and the perimeter itself is used again later for
|
|
4816
|
+
// the bottom face of the floating island (see UNDERSIDE_LIFT).
|
|
4611
4817
|
function grassPerim(offset){
|
|
4612
4818
|
// Use the same world corners as the grass field, offset outward.
|
|
4613
4819
|
var o = offset;
|
|
@@ -4724,39 +4930,80 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4724
4930
|
X.fill();
|
|
4725
4931
|
}
|
|
4726
4932
|
X.restore();
|
|
4727
|
-
// 3. GOLD
|
|
4728
|
-
//
|
|
4729
|
-
//
|
|
4730
|
-
//
|
|
4731
|
-
|
|
4933
|
+
// 3. RAISED GOLD WALLS — proper 3D parapet around the park.
|
|
4934
|
+
// The wall follows the grass perimeter (4-corner rectangle).
|
|
4935
|
+
// Each side has a visible front face that extends downward in
|
|
4936
|
+
// screen space, plus a thin top-edge highlight that catches
|
|
4937
|
+
// the warm sun light. Drawn AFTER the underside so the top
|
|
4938
|
+
// edge of the wall sits flush with the top of the grass.
|
|
4939
|
+
//
|
|
4940
|
+
// Geometry: the wall's TOP edge is the grass perimeter line.
|
|
4941
|
+
// Its BOTTOM edge is the same line shifted down by WALL_H
|
|
4942
|
+
// pixels. We draw each of the 4 sides as a quad (TL, TR,
|
|
4943
|
+
// BR, BL), then trace a thin gold line along the top of
|
|
4944
|
+
// each side for the highlight.
|
|
4732
4945
|
var rimInner = grassPerim(0);
|
|
4733
|
-
var rimOuterScreen = rimOuter.map(function(p){ return iso(p[0], p[1]); });
|
|
4734
4946
|
var rimInnerScreen = rimInner.map(function(p){ return iso(p[0], p[1]); });
|
|
4947
|
+
var WALL_H = 26 * z; // wall height in screen pixels
|
|
4948
|
+
// Build bottom-edge screen points by shifting each top point
|
|
4949
|
+
// down by WALL_H.
|
|
4950
|
+
var rimBotScreen = rimInnerScreen.map(function(s){
|
|
4951
|
+
return { x: s.x, y: s.y + WALL_H };
|
|
4952
|
+
});
|
|
4953
|
+
// Front face: a single closed polygon (the grass perimeter top
|
|
4954
|
+
// + the grass perimeter bottom, traversed in reverse). Drawn
|
|
4955
|
+
// in two passes — first a darker base, then a lighter overlay
|
|
4956
|
+
// stripe near the top to give the wall some "warm-light from
|
|
4957
|
+
// above" shading.
|
|
4735
4958
|
X.save();
|
|
4736
|
-
//
|
|
4737
|
-
|
|
4738
|
-
X.fillStyle = 'rgba(255, 200, 110, 0.0)';
|
|
4739
|
-
// fill the rim band as a quad
|
|
4959
|
+
// base wall (slightly darker gold)
|
|
4960
|
+
X.fillStyle = 'rgba(176, 124, 44, 0.96)';
|
|
4740
4961
|
X.beginPath();
|
|
4741
|
-
X.moveTo(
|
|
4742
|
-
X.lineTo(
|
|
4743
|
-
X.lineTo(
|
|
4744
|
-
X.lineTo(
|
|
4962
|
+
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4963
|
+
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
4964
|
+
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
4965
|
+
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4966
|
+
X.lineTo(rimBotScreen[3].x, rimBotScreen[3].y);
|
|
4967
|
+
X.lineTo(rimBotScreen[2].x, rimBotScreen[2].y);
|
|
4968
|
+
X.lineTo(rimBotScreen[1].x, rimBotScreen[1].y);
|
|
4969
|
+
X.lineTo(rimBotScreen[0].x, rimBotScreen[0].y);
|
|
4745
4970
|
X.closePath();
|
|
4746
4971
|
X.fill();
|
|
4747
|
-
//
|
|
4748
|
-
|
|
4749
|
-
|
|
4972
|
+
// warm-light gradient overlay — fades from bright gold at the
|
|
4973
|
+
// top of the wall to transparent at the bottom, so the wall
|
|
4974
|
+
// reads as if the top is catching the sun and the bottom is
|
|
4975
|
+
// in shadow.
|
|
4976
|
+
var wallGrad = X.createLinearGradient(0, rimInnerScreen[0].y, 0, rimInnerScreen[0].y + WALL_H);
|
|
4977
|
+
wallGrad.addColorStop(0, 'rgba(255, 226, 132, 0.85)');
|
|
4978
|
+
wallGrad.addColorStop(0.35, 'rgba(255, 196, 92, 0.55)');
|
|
4979
|
+
wallGrad.addColorStop(1, 'rgba(176, 124, 44, 0)');
|
|
4980
|
+
X.fillStyle = wallGrad;
|
|
4750
4981
|
X.beginPath();
|
|
4751
4982
|
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4752
4983
|
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
4753
4984
|
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
4754
4985
|
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4986
|
+
X.lineTo(rimBotScreen[3].x, rimBotScreen[3].y);
|
|
4987
|
+
X.lineTo(rimBotScreen[2].x, rimBotScreen[2].y);
|
|
4988
|
+
X.lineTo(rimBotScreen[1].x, rimBotScreen[1].y);
|
|
4989
|
+
X.lineTo(rimBotScreen[0].x, rimBotScreen[0].y);
|
|
4990
|
+
X.closePath();
|
|
4991
|
+
X.fill();
|
|
4992
|
+
// outer dark base line at the bottom of the wall (the bottom
|
|
4993
|
+
// edge against the sky) — sells the "raised" effect
|
|
4994
|
+
X.strokeStyle = 'rgba(124, 84, 28, 0.95)';
|
|
4995
|
+
X.lineWidth = 1.4 * z;
|
|
4996
|
+
X.beginPath();
|
|
4997
|
+
X.moveTo(rimBotScreen[0].x, rimBotScreen[0].y);
|
|
4998
|
+
X.lineTo(rimBotScreen[1].x, rimBotScreen[1].y);
|
|
4999
|
+
X.lineTo(rimBotScreen[2].x, rimBotScreen[2].y);
|
|
5000
|
+
X.lineTo(rimBotScreen[3].x, rimBotScreen[3].y);
|
|
4755
5001
|
X.closePath();
|
|
4756
5002
|
X.stroke();
|
|
4757
|
-
//
|
|
4758
|
-
|
|
4759
|
-
X.
|
|
5003
|
+
// top edge — gold band right at the grass line, with a thin
|
|
5004
|
+
// lighter highlight on top to catch the sun.
|
|
5005
|
+
X.strokeStyle = 'rgba(196, 138, 48, 0.95)';
|
|
5006
|
+
X.lineWidth = 4.0 * z;
|
|
4760
5007
|
X.beginPath();
|
|
4761
5008
|
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
4762
5009
|
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
@@ -4764,16 +5011,26 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
4764
5011
|
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4765
5012
|
X.closePath();
|
|
4766
5013
|
X.stroke();
|
|
4767
|
-
//
|
|
4768
|
-
X.strokeStyle = 'rgba(
|
|
4769
|
-
X.lineWidth = 1.2*z;
|
|
5014
|
+
// bright top highlight (thin, hottest color)
|
|
5015
|
+
X.strokeStyle = 'rgba(255, 232, 158, 0.95)';
|
|
5016
|
+
X.lineWidth = 1.2 * z;
|
|
4770
5017
|
X.beginPath();
|
|
4771
|
-
X.moveTo(
|
|
4772
|
-
X.lineTo(
|
|
4773
|
-
X.lineTo(
|
|
4774
|
-
X.lineTo(
|
|
5018
|
+
X.moveTo(rimInnerScreen[0].x, rimInnerScreen[0].y);
|
|
5019
|
+
X.lineTo(rimInnerScreen[1].x, rimInnerScreen[1].y);
|
|
5020
|
+
X.lineTo(rimInnerScreen[2].x, rimInnerScreen[2].y);
|
|
5021
|
+
X.lineTo(rimInnerScreen[3].x, rimInnerScreen[3].y);
|
|
4775
5022
|
X.closePath();
|
|
4776
5023
|
X.stroke();
|
|
5024
|
+
// corner studs — small gold dots at each of the 4 perimeter
|
|
5025
|
+
// vertices so the wall reads as constructed panels, not a
|
|
5026
|
+
// single continuous block.
|
|
5027
|
+
for(var sci=0; sci<rimInnerScreen.length; sci++){
|
|
5028
|
+
var sPt = rimInnerScreen[sci];
|
|
5029
|
+
X.fillStyle = 'rgba(255, 220, 140, 0.95)';
|
|
5030
|
+
X.beginPath(); X.arc(sPt.x, sPt.y, 2.2 * z, 0, Math.PI*2); X.fill();
|
|
5031
|
+
X.fillStyle = 'rgba(196, 138, 48, 0.85)';
|
|
5032
|
+
X.beginPath(); X.arc(sPt.x, sPt.y, 3.4 * z, 0, Math.PI*2); X.stroke();
|
|
5033
|
+
}
|
|
4777
5034
|
X.restore();
|
|
4778
5035
|
// ── flower spots — deterministic small dots in 3 colors, scattered in the lawn only ──
|
|
4779
5036
|
X.save();
|
|
@@ -6263,21 +6520,40 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6263
6520
|
// Cars share the same elapsed-dt clock so a tab-switch doesn't
|
|
6264
6521
|
// catapult every car off the road at once. speed is world units
|
|
6265
6522
|
// per second, dt is milliseconds. cdt is the already-clamped dt.
|
|
6523
|
+
// Per-frame speed jitter (±5%) so the same lane never settles into
|
|
6524
|
+
// a stable cluster — the fastest car gradually pulls away from the
|
|
6525
|
+
// slowest one and the spread stays alive even after many wraps.
|
|
6266
6526
|
var cdt = Math.min(64, dt);
|
|
6267
6527
|
for(var ci=0; ci<CARS.length; ci++){
|
|
6268
6528
|
var car = CARS[ci];
|
|
6269
|
-
|
|
6529
|
+
// Deterministic per-car jitter based on wall-clock + index so it
|
|
6530
|
+
// changes every frame but isn't pure noise (smooth-ish accel).
|
|
6531
|
+
var jitter = 1 + (Math.sin(now/380 + ci*1.7) * 0.05);
|
|
6532
|
+
car.x += car.dir * car.speed * jitter * (cdt/1000);
|
|
6270
6533
|
// Wrap the car around the road. When it leaves one end, it
|
|
6271
|
-
// re-enters the opposite end on the other lane
|
|
6272
|
-
//
|
|
6534
|
+
// re-enters the opposite end on the other lane — and the
|
|
6535
|
+
// re-entry x is randomized across the visible road so two
|
|
6536
|
+
// cars that arrived at the boundary together don't land on
|
|
6537
|
+
// top of each other.
|
|
6273
6538
|
if(car.dir > 0 && car.x > PARK_W + 0.5){
|
|
6274
|
-
car
|
|
6539
|
+
// Outbound (right-bound) car reached the right edge —
|
|
6540
|
+
// re-enter on the left. We swap to the OTHER lane so two
|
|
6541
|
+
// lanes of traffic are visible at all times; the wrap dir
|
|
6542
|
+
// is explicitly set to +1 so the car continues in its
|
|
6543
|
+
// original direction on the new lane. (Previous version
|
|
6544
|
+
// computed dir from the *post-wrap* y, which always
|
|
6545
|
+
// flipped the car to the opposite direction — cars would
|
|
6546
|
+
// oscillate at the road edge instead of crossing it.)
|
|
6547
|
+
car.x = -0.5 - Math.random() * (PARK_W * 0.5);
|
|
6275
6548
|
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6276
|
-
car.dir =
|
|
6549
|
+
car.dir = +1;
|
|
6277
6550
|
} else if(car.dir < 0 && car.x < -0.5){
|
|
6278
|
-
car
|
|
6551
|
+
// Inbound (left-bound) car reached the left edge —
|
|
6552
|
+
// re-enter on the right, swap to the other lane, keep
|
|
6553
|
+
// direction at -1. (See note above about the prior bug.)
|
|
6554
|
+
car.x = PARK_W + 0.5 + Math.random() * (PARK_W * 0.5);
|
|
6279
6555
|
car.y = (car.y < -0.9) ? -0.65 : -1.18;
|
|
6280
|
-
car.dir =
|
|
6556
|
+
car.dir = -1;
|
|
6281
6557
|
}
|
|
6282
6558
|
}
|
|
6283
6559
|
}
|
|
@@ -6489,6 +6765,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6489
6765
|
ensure();
|
|
6490
6766
|
loadSDK();
|
|
6491
6767
|
loadImages();
|
|
6768
|
+
// Pull the latest operator-saved positions from the scheduler
|
|
6769
|
+
// and seed the in-memory override map. Without this step the
|
|
6770
|
+
// dashboard would only know about local (this-session) drags
|
|
6771
|
+
// and would reset to slot defaults on every reload.
|
|
6772
|
+
try{
|
|
6773
|
+
getJson('/api/character-presets'+QS).then(function(list){
|
|
6774
|
+
if(Array.isArray(list)){
|
|
6775
|
+
for(var pi=0; pi<list.length; pi++){
|
|
6776
|
+
var p = list[pi];
|
|
6777
|
+
if(p && p.id && typeof p.nx === 'number' && typeof p.ny === 'number'){
|
|
6778
|
+
_parkPosOverrides[p.id] = { nx: p.nx, ny: p.ny };
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
// re-apply on the live node list (if it's already populated
|
|
6783
|
+
// by a prior /fed/status frame)
|
|
6784
|
+
if(typeof _applyPosOverrides === 'function') _applyPosOverrides();
|
|
6785
|
+
}).catch(function(){ /* offline — use slot defaults */ });
|
|
6786
|
+
}catch(e){}
|
|
6492
6787
|
if(lastStatus) onStatus(lastStatus);
|
|
6493
6788
|
resize();
|
|
6494
6789
|
connectStream();
|
|
@@ -7164,7 +7459,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7164
7459
|
+'<div class="pk-note" id="pk-camnote">seeking live feed…</div>'
|
|
7165
7460
|
+'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-golive" data-robot="'+esc(n.name)+'">↻ Reconnect camera</button><button class="rp-btn primary" id="pk-camera-fullscreen">Fullscreen operations view</button><span class="pk-note" id="pk-golivenote" style="margin:0">always-on RoboVision relay</span></div>'
|
|
7166
7461
|
+'<label class="pk-prodtoggle"><input type="checkbox" id="pk-prodmode"'+(devForPm.production_mode?' checked':'')+'><span>Production mode — continuous motion-triggered loop, camera always live</span></label>')
|
|
7167
|
-
+ sect('Remote production test','<div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
|
|
7462
|
+
+ sect('Remote production test','<div id="pk-live-stage" class="pk-live-stage-focus waiting"><span class="eyebrow">Current production stage</span><b>Checking robot state...</b><span>Waiting for scheduler telemetry</span></div><div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
|
|
7168
7463
|
+ '<div class="pk-ctl" style="margin-top:0.55rem"><button class="rp-btn primary" id="pk-unattended-test">Run unattended readiness</button><button class="rp-btn" id="pk-pipeline-start">Start full test</button><button class="rp-btn danger" id="pk-pipeline-stop">Stop conversation</button></div>'
|
|
7169
7464
|
+ '<div class="pk-note" id="pk-unattended-result">Unattended readiness runs an acoustic speaker/mic round-trip, triggers the production motion path, and requires greeting playback on the robot.</div>'
|
|
7170
7465
|
+ '<div class="pk-pipeline" id="pk-pipeline"></div>')
|
|
@@ -7356,7 +7651,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7356
7651
|
return stages[Math.min(stages.length-1,lastOk+1)];
|
|
7357
7652
|
}
|
|
7358
7653
|
function renderPipeline(data){
|
|
7359
|
-
var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'); if(!host||!summary) return;
|
|
7654
|
+
var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'),focus=$('pk-live-stage'); if(!host||!summary) return;
|
|
7360
7655
|
var latest={}, prereqs=data.prerequisites||{}, session=data.latest_session||{}, sid=session.id||null;
|
|
7361
7656
|
(data.events||[]).forEach(function(ev){
|
|
7362
7657
|
if(ev.session_id && sid && ev.session_id!==sid) return;
|
|
@@ -7365,15 +7660,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7365
7660
|
['robot_online','camera_ready','microphone_ready','speaker_ready'].forEach(function(stage){
|
|
7366
7661
|
if(!latest[stage]) latest[stage]={stage:stage,status:prereqs[stage]?'ok':'blocked',message:prereqs[stage]?'Ready':'Not reported'};
|
|
7367
7662
|
});
|
|
7368
|
-
var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),current=pipelineCurrentStage(stages,latest,running);
|
|
7663
|
+
var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),authoritative=data.current_stage||{},current=authoritative.stage||pipelineCurrentStage(stages,latest,running);
|
|
7369
7664
|
host.innerHTML=stages.map(function(stage,index){
|
|
7370
|
-
var ev=latest[stage],isCurrent=stage===current,status=ev&&ev.status||(isCurrent?'running':'pending');
|
|
7371
|
-
var detail=ev&&(ev.message||ev.timestamp)||'waiting';
|
|
7665
|
+
var ev=latest[stage],isCurrent=stage===current,status=(isCurrent&&authoritative.status)||ev&&ev.status||(isCurrent?'running':'pending');
|
|
7666
|
+
var detail=isCurrent&&(authoritative.message||authoritative.label)||ev&&(ev.message||ev.timestamp)||'waiting';
|
|
7372
7667
|
var time=ev&&ev.timestamp?new Date(ev.timestamp+'Z').toLocaleString():'';
|
|
7373
7668
|
var phase=PIPELINE_PHASES[stage]||'session';
|
|
7374
|
-
return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'</span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
|
|
7669
|
+
return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"'+(isCurrent?' aria-current="step"':'')+'><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'<em class="step-state">'+esc(isCurrent?'ACTIVE':status)+'</em></span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
|
|
7375
7670
|
}).join('');
|
|
7376
7671
|
var blockers=data.blockers||[];
|
|
7672
|
+
if(focus){var focusStatus=authoritative.status||blockers.length?'blocked':running?'active':'waiting';if(authoritative.status)focusStatus=authoritative.status;focus.className='pk-live-stage-focus '+esc(focusStatus);focus.innerHTML='<span class="eyebrow">Current production stage</span><b>'+esc(authoritative.label||PIPELINE_LABELS[current]||current||'Standing by')+'</b><span>'+esc(authoritative.message||blockers[0]||'Robot is armed and waiting for motion')+'</span>';}
|
|
7377
7673
|
summary.className='pk-pipeline-summary '+esc(data.overall||'blocked');
|
|
7378
7674
|
summary.textContent=blockers.length?('BLOCKED: '+blockers[0]):(running?('ACTIVE: '+(PIPELINE_LABELS[current]||current||'starting')+' | session '+(sid||'allocating')):'READY: camera-to-speaker remote test can start');
|
|
7379
7675
|
var micHealth=$('pk-mic-health');if(micHealth){
|
|
@@ -7519,11 +7815,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7519
7815
|
function refreshCameraOperations(){
|
|
7520
7816
|
var n=cameraOpsNode;if(!n)return;var row=schedRow(n)||{},device=schedulerDevice(n)||{},hw=hwByNode[n.nodeId]||{};
|
|
7521
7817
|
var readiness=String(row.readiness_code||(!n.connected?'offline':'mesh_online')),active=!!(row.active_session||row.in_session||row.session_active||row.current_session_id||row.status==='running'||row.status==='connecting');
|
|
7818
|
+
var currentStage=row.current_stage||{},currentStageLabel=currentStage.label||row.current_stage_label||(active?'Conversation starting':device.production_mode?'Armed - waiting for motion':'Production paused'),currentStageStatus=currentStage.status||row.current_stage_status||(active?'active':'waiting');
|
|
7522
7819
|
var img=$('pk-camera-ops-img'),cameraLive=!!(img&&img.dataset.live==='true'),clockEl=$('pk-camera-ops-clock');if(clockEl)clockEl.textContent=clock(Date.now());
|
|
7523
7820
|
var rec=$('pk-camera-ops-rec');if(rec)rec.textContent=(cameraLive?'REC / LIVE':'REC / CONNECTING')+' / '+String(n.displayName||n.name||'robot').toUpperCase();
|
|
7524
|
-
var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(
|
|
7821
|
+
var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(currentStageLabel)+'</b><span>active stage</span></div><div class="metric"><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>readiness</span></div><div class="metric"><b>'+(row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'--')+'</b><span>average latency</span></div><div class="metric"><b>'+(row.drop_rate!=null?Math.round(row.drop_rate*100)+'%':'--')+'</b><span>session drops</span></div>';
|
|
7525
7822
|
var insights=$('pk-camera-ops-insights');if(insights)insights.innerHTML='<span>Camera relay</span><b class="'+(cameraLive?'ok':'bad')+'">'+(cameraLive?'LIVE':'CONNECTING')+'</b><span>Mesh node</span><b class="'+(n.connected?'ok':'bad')+'">'+(n.connected?'connected':'offline')+'</b><span>Production mode</span><b class="'+(device.production_mode?'ok':'')+'">'+(device.production_mode?'armed':'disabled')+'</b><span>Readiness</span><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>Heartbeat</span><b>'+(row.heartbeat_age_seconds!=null?Math.round(row.heartbeat_age_seconds)+'s ago':'--')+'</b><span>Current room</span><b>'+esc(String(row.current_session_id||row.active_session_id||'none').slice(0,24))+'</b><span>Triggers</span><b>'+esc(row.trigger_count!=null?row.trigger_count:0)+'</b><span>Sessions</span><b>'+esc(row.sessions_total!=null?row.sessions_total:0)+'</b>';
|
|
7526
|
-
var stages=[['Robot online',!!n.connected],['Camera streaming',cameraLive],['Motion detector armed',!!device.production_mode],['Scheduler session',active
|
|
7823
|
+
var stages=[[currentStageLabel,true,'ACTIVE'],['Robot online',!!n.connected,'ready'],['Camera streaming',cameraLive,'ready'],['Motion detector armed',!!device.production_mode,'armed'],['Scheduler session',active,'active']],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s,index){return '<div class="'+(index===0?'fleet-stage-focus '+esc(currentStageStatus):'')+'"><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?s[2]:'waiting')+'</b></div>';}).join('');
|
|
7527
7824
|
var inv=device.device_inventory||{},mh=inv.media_health||{},hardware=$('pk-camera-ops-hardware');if(hardware)hardware.innerHTML='<span>CPU</span><b>'+(hw.cpu&&hw.cpu.percent!=null?Math.round(hw.cpu.percent)+'%':'--')+'</b><span>Memory</span><b>'+(hw.memory&&hw.memory.percent!=null?Math.round(hw.memory.percent)+'%':'--')+'</b><span>Temperature</span><b>'+(hw.temperatureC!=null?Math.round(hw.temperatureC)+'C':'--')+'</b><span>Camera inputs</span><b>'+((inv.video||[]).length||0)+'</b><span>Microphones</span><b>'+((inv.audio_input||[]).length||0)+'</b><span>Speaker outputs</span><b>'+((inv.audio_output||[]).length||0)+'</b><span>Camera permission</span><b class="'+(mh.camera_access===false?'bad':'ok')+'">'+(mh.camera_access===false?'DENIED':mh.camera_access===true?'granted':'--')+'</b><span>Audio permission</span><b class="'+(mh.audio_access===false?'bad':'ok')+'">'+(mh.audio_access===false?'DENIED':mh.audio_access===true?'granted':'--')+'</b><span>Camera worker</span><b class="'+(mh.camera_stalled?'bad':'ok')+'">'+(mh.camera_stalled?'STALLED':mh.camera_worker?'streaming':'--')+'</b><span>Frame age</span><b>'+(mh.camera_frame_age_seconds!=null?Number(mh.camera_frame_age_seconds).toFixed(1)+'s':'--')+'</b><span>Selected camera</span><b>'+esc(device.video_device||'auto')+'</b><span>Selected microphone</span><b>'+esc(device.audio_device||'default')+'</b>';
|
|
7528
7825
|
var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
|
|
7529
7826
|
}
|