infinicode 2.8.65 → 2.8.67
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 +4 -5
- package/dist/kernel/federation/dashboard-html.js +266 -53
- package/dist/kernel/federation/federation.js +1 -0
- package/dist/kernel/federation/peer-mesh.js +12 -0
- package/dist/kernel/federation/transport-http.d.ts +6 -1
- package/dist/kernel/federation/transport-http.js +75 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +393 -42
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
* for its cam / mic / speaker / hardware drawer)
|
|
15
15
|
* Sessions & Telemetry — RoboPark scheduler stats + production_mode + controls
|
|
16
16
|
*
|
|
17
|
-
* The Park cam panel
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* That token/track is produced by the robot-side RoboPark plugin.
|
|
17
|
+
* The Park cam panel relays RoboVisionAI_PI's existing MJPEG feed through the
|
|
18
|
+
* authenticated mesh. LiveKit remains the voice transport but is deliberately
|
|
19
|
+
* not used for dashboard video, avoiding camera ownership conflicts on robots.
|
|
21
20
|
*
|
|
22
21
|
* Kept free of backticks and ${...} so the whole page fits inside this TS
|
|
23
22
|
* template literal without escaping; the client JS uses string concatenation.
|
|
@@ -530,8 +529,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
530
529
|
.rp-modal-body .kv{display:grid;grid-template-columns:160px 1fr;gap:0.4rem 0.8rem;margin-bottom:0.4rem;}
|
|
531
530
|
.rp-modal-body .kv .k{color:var(--muted);font-size:0.66rem;text-transform:uppercase;letter-spacing:0.05em;}
|
|
532
531
|
.rp-modal-body .kv .v{color:var(--ink);word-break:break-word;white-space:pre-wrap;}
|
|
533
|
-
.rp-modal-body pre{background:var(--panel-solid);border:1px solid var(--glass-border);border-radius:8px;padding:0.5rem 0.7rem;
|
|
534
|
-
font-size:0.66rem;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-word;color:var(--ink);}
|
|
532
|
+
.rp-modal-body pre{background:var(--panel-solid);border:1px solid var(--glass-border);border-radius:8px;padding:0.5rem 0.7rem;
|
|
533
|
+
font-size:0.66rem;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-word;color:var(--ink);}
|
|
534
|
+
|
|
535
|
+
.rp-call-shell{display:grid;grid-template-columns:minmax(260px,0.8fr) minmax(320px,1.2fr);gap:0.8rem;min-height:500px;}
|
|
536
|
+
.rp-call-panel{border:1px solid var(--glass-border);border-radius:14px;background:var(--panel);box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi);padding:0.85rem;}
|
|
537
|
+
.rp-call-character{display:flex;align-items:center;gap:0.8rem;padding:0.7rem;border:1px solid var(--glass-border);border-radius:12px;background:var(--panel-2);margin-bottom:0.75rem;}
|
|
538
|
+
.rp-call-character img{width:78px;height:78px;object-fit:contain;filter:drop-shadow(0 8px 16px rgba(0,0,0,.35));}
|
|
539
|
+
.rp-call-character .name{font-size:1.05rem;color:var(--gold-bright);font-weight:700;}
|
|
540
|
+
.rp-call-state{display:flex;align-items:center;gap:0.55rem;font-family:var(--mono);font-size:0.72rem;padding:0.65rem 0;}
|
|
541
|
+
.rp-call-orb{width:10px;height:10px;border-radius:50%;background:var(--muted);box-shadow:0 0 0 transparent;}
|
|
542
|
+
.rp-call-orb.live{background:var(--ok);box-shadow:0 0 12px var(--ok);animation:pulse 1.6s infinite;}
|
|
543
|
+
.rp-call-orb.busy{background:var(--gold-bright);box-shadow:0 0 12px var(--gold-bright);animation:pulse 1s infinite;}
|
|
544
|
+
.rp-call-actions{display:flex;gap:0.45rem;flex-wrap:wrap;margin-top:0.7rem;}
|
|
545
|
+
.rp-call-transcript{height:430px;overflow-y:auto;display:flex;flex-direction:column;gap:0.55rem;padding:0.3rem;}
|
|
546
|
+
.rp-turn{max-width:86%;border:1px solid var(--glass-border);border-radius:12px;padding:0.55rem 0.7rem;font-family:var(--mono);font-size:0.72rem;line-height:1.45;background:var(--panel-2);}
|
|
547
|
+
.rp-turn.user{align-self:flex-end;border-color:color-mix(in srgb,var(--info) 45%,transparent);background:color-mix(in srgb,var(--info) 9%,var(--panel-2));}
|
|
548
|
+
.rp-turn.assistant{align-self:flex-start;border-color:color-mix(in srgb,var(--gold-bright) 42%,transparent);}
|
|
549
|
+
.rp-turn .who{font-size:0.57rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:.18rem;}
|
|
550
|
+
.rp-detail-hero{display:flex;align-items:center;gap:.8rem;padding:.6rem;border:1px solid var(--glass-border);border-radius:12px;background:var(--panel-2);margin-bottom:.8rem;}
|
|
551
|
+
.rp-detail-hero img{width:72px;height:72px;object-fit:contain;}
|
|
552
|
+
.rp-pipeline{display:flex;flex-direction:column;gap:.3rem;margin-top:.5rem;}
|
|
553
|
+
.rp-pipeline-row{display:grid;grid-template-columns:76px 125px 70px 1fr;gap:.5rem;padding:.38rem .5rem;border-left:3px solid var(--glass-border);background:var(--panel-2);border-radius:5px;font-family:var(--mono);font-size:.64rem;}
|
|
554
|
+
.rp-pipeline-row.ok{border-left-color:var(--ok)} .rp-pipeline-row.failed,.rp-pipeline-row.blocked{border-left-color:var(--bad)}
|
|
555
|
+
.node .node-hero{display:flex;align-items:center;gap:.65rem;margin:.45rem 0 .55rem;}
|
|
556
|
+
.node .node-avatar{width:68px;height:68px;object-fit:contain;flex:none;filter:drop-shadow(0 7px 12px rgba(0,0,0,.3));}
|
|
557
|
+
.node .node-health{font-family:var(--mono);font-size:.61rem;line-height:1.45;color:var(--muted);}
|
|
558
|
+
.node .node-health b{color:var(--ink);}
|
|
559
|
+
@media(max-width:820px){.rp-call-shell{grid-template-columns:1fr}.rp-call-transcript{height:320px}.rp-pipeline-row{grid-template-columns:62px 105px 58px 1fr}}
|
|
535
560
|
|
|
536
561
|
/* ── TTS voice picker (used inside the generic modal) ── */
|
|
537
562
|
.rp-voice-picker{display:flex;flex-direction:column;gap:0.6rem;min-height:320px;}
|
|
@@ -659,7 +684,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
659
684
|
<div class="rp-subtabs" id="rp-subtabs" role="tablist">
|
|
660
685
|
<button class="rp-subtab active" data-sub="overview" id="rpsbtn-overview" role="tab" aria-selected="true">Overview</button>
|
|
661
686
|
<button class="rp-subtab" data-sub="incidents" id="rpsbtn-incidents" role="tab" aria-selected="false">Incidents <span class="badge" id="rps-incidents-badge">0</span></button>
|
|
662
|
-
<button class="rp-subtab" data-sub="live" id="rpsbtn-live" role="tab" aria-selected="false">Live Sessions <span class="badge" id="rps-live-badge">0</span></button>
|
|
687
|
+
<button class="rp-subtab" data-sub="live" id="rpsbtn-live" role="tab" aria-selected="false">Live Sessions <span class="badge" id="rps-live-badge">0</span></button>
|
|
688
|
+
<button class="rp-subtab" data-sub="voice" id="rpsbtn-voice" role="tab" aria-selected="false">Voice Call</button>
|
|
663
689
|
<button class="rp-subtab" data-sub="history" id="rpsbtn-history" role="tab" aria-selected="false">History</button>
|
|
664
690
|
<button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
|
|
665
691
|
<button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
|
|
@@ -701,7 +727,30 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
701
727
|
</div>
|
|
702
728
|
|
|
703
729
|
<!-- ═══ History ═══ -->
|
|
704
|
-
<div class="rp-sub" id="rp-sub-
|
|
730
|
+
<div class="rp-sub" id="rp-sub-voice">
|
|
731
|
+
<div class="rp-call-shell">
|
|
732
|
+
<div class="rp-call-panel">
|
|
733
|
+
<div class="rp-section-h">Operator voice call <span class="meta">RoboVoice pipeline</span></div>
|
|
734
|
+
<div class="rp-call-character" id="rp-call-character"><div><div class="name">Choose a character</div><div class="meta">The selected preset and stack are frozen into session history.</div></div></div>
|
|
735
|
+
<div class="rp-field"><label>Character preset</label><select id="rp-call-character-select"><option value="">select character...</option></select></div>
|
|
736
|
+
<div class="rp-field" style="margin-top:.55rem"><label>Voice stack</label><select id="rp-call-stack-select"><option value="">character default</option></select></div>
|
|
737
|
+
<div class="rp-call-state"><span class="rp-call-orb" id="rp-call-orb"></span><span id="rp-call-status">ready to start</span></div>
|
|
738
|
+
<div class="rp-call-actions">
|
|
739
|
+
<button class="rp-btn primary" id="rp-call-start">Start voice call</button>
|
|
740
|
+
<button class="rp-btn" id="rp-call-mute" disabled>Mute microphone</button>
|
|
741
|
+
<button class="rp-btn danger" id="rp-call-end" disabled>End call</button>
|
|
742
|
+
</div>
|
|
743
|
+
<div class="pk-note" style="margin-top:.7rem">Uses your browser microphone. Agent audio plays here and every final transcript turn is saved to session history.</div>
|
|
744
|
+
</div>
|
|
745
|
+
<div class="rp-call-panel">
|
|
746
|
+
<div class="rp-section-h">Live transcript <span class="meta" id="rp-call-session">not connected</span></div>
|
|
747
|
+
<div class="rp-call-transcript" id="rp-call-transcript"><div class="rp-tbl-empty">Transcript appears when the call starts.</div></div>
|
|
748
|
+
<div id="rp-call-audio" hidden></div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<div class="rp-sub" id="rp-sub-history">
|
|
705
754
|
<div class="rp-filterbar">
|
|
706
755
|
<div class="rp-field"><label>Search</label><input id="rp-hist-search" placeholder="session id, robot…"></div>
|
|
707
756
|
<div class="rp-field"><label>Robot</label><select id="rp-hist-robot"><option value="">all robots</option></select></div>
|
|
@@ -725,7 +774,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
725
774
|
<div class="rp-tbl-wrap" id="rp-hist-wrap">
|
|
726
775
|
<table class="rp-history-table" id="rp-hist-table">
|
|
727
776
|
<thead><tr>
|
|
728
|
-
<th>Session</th><th>Robot</th><th>Started</th><th>Ended</th><th>Duration</th><th>End reason</th><th>
|
|
777
|
+
<th>Session</th><th>Robot</th><th>Started</th><th>Ended</th><th>Duration</th><th>End reason</th><th>Turns</th><th>Pipeline</th><th>Latency</th><th>Actions</th>
|
|
729
778
|
</tr></thead>
|
|
730
779
|
<tbody id="rp-hist-tbody"></tbody>
|
|
731
780
|
</table>
|
|
@@ -1143,7 +1192,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1143
1192
|
+ '<div class="bar-track"><div class="bar-fill '+loadClass(v)+'" style="width:'+v+'%"></div></div>'
|
|
1144
1193
|
+ '<span class="mv '+(cls||'')+'">'+esc(valTxt)+'</span></div>';
|
|
1145
1194
|
}
|
|
1146
|
-
function
|
|
1195
|
+
function rpAvatarFor(label){
|
|
1196
|
+
var key=String(label||'').toLowerCase();
|
|
1197
|
+
var presets=(typeof characterPresets!=='undefined' && Array.isArray(characterPresets))?characterPresets:[];
|
|
1198
|
+
for(var i=0;i<presets.length;i++){
|
|
1199
|
+
var p=presets[i], aliases=[p.id,p.name];
|
|
1200
|
+
for(var j=0;j<aliases.length;j++){
|
|
1201
|
+
var a=String(aliases[j]||'').toLowerCase();
|
|
1202
|
+
if(a && (key===a || key.indexOf(a)>=0 || a.indexOf(key)>=0)) return p.img||'';
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if(key.indexOf('bmw')>=0 || key.indexOf('vixen')>=0) return '/static/robopark/bmw.png';
|
|
1206
|
+
var known=['volt','tesla','jaguar','panda'];
|
|
1207
|
+
for(var k=0;k<known.length;k++) if(key.indexOf(known[k])>=0) return '/static/robopark/'+known[k]+'.png';
|
|
1208
|
+
return '';
|
|
1209
|
+
}
|
|
1210
|
+
function rpTelemetryForNode(n){
|
|
1211
|
+
if(typeof roboparkByName==='undefined' || !roboparkByName) return null;
|
|
1212
|
+
var aliases=[n&&n.displayName,n&&n.name,n&&n.nodeId];
|
|
1213
|
+
for(var i=0;i<aliases.length;i++){
|
|
1214
|
+
var key=String(aliases[i]||'').toLowerCase();
|
|
1215
|
+
if(roboparkByName[key]) return roboparkByName[key];
|
|
1216
|
+
for(var existing in roboparkByName){
|
|
1217
|
+
if(existing===key || (key && existing.indexOf(key)>=0) || (existing && key.indexOf(existing)>=0)) return roboparkByName[existing];
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
function renderFleet(nodes){
|
|
1147
1223
|
var el = $('fleet');
|
|
1148
1224
|
if(!nodes || !nodes.length){ el.innerHTML = '<div class="empty">No nodes reporting yet. Start peers with <span class="mono">infinicode serve</span> or <span class="mono">robopark setup</span>.</div>'; return; }
|
|
1149
1225
|
// connected first, then robots/satellites above hubs so the fleet reads robot-first
|
|
@@ -1164,13 +1240,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1164
1240
|
var disk = (hw&&hw.disk&&hw.disk.mounts&&hw.disk.mounts[0]) ? hw.disk.mounts[0].percent : null;
|
|
1165
1241
|
var nt = netRxTx(hw);
|
|
1166
1242
|
var upt = hw&&hw.system ? hw.system.uptimeSec : null;
|
|
1167
|
-
var caps = (n.capabilities||[]).filter(function(c){ return c.indexOf('backend:')!==0; }).slice(0,5);
|
|
1168
|
-
var capHtml=''; for(var c=0;c<caps.length;c++) capHtml += '<span class="cap">'+esc(caps[c])+'</span>';
|
|
1243
|
+
var caps = (n.capabilities||[]).filter(function(c){ return c.indexOf('backend:')!==0; }).slice(0,5);
|
|
1244
|
+
var capHtml=''; for(var c=0;c<caps.length;c++) capHtml += '<span class="cap">'+esc(caps[c])+'</span>';
|
|
1245
|
+
var tele=rpTelemetryForNode(n);
|
|
1246
|
+
var avatar=rpAvatarFor(tele&&(tele.character_name||tele.character_id)||n.displayName);
|
|
1247
|
+
var readiness=tele&&(tele.readiness_code||tele.status)||(!up?'offline':'mesh online');
|
|
1248
|
+
var hb=tele&&tele.heartbeat_age_seconds!=null?tele.heartbeat_age_seconds:null;
|
|
1169
1249
|
var site = n.site?(' · '+esc(n.site)):'';
|
|
1170
1250
|
|
|
1171
1251
|
html += '<div class="node '+(up?'up':'down')+(selected===n.nodeId?' sel':'')+'" tabindex="0" role="button" data-id="'+esc(n.nodeId)+'" data-name="'+esc(n.displayName)+'">'
|
|
1172
1252
|
+ '<div class="top"><span class="name"><span class="statdot '+(up?'on':'off')+'"></span>'+esc(n.displayName)+(isSelf?' <span class="selfbadge">this</span>':'')+'</span><span class="role">'+esc(n.role)+'</span></div>'
|
|
1173
|
-
+ '<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>'
|
|
1253
|
+
+ '<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>'
|
|
1254
|
+
+ (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>'
|
|
1255
|
+
+(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>'):'');
|
|
1174
1256
|
|
|
1175
1257
|
if(up){
|
|
1176
1258
|
html += mrow('CPU', cpu, cpu+'%'+(cores?(' /'+cores+'c'):''), loadClass(cpu))
|
|
@@ -1611,12 +1693,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1611
1693
|
});
|
|
1612
1694
|
|
|
1613
1695
|
var characterPresets=[];
|
|
1614
|
-
function loadCharacterPresets(){
|
|
1615
|
-
fetch('/robopark/api/character-presets'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){ characterPresets=Array.isArray(d)?d:(d&&d.presets)||[]; fillCharacterDropdowns(); fillTemplateCharacterList(); refreshDrawerPresetOptions(); }).catch(function(){});
|
|
1616
|
-
}
|
|
1617
|
-
function loadVoiceStacks(){
|
|
1618
|
-
fetch('/robopark/api/voice-stacks'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){ voiceStacks=Array.isArray(d)?d:(d&&d.stacks)||[]; fillVoiceStackDropdowns(); fillTemplateVoiceList(); refreshDrawerPresetOptions(); }).catch(function(){});
|
|
1619
|
-
}
|
|
1696
|
+
function loadCharacterPresets(){
|
|
1697
|
+
return fetch('/robopark/api/character-presets'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){ characterPresets=Array.isArray(d)?d:(d&&d.presets)||[]; fillCharacterDropdowns(); fillTemplateCharacterList(); refreshDrawerPresetOptions(); refreshVoiceCallOptions(); return characterPresets; }).catch(function(){ return []; });
|
|
1698
|
+
}
|
|
1699
|
+
function loadVoiceStacks(){
|
|
1700
|
+
return fetch('/robopark/api/voice-stacks'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){ voiceStacks=Array.isArray(d)?d:(d&&d.stacks)||[]; fillVoiceStackDropdowns(); fillTemplateVoiceList(); refreshDrawerPresetOptions(); refreshVoiceCallOptions(); return voiceStacks; }).catch(function(){ return []; });
|
|
1701
|
+
}
|
|
1620
1702
|
|
|
1621
1703
|
// ── TTS voice catalog (per provider) ──
|
|
1622
1704
|
// Holds the dropdown options for whichever TTS provider is currently
|
|
@@ -2081,7 +2163,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2081
2163
|
for (var i=0;i<robots.length;i++){
|
|
2082
2164
|
var x = robots[i];
|
|
2083
2165
|
var name = robotName(x);
|
|
2084
|
-
var avatar =
|
|
2166
|
+
var avatar = rpAvatarFor(x.character_name||x.character_id||x.character_preset_id||name);
|
|
2085
2167
|
var lat = x.avg_latency_ms!=null?Math.round(x.avg_latency_ms)+'ms':'—';
|
|
2086
2168
|
var dcls = x.drop_rate!=null?(x.drop_rate>=0.2?'bad':(x.drop_rate>=0.05?'warn':'ok')):'';
|
|
2087
2169
|
var drop = x.drop_rate!=null?Math.round(x.drop_rate*100)+'%':'—';
|
|
@@ -2193,14 +2275,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2193
2275
|
s.classList.toggle('active', s.id==='rp-sub-'+name);
|
|
2194
2276
|
}
|
|
2195
2277
|
// stop every sub-tab interval, then start only the active one
|
|
2196
|
-
var names = ['overview','incidents','live','history','audit','fleet','setup'];
|
|
2278
|
+
var names = ['overview','incidents','live','voice','history','audit','fleet','setup'];
|
|
2197
2279
|
for(var k=0;k<names.length;k++){
|
|
2198
2280
|
var n=names[k];
|
|
2199
2281
|
if(rpIntervals[n]){ clearInterval(rpIntervals[n]); rpIntervals[n]=null; }
|
|
2200
2282
|
}
|
|
2201
2283
|
if(name==='overview'){ pollOverview(); rpIntervals.overview = setInterval(pollOverview, 4000); }
|
|
2202
2284
|
if(name==='incidents'){ pollIncidents(); rpIntervals.incidents = setInterval(pollIncidents, 4000); }
|
|
2203
|
-
if(name==='live'){ pollLiveSessions(); startLiveAutoRefresh(); }
|
|
2285
|
+
if(name==='live'){ pollLiveSessions(); startLiveAutoRefresh(); }
|
|
2286
|
+
if(name==='voice'){ refreshVoiceCallOptions(); }
|
|
2204
2287
|
if(name==='history'){ rpHistoryPage=0; pollHistory(); }
|
|
2205
2288
|
if(name==='audit'){ pollAuditLog(); rpIntervals.audit = setInterval(pollAuditLog, 6000); }
|
|
2206
2289
|
if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
|
|
@@ -2219,7 +2302,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2219
2302
|
var h=String(location.hash||'').replace('#','');
|
|
2220
2303
|
var m = h.match(/^robopark\\/(.+)$/);
|
|
2221
2304
|
var initial = (m && m[1]) ? m[1] : 'overview';
|
|
2222
|
-
if(['overview','incidents','live','history','audit','fleet','setup'].indexOf(initial)<0) initial='overview';
|
|
2305
|
+
if(['overview','incidents','live','voice','history','audit','fleet','setup'].indexOf(initial)<0) initial='overview';
|
|
2223
2306
|
setRpSubTab(initial);
|
|
2224
2307
|
}
|
|
2225
2308
|
|
|
@@ -2514,7 +2597,111 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2514
2597
|
})();
|
|
2515
2598
|
|
|
2516
2599
|
// ── History sub-tab ──
|
|
2517
|
-
var
|
|
2600
|
+
var rpVoiceCall={room:null,session:null,eventToken:null,muted:false,ending:false,seen:{}};
|
|
2601
|
+
function refreshVoiceCallOptions(){
|
|
2602
|
+
var cs=$('rp-call-character-select'), ss=$('rp-call-stack-select');
|
|
2603
|
+
if(!cs||!ss) return;
|
|
2604
|
+
var cv=cs.value, sv=ss.value;
|
|
2605
|
+
cs.innerHTML='<option value="">select character...</option>'+(characterPresets||[]).map(function(c){return '<option value="'+esc(c.id)+'">'+esc(c.name||c.id)+'</option>';}).join('');
|
|
2606
|
+
ss.innerHTML='<option value="">character default</option>'+(voiceStacks||[]).map(function(s){return '<option value="'+esc(s.id)+'">'+esc(s.name||s.id)+' - '+esc(s.stt_provider)+' / '+esc(s.llm_model)+' / '+esc(s.tts_provider)+'</option>';}).join('');
|
|
2607
|
+
if(cv) cs.value=cv; if(sv) ss.value=sv;
|
|
2608
|
+
paintVoiceCallCharacter();
|
|
2609
|
+
}
|
|
2610
|
+
function paintVoiceCallCharacter(){
|
|
2611
|
+
var sel=$('rp-call-character-select'), box=$('rp-call-character'); if(!sel||!box) return;
|
|
2612
|
+
var chosen=null; for(var i=0;i<characterPresets.length;i++) if(characterPresets[i].id===sel.value){chosen=characterPresets[i];break;}
|
|
2613
|
+
if(!chosen){ box.innerHTML='<div><div class="name">Choose a character</div><div class="meta">The selected preset and stack are frozen into session history.</div></div>'; return; }
|
|
2614
|
+
var img=chosen.img||rpAvatarFor(chosen.id||chosen.name);
|
|
2615
|
+
box.innerHTML=(img?'<img alt="'+esc(chosen.name)+'" src="'+esc(img+QS)+'">':'')+'<div><div class="name">'+esc(chosen.name||chosen.id)+'</div><div class="meta">'+esc(chosen.description||'Ready for an operator voice call')+'</div></div>';
|
|
2616
|
+
var stack=$('rp-call-stack-select'); if(stack && !stack.value && chosen.voice_stack_id) stack.value=chosen.voice_stack_id;
|
|
2617
|
+
}
|
|
2618
|
+
function setVoiceCallStatus(text,state){
|
|
2619
|
+
var s=$('rp-call-status'), o=$('rp-call-orb'); if(s)s.textContent=text; if(o)o.className='rp-call-orb '+(state||'');
|
|
2620
|
+
}
|
|
2621
|
+
function ensureVoiceLiveKit(){
|
|
2622
|
+
if(window.LivekitClient) return Promise.resolve(window.LivekitClient);
|
|
2623
|
+
return new Promise(function(resolve,reject){
|
|
2624
|
+
var old=document.querySelector('script[data-rp-voice-livekit]');
|
|
2625
|
+
if(old){ old.addEventListener('load',function(){resolve(window.LivekitClient);}); old.addEventListener('error',reject); return; }
|
|
2626
|
+
var script=document.createElement('script'); script.dataset.rpVoiceLivekit='1'; script.src='/vendor/livekit-client.umd.js'+QS;
|
|
2627
|
+
script.onload=function(){resolve(window.LivekitClient);}; script.onerror=function(){reject(new Error('LiveKit browser library failed to load'));}; document.head.appendChild(script);
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
function appendVoiceTurn(role,text){
|
|
2631
|
+
var wrap=$('rp-call-transcript'); if(!wrap)return;
|
|
2632
|
+
if(wrap.querySelector('.rp-tbl-empty')) wrap.innerHTML='';
|
|
2633
|
+
var div=document.createElement('div'); div.className='rp-turn '+role;
|
|
2634
|
+
div.innerHTML='<div class="who">'+esc(role==='user'?'operator':'character')+'</div><div>'+esc(text)+'</div>';
|
|
2635
|
+
wrap.appendChild(div); wrap.scrollTop=wrap.scrollHeight;
|
|
2636
|
+
}
|
|
2637
|
+
function saveVoiceTurn(role,text,sequence){
|
|
2638
|
+
if(!rpVoiceCall.session||!rpVoiceCall.eventToken)return;
|
|
2639
|
+
fetch('/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/transcript'+QS,{
|
|
2640
|
+
method:'POST',headers:Object.assign({'content-type':'application/json','X-RoboPark-Session-Token':rpVoiceCall.eventToken},operatorHeaders()),
|
|
2641
|
+
body:JSON.stringify({role:role,text:text,is_final:true,sequence:sequence,source:'control_center'})
|
|
2642
|
+
}).catch(function(){});
|
|
2643
|
+
}
|
|
2644
|
+
function startVoiceCall(){
|
|
2645
|
+
var cid=$('rp-call-character-select').value, sid=$('rp-call-stack-select').value;
|
|
2646
|
+
if(!cid){setVoiceCallStatus('select a character first','');return;}
|
|
2647
|
+
if(rpVoiceCall.room)return;
|
|
2648
|
+
setVoiceCallStatus('creating scheduled session...','busy'); $('rp-call-start').disabled=true;
|
|
2649
|
+
rpAction('POST','/robopark/api/sessions/voice-call',{character_preset_id:cid,voice_stack_id:sid||null,client_name:getOperatorName()||'RoboPark Operator'})
|
|
2650
|
+
.then(function(call){
|
|
2651
|
+
if(!call)throw new Error('scheduler returned no call');
|
|
2652
|
+
rpVoiceCall.session=call.session_id; rpVoiceCall.eventToken=call.event_token; rpVoiceCall.seen={}; rpVoiceCall.ending=false;
|
|
2653
|
+
$('rp-call-session').textContent=call.session_id; $('rp-call-transcript').innerHTML='<div class="rp-tbl-empty">Connecting to '+esc(call.character&&call.character.name||cid)+'...</div>';
|
|
2654
|
+
return ensureVoiceLiveKit().then(function(LK){
|
|
2655
|
+
var room=new LK.Room({adaptiveStream:true,dynacast:true}); rpVoiceCall.room=room;
|
|
2656
|
+
room.on(LK.RoomEvent.TrackSubscribed,function(track){ if(track.kind==='audio'){var el=track.attach();el.autoplay=true;$('rp-call-audio').appendChild(el);} });
|
|
2657
|
+
room.on(LK.RoomEvent.TranscriptionReceived,function(segments,participant){
|
|
2658
|
+
for(var i=0;i<(segments||[]).length;i++){
|
|
2659
|
+
var seg=segments[i], final=seg.final===true||seg.isFinal===true; if(!final||!String(seg.text||'').trim())continue;
|
|
2660
|
+
var key=String(seg.id||'')+'|'+String(seg.text); if(rpVoiceCall.seen[key])continue; rpVoiceCall.seen[key]=1;
|
|
2661
|
+
var identity=participant&&participant.identity||''; var role=identity.indexOf('operator:')===0?'user':'assistant';
|
|
2662
|
+
appendVoiceTurn(role,String(seg.text)); saveVoiceTurn(role,String(seg.text),Object.keys(rpVoiceCall.seen).length);
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
room.on(LK.RoomEvent.DataReceived,function(payload,participant,kind,topic){
|
|
2666
|
+
if(topic && topic!=='lk-chat-topic')return;
|
|
2667
|
+
try{
|
|
2668
|
+
var msg=JSON.parse(new TextDecoder().decode(payload));
|
|
2669
|
+
var text=String(msg.message||msg.text||'').trim(); if(!text)return;
|
|
2670
|
+
var role=msg.role==='user'?'user':'assistant';
|
|
2671
|
+
var key=role+'|'+text; if(rpVoiceCall.seen[key])return; rpVoiceCall.seen[key]=1;
|
|
2672
|
+
appendVoiceTurn(role,text); saveVoiceTurn(role,text,Object.keys(rpVoiceCall.seen).length);
|
|
2673
|
+
}catch(_){}
|
|
2674
|
+
});
|
|
2675
|
+
room.on(LK.RoomEvent.Disconnected,function(){ if(!rpVoiceCall.ending) finishVoiceCall('connection_lost'); });
|
|
2676
|
+
return room.connect(call.server_url,call.token).then(function(){return room.localParticipant.setMicrophoneEnabled(true);});
|
|
2677
|
+
}).then(function(){
|
|
2678
|
+
setVoiceCallStatus('connected - listening','live'); $('rp-call-mute').disabled=false; $('rp-call-end').disabled=false;
|
|
2679
|
+
return fetch('/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/joined'+QS,{method:'POST',headers:{'X-RoboPark-Session-Token':rpVoiceCall.eventToken}}).catch(function(){});
|
|
2680
|
+
});
|
|
2681
|
+
}).catch(function(e){
|
|
2682
|
+
setVoiceCallStatus('call failed: '+(e&&e.message||e),''); $('rp-call-start').disabled=false;
|
|
2683
|
+
if(rpVoiceCall.room){try{rpVoiceCall.room.disconnect();}catch(_){} rpVoiceCall.room=null;}
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
function toggleVoiceMute(){
|
|
2687
|
+
if(!rpVoiceCall.room)return; rpVoiceCall.muted=!rpVoiceCall.muted;
|
|
2688
|
+
rpVoiceCall.room.localParticipant.setMicrophoneEnabled(!rpVoiceCall.muted);
|
|
2689
|
+
$('rp-call-mute').textContent=rpVoiceCall.muted?'Unmute microphone':'Mute microphone'; setVoiceCallStatus(rpVoiceCall.muted?'connected - microphone muted':'connected - listening','live');
|
|
2690
|
+
}
|
|
2691
|
+
function finishVoiceCall(reason){
|
|
2692
|
+
if(rpVoiceCall.ending)return; rpVoiceCall.ending=true; var session=rpVoiceCall.session, room=rpVoiceCall.room;
|
|
2693
|
+
rpVoiceCall.room=null; if(room){try{room.disconnect();}catch(_){}}
|
|
2694
|
+
if(session) rpAction('POST','/robopark/api/sessions/'+encodeURIComponent(session)+'/end?reason='+encodeURIComponent(reason||'operator'));
|
|
2695
|
+
setVoiceCallStatus('call ended',''); $('rp-call-start').disabled=false; $('rp-call-mute').disabled=true; $('rp-call-end').disabled=true;
|
|
2696
|
+
$('rp-call-mute').textContent='Mute microphone'; rpVoiceCall.session=null; rpVoiceCall.eventToken=null; rpVoiceCall.muted=false; setTimeout(function(){rpVoiceCall.ending=false;},300);
|
|
2697
|
+
}
|
|
2698
|
+
(function(){
|
|
2699
|
+
var cs=$('rp-call-character-select'), start=$('rp-call-start'), mute=$('rp-call-mute'), end=$('rp-call-end');
|
|
2700
|
+
if(cs)cs.addEventListener('change',paintVoiceCallCharacter); if(start)start.addEventListener('click',startVoiceCall);
|
|
2701
|
+
if(mute)mute.addEventListener('click',toggleVoiceMute); if(end)end.addEventListener('click',function(){finishVoiceCall('operator');});
|
|
2702
|
+
})();
|
|
2703
|
+
|
|
2704
|
+
var rpHistoryFilter = { robot: '', endReason: '', range: '7d', search: '' };
|
|
2518
2705
|
var rpHistoryPage = 0;
|
|
2519
2706
|
var RP_HISTORY_PAGE_SIZE = 25;
|
|
2520
2707
|
function pollHistory(){
|
|
@@ -2588,7 +2775,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2588
2775
|
var dur = s.duration_seconds!=null ? s.duration_seconds : (started&&ended?Math.floor((ended-started)/1000):null);
|
|
2589
2776
|
var reason = s.end_reason || '';
|
|
2590
2777
|
var reasonCls = rpEndReasonClass(reason);
|
|
2591
|
-
var
|
|
2778
|
+
var turns = s.transcript_count!=null ? s.transcript_count : 0;
|
|
2779
|
+
var pipe = s.error_count ? (s.error_count+' errors') : ((s.pipeline_event_count||0)+' events');
|
|
2592
2780
|
var lat = s.avg_latency_ms!=null ? Math.round(s.avg_latency_ms)+'ms' : '—';
|
|
2593
2781
|
html += '<tr data-sid="'+esc(sid)+'">'
|
|
2594
2782
|
+ '<td><span class="sid" title="'+esc(sid)+'">'+esc(sid)+'</span></td>'
|
|
@@ -2597,7 +2785,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2597
2785
|
+ '<td><span class="t-rel">'+esc(rpFmtRelative(ended))+'</span><span class="t-abs">'+esc(rpFmtAbs(ended))+'</span></td>'
|
|
2598
2786
|
+ '<td>'+esc(rpFmtDuration(dur))+'</td>'
|
|
2599
2787
|
+ '<td><span class="rp-end-badge '+(reasonCls||'')+'">'+esc(reason||'—')+'</span></td>'
|
|
2600
|
-
+ '<td>'+esc(
|
|
2788
|
+
+ '<td>'+esc(turns)+'</td>'
|
|
2789
|
+
+ '<td class="'+(s.error_count?'bad':'ok')+'">'+esc(pipe)+'</td>'
|
|
2601
2790
|
+ '<td>'+esc(lat)+'</td>'
|
|
2602
2791
|
+ '<td><button class="rp-btn" data-act="view" data-sid="'+esc(sid)+'">View</button></td>'
|
|
2603
2792
|
+ '</tr>';
|
|
@@ -2608,29 +2797,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2608
2797
|
if(prev) prev.disabled = rpHistoryPage<=0;
|
|
2609
2798
|
if(next) next.disabled = rpHistoryPage>=totalPages-1;
|
|
2610
2799
|
}
|
|
2611
|
-
function
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
var html = ''
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
html += row('Room', s.room_name);
|
|
2618
|
-
html += row('Started', rpFmtAbs(s.started_at)
|
|
2619
|
-
html += row('
|
|
2620
|
-
html += row('
|
|
2621
|
-
html +=
|
|
2622
|
-
|
|
2623
|
-
html +=
|
|
2624
|
-
html +=
|
|
2625
|
-
html +=
|
|
2626
|
-
html
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
if(s.
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
showModal('Session '+(s.session_id||''), html);
|
|
2633
|
-
}
|
|
2800
|
+
function renderSessionDetail(s){
|
|
2801
|
+
function row(k,v){ return '<div class="kv"><div class="k">'+esc(k)+'</div><div class="v">'+esc(v==null?'—':v)+'</div></div>'; }
|
|
2802
|
+
var avatar=s.character_img||rpAvatarFor(s.character_id||s.character_name||s.robot_id), summary=s.summary||{};
|
|
2803
|
+
var html='<div class="rp-detail-hero">'+(avatar?'<img alt="'+esc(s.character_name||s.robot_id)+'" src="'+esc(avatar+QS)+'">':'')
|
|
2804
|
+
+'<div><div style="font-size:1rem;color:var(--gold-bright);font-weight:700">'+esc(s.character_name||s.robot_id||'Session')+'</div>'
|
|
2805
|
+
+'<div class="meta">'+esc(summary.turns||0)+' turns · '+esc(summary.pipeline_errors||0)+' pipeline errors</div></div></div>';
|
|
2806
|
+
html += row('Session ID', s.session_id)+row('Robot / source', s.robot_id)+row('Room', s.room_name);
|
|
2807
|
+
html += row('Started', rpFmtAbs(s.started_at)+' ('+rpFmtRelative(s.started_at)+')')+row('Ended',rpFmtAbs(s.ended_at)+' ('+rpFmtRelative(s.ended_at)+')');
|
|
2808
|
+
html += row('Duration',rpFmtDuration(s.duration_seconds))+row('End reason',s.end_reason||'active')+row('Join latency',s.latency_ms!=null?s.latency_ms+'ms':'—');
|
|
2809
|
+
html += row('Operator',s.initiated_by||'robot motion')+row('Character',s.character_name||s.character_id||'—')+row('Voice stack',s.voice_stack_name||s.voice_stack_id||'—');
|
|
2810
|
+
html += '<div class="rp-section-h" style="margin-top:1rem">Transcript <span class="meta">'+esc(summary.user_turns||0)+' user / '+esc(summary.assistant_turns||0)+' assistant</span></div>';
|
|
2811
|
+
var turns=(s.transcript||[]).filter(function(t){return t.is_final;});
|
|
2812
|
+
html += '<div class="rp-call-transcript" style="height:auto;max-height:320px">'+(turns.length?turns.map(function(t){return '<div class="rp-turn '+esc(t.role)+'"><div class="who">'+esc(t.speaker||t.role)+' · '+esc(clock(t.timestamp))+'</div><div>'+esc(t.text)+'</div></div>';}).join(''):'<div class="rp-tbl-empty">No transcript was recorded for this session.</div>')+'</div>';
|
|
2813
|
+
html += '<div class="rp-section-h" style="margin-top:1rem">Pipeline timeline <span class="meta">'+esc((s.pipeline_events||[]).length)+' events</span></div>';
|
|
2814
|
+
html += '<div class="rp-pipeline">'+((s.pipeline_events||[]).length?s.pipeline_events.map(function(e){return '<div class="rp-pipeline-row '+esc(e.status)+'"><span>'+esc(clock(e.timestamp))+'</span><b>'+esc(e.stage)+'</b><span>'+esc(e.status)+'</span><span>'+esc(e.message||'')+'</span></div>';}).join(''):'<div class="rp-tbl-empty">No pipeline events were recorded.</div>')+'</div>';
|
|
2815
|
+
return html;
|
|
2816
|
+
}
|
|
2817
|
+
function openSessionDetailModal(s){
|
|
2818
|
+
if(!s)return; showModal('Session '+(s.session_id||''),'<div class="rp-tbl-empty">Loading transcript and pipeline timeline...</div>');
|
|
2819
|
+
getJson('/robopark/api/sessions/'+encodeURIComponent(s.session_id)+'/detail').then(function(detail){$('rp-modal-body').innerHTML=renderSessionDetail(detail);}).catch(function(e){$('rp-modal-body').innerHTML='<div class="rp-tbl-empty">Session detail unavailable: '+esc(e&&e.message||e)+'</div>';});
|
|
2820
|
+
}
|
|
2634
2821
|
(function(){
|
|
2635
2822
|
var s=$('rp-hist-search'); if(s) s.addEventListener('input', function(){ rpHistoryFilter.search = String(s.value||'').toLowerCase(); rpHistoryPage=0; renderHistory(); });
|
|
2636
2823
|
var r=$('rp-hist-robot'); if(r) r.addEventListener('change', function(){ rpHistoryFilter.robot = r.value||''; rpHistoryPage=0; renderHistory(); });
|
|
@@ -3175,7 +3362,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
3175
3362
|
for(var i=0;i<top3.length;i++){
|
|
3176
3363
|
var x = top3[i];
|
|
3177
3364
|
var name = x.name || x.id || 'robot';
|
|
3178
|
-
var avatar =
|
|
3365
|
+
var avatar = rpAvatarFor(x.character_name||x.character_id||name);
|
|
3179
3366
|
thtml += '<div class="rp-card" style="padding:0.55rem 0.7rem;">'
|
|
3180
3367
|
+ '<div class="rp-card-top">'
|
|
3181
3368
|
+ (avatar?'<div class="rp-card-img"><img alt="'+esc(name)+'" src="'+esc(avatar+QS)+'"></div>':'')
|
|
@@ -5446,7 +5633,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5446
5633
|
// ── on-demand operator preview: open the robot's cam without a scene-detection session ──
|
|
5447
5634
|
function updateGoLive(){ var b=$('pk-golive'); if(b){ b.textContent = previewOn?'■ Stop live':'▶ Go live'; b.classList.toggle('danger', previewOn); } }
|
|
5448
5635
|
function goLive(n){ if(previewOn) stopPreview(n); else startPreview(n); }
|
|
5449
|
-
function startPreview(n){
|
|
5636
|
+
function startPreview(n){
|
|
5637
|
+
previewOn=true; updateGoLive();
|
|
5638
|
+
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
5639
|
+
idleCam('connecting');
|
|
5640
|
+
tryVisionOverlay(n, true);
|
|
5641
|
+
return;
|
|
5450
5642
|
previewOn=true; updateGoLive();
|
|
5451
5643
|
var note=$('pk-golivenote'); if(note) note.textContent='requesting preview…';
|
|
5452
5644
|
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; } // drop any session cam first
|
|
@@ -5463,7 +5655,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5463
5655
|
})
|
|
5464
5656
|
.catch(function(e){ idleCam('unreachable'); if(note) note.textContent='preview request failed'; if(window.console) console.error('RoboPark preview request failed',e); previewOn=false; updateGoLive(); });
|
|
5465
5657
|
}
|
|
5466
|
-
function stopPreview(n){
|
|
5658
|
+
function stopPreview(n){
|
|
5659
|
+
previewOn=false; updateGoLive();
|
|
5660
|
+
if(previewTimer){ clearInterval(previewTimer); previewTimer=null; }
|
|
5661
|
+
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
5662
|
+
stopVision();
|
|
5663
|
+
var directNote=$('pk-golivenote'); if(directNote) directNote.textContent='';
|
|
5664
|
+
idleCam('idle');
|
|
5665
|
+
return;
|
|
5467
5666
|
previewOn=false; updateGoLive();
|
|
5468
5667
|
if(previewTimer){ clearInterval(previewTimer); previewTimer=null; }
|
|
5469
5668
|
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
@@ -5478,6 +5677,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5478
5677
|
// server is opt-in because running it beside preview_agent would open the
|
|
5479
5678
|
// same Windows camera twice and make both feeds unreliable.
|
|
5480
5679
|
function tryVisionOverlay(n, force){
|
|
5680
|
+
var directImg=$('pk-vision-img'); if(!directImg) return;
|
|
5681
|
+
// Names stay stable when a robot is re-enrolled and receives a new mesh
|
|
5682
|
+
// node id. The hub resolves the name to the currently connected peer.
|
|
5683
|
+
var directRef=n.displayName||n.name||n.nodeId||n.id;
|
|
5684
|
+
if(!directRef){ visionOn=false; directImg.src=''; directImg.style.display='none'; return; }
|
|
5685
|
+
var directCamNote=$('pk-camnote'), directLiveNote=$('pk-golivenote'), directRes=$('pk-camres');
|
|
5686
|
+
visionOn=true;
|
|
5687
|
+
directImg.onerror=function(){ visionOn=false; directImg.style.display='none'; directImg.src=''; if(directCamNote) directCamNote.textContent='RoboVision feed unavailable on robot'; if(directLiveNote) directLiveNote.textContent='camera unavailable'; if(directRes) directRes.textContent='OFFLINE'; };
|
|
5688
|
+
directImg.onload=function(){ directImg.style.display='block'; if(directCamNote) directCamNote.textContent='live camera via RoboVision mesh relay'; if(directLiveNote) directLiveNote.textContent='live'; if(directRes) directRes.textContent='LIVE'; };
|
|
5689
|
+
directImg.src='/fed/media/robots/'+encodeURIComponent(directRef)+'/video_feed'+QS+(QS?'&':'?')+'_='+Date.now();
|
|
5690
|
+
return;
|
|
5481
5691
|
var img=$('pk-vision-img'); if(!img) return;
|
|
5482
5692
|
if(!force && String(window.localStorage.getItem('robopark.visionOverlay')||'').toLowerCase()!=='true'){
|
|
5483
5693
|
visionOn=false; img.src=''; img.style.display='none'; return;
|
|
@@ -6084,7 +6294,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6084
6294
|
function setFill(id,pct){ var e=$(id); if(!e||pct==null) return; e.style.width=Math.max(2,Math.min(100,pct))+'%'; e.style.background=pct>=75?'var(--bad)':pct>=45?'var(--warn)':'var(--gold-bright)'; }
|
|
6085
6295
|
|
|
6086
6296
|
// ── cam: target a LiveKit track; fall back to a styled preview ──
|
|
6087
|
-
function startCam(n){
|
|
6297
|
+
function startCam(n){
|
|
6298
|
+
idleCam('connecting');
|
|
6299
|
+
tryVisionOverlay(n, true);
|
|
6300
|
+
return;
|
|
6088
6301
|
var note=$('pk-camnote');
|
|
6089
6302
|
camAbort = ('AbortController' in window)?new AbortController():null;
|
|
6090
6303
|
idleCam('connecting');
|
|
@@ -86,6 +86,7 @@ export class Federation {
|
|
|
86
86
|
logger,
|
|
87
87
|
getManifest: () => buildManifest(identity, this.describeWithBackends()),
|
|
88
88
|
getNodes: () => this.nodeStatuses(),
|
|
89
|
+
getPeers: () => this.peers(),
|
|
89
90
|
getStatus: () => this.statusSnapshot(),
|
|
90
91
|
runCommand: (input) => this.deps.kernel.command(input),
|
|
91
92
|
commandGate: options.commandGate,
|
|
@@ -37,6 +37,18 @@ export class PeerMesh {
|
|
|
37
37
|
manifest,
|
|
38
38
|
version: manifest.version,
|
|
39
39
|
};
|
|
40
|
+
const endpoint = url.replace(/\/+$/, '').toLowerCase();
|
|
41
|
+
const name = manifest.displayName.trim().toLowerCase();
|
|
42
|
+
for (const existing of this.peers.values()) {
|
|
43
|
+
if (existing.nodeId === peer.nodeId)
|
|
44
|
+
continue;
|
|
45
|
+
const sameEndpoint = existing.url.replace(/\/+$/, '').toLowerCase() === endpoint;
|
|
46
|
+
const sameNamedDevice = existing.role === manifest.role && existing.displayName.trim().toLowerCase() === name;
|
|
47
|
+
if (!sameEndpoint && !sameNamedDevice)
|
|
48
|
+
continue;
|
|
49
|
+
this.opts.logger.warn(`[federation] replacing stale peer ${existing.displayName} (${existing.nodeId}) with ${peer.nodeId}`);
|
|
50
|
+
this.remove(existing.nodeId);
|
|
51
|
+
}
|
|
40
52
|
this.peers.set(peer.nodeId, peer);
|
|
41
53
|
this.openStream(peer);
|
|
42
54
|
// Immediate heartbeat so the peer learns us (nodeId + port) right away —
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Logger } from '../types.js';
|
|
2
|
-
import type { FederationEnvelope, NodeManifest, StreamFrame } from './types.js';
|
|
2
|
+
import type { FederationEnvelope, NodeManifest, PeerInfo, StreamFrame } from './types.js';
|
|
3
3
|
export type RpcHandler = (env: FederationEnvelope, remote: string) => Promise<FederationEnvelope | void>;
|
|
4
4
|
export interface MeshServerOptions {
|
|
5
5
|
host?: string;
|
|
@@ -9,6 +9,8 @@ export interface MeshServerOptions {
|
|
|
9
9
|
getManifest: () => NodeManifest;
|
|
10
10
|
/** Optional: full mesh view (self + peers) for the GET /fed/nodes probe. */
|
|
11
11
|
getNodes?: () => unknown;
|
|
12
|
+
/** Connected peers and their reachable mesh URLs. */
|
|
13
|
+
getPeers?: () => PeerInfo[];
|
|
12
14
|
/** Optional: consolidated fleet snapshot (self + nodes + hardware + roles +
|
|
13
15
|
* runs + recent activity) for the dashboard and `robopark fleet` (GET /fed/status). */
|
|
14
16
|
getStatus?: () => unknown;
|
|
@@ -50,6 +52,9 @@ export declare class MeshServer {
|
|
|
50
52
|
private seq;
|
|
51
53
|
constructor(opts: MeshServerOptions);
|
|
52
54
|
get streamCount(): number;
|
|
55
|
+
private streamHttp;
|
|
56
|
+
private streamLocalRoboVision;
|
|
57
|
+
private streamPeerRoboVision;
|
|
53
58
|
start(): Promise<void>;
|
|
54
59
|
/** Proxy a dashboard request to the RoboPark scheduler (product data). */
|
|
55
60
|
private proxyToScheduler;
|