infinicode 2.8.66 → 2.8.68

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.
@@ -529,8 +529,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
529
529
  .rp-modal-body .kv{display:grid;grid-template-columns:160px 1fr;gap:0.4rem 0.8rem;margin-bottom:0.4rem;}
530
530
  .rp-modal-body .kv .k{color:var(--muted);font-size:0.66rem;text-transform:uppercase;letter-spacing:0.05em;}
531
531
  .rp-modal-body .kv .v{color:var(--ink);word-break:break-word;white-space:pre-wrap;}
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);}
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}}
534
560
 
535
561
  /* ── TTS voice picker (used inside the generic modal) ── */
536
562
  .rp-voice-picker{display:flex;flex-direction:column;gap:0.6rem;min-height:320px;}
@@ -658,7 +684,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
658
684
  <div class="rp-subtabs" id="rp-subtabs" role="tablist">
659
685
  <button class="rp-subtab active" data-sub="overview" id="rpsbtn-overview" role="tab" aria-selected="true">Overview</button>
660
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>
661
- <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>
662
689
  <button class="rp-subtab" data-sub="history" id="rpsbtn-history" role="tab" aria-selected="false">History</button>
663
690
  <button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
664
691
  <button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
@@ -700,7 +727,30 @@ button.act:disabled{opacity:0.5;cursor:default;}
700
727
  </div>
701
728
 
702
729
  <!-- ═══ History ═══ -->
703
- <div class="rp-sub" id="rp-sub-history">
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">
704
754
  <div class="rp-filterbar">
705
755
  <div class="rp-field"><label>Search</label><input id="rp-hist-search" placeholder="session id, robot…"></div>
706
756
  <div class="rp-field"><label>Robot</label><select id="rp-hist-robot"><option value="">all robots</option></select></div>
@@ -724,7 +774,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
724
774
  <div class="rp-tbl-wrap" id="rp-hist-wrap">
725
775
  <table class="rp-history-table" id="rp-hist-table">
726
776
  <thead><tr>
727
- <th>Session</th><th>Robot</th><th>Started</th><th>Ended</th><th>Duration</th><th>End reason</th><th>Triggers</th><th>Latency</th><th>Actions</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>
728
778
  </tr></thead>
729
779
  <tbody id="rp-hist-tbody"></tbody>
730
780
  </table>
@@ -1142,7 +1192,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
1142
1192
  + '<div class="bar-track"><div class="bar-fill '+loadClass(v)+'" style="width:'+v+'%"></div></div>'
1143
1193
  + '<span class="mv '+(cls||'')+'">'+esc(valTxt)+'</span></div>';
1144
1194
  }
1145
- function renderFleet(nodes){
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){
1146
1223
  var el = $('fleet');
1147
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; }
1148
1225
  // connected first, then robots/satellites above hubs so the fleet reads robot-first
@@ -1163,13 +1240,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
1163
1240
  var disk = (hw&&hw.disk&&hw.disk.mounts&&hw.disk.mounts[0]) ? hw.disk.mounts[0].percent : null;
1164
1241
  var nt = netRxTx(hw);
1165
1242
  var upt = hw&&hw.system ? hw.system.uptimeSec : null;
1166
- var caps = (n.capabilities||[]).filter(function(c){ return c.indexOf('backend:')!==0; }).slice(0,5);
1167
- 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;
1168
1249
  var site = n.site?(' · '+esc(n.site)):'';
1169
1250
 
1170
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)+'">'
1171
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>'
1172
- + '<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>'):'');
1173
1256
 
1174
1257
  if(up){
1175
1258
  html += mrow('CPU', cpu, cpu+'%'+(cores?(' /'+cores+'c'):''), loadClass(cpu))
@@ -1610,12 +1693,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
1610
1693
  });
1611
1694
 
1612
1695
  var characterPresets=[];
1613
- function loadCharacterPresets(){
1614
- 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(){});
1615
- }
1616
- function loadVoiceStacks(){
1617
- 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(){});
1618
- }
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
+ }
1619
1702
 
1620
1703
  // ── TTS voice catalog (per provider) ──
1621
1704
  // Holds the dropdown options for whichever TTS provider is currently
@@ -2080,7 +2163,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2080
2163
  for (var i=0;i<robots.length;i++){
2081
2164
  var x = robots[i];
2082
2165
  var name = robotName(x);
2083
- var avatar = avatarSrcForNode({label:(x.character_name||x.character_id||x.character_preset_id||name)});
2166
+ var avatar = rpAvatarFor(x.character_name||x.character_id||x.character_preset_id||name);
2084
2167
  var lat = x.avg_latency_ms!=null?Math.round(x.avg_latency_ms)+'ms':'—';
2085
2168
  var dcls = x.drop_rate!=null?(x.drop_rate>=0.2?'bad':(x.drop_rate>=0.05?'warn':'ok')):'';
2086
2169
  var drop = x.drop_rate!=null?Math.round(x.drop_rate*100)+'%':'—';
@@ -2192,14 +2275,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
2192
2275
  s.classList.toggle('active', s.id==='rp-sub-'+name);
2193
2276
  }
2194
2277
  // stop every sub-tab interval, then start only the active one
2195
- var names = ['overview','incidents','live','history','audit','fleet','setup'];
2278
+ var names = ['overview','incidents','live','voice','history','audit','fleet','setup'];
2196
2279
  for(var k=0;k<names.length;k++){
2197
2280
  var n=names[k];
2198
2281
  if(rpIntervals[n]){ clearInterval(rpIntervals[n]); rpIntervals[n]=null; }
2199
2282
  }
2200
2283
  if(name==='overview'){ pollOverview(); rpIntervals.overview = setInterval(pollOverview, 4000); }
2201
2284
  if(name==='incidents'){ pollIncidents(); rpIntervals.incidents = setInterval(pollIncidents, 4000); }
2202
- if(name==='live'){ pollLiveSessions(); startLiveAutoRefresh(); }
2285
+ if(name==='live'){ pollLiveSessions(); startLiveAutoRefresh(); }
2286
+ if(name==='voice'){ refreshVoiceCallOptions(); }
2203
2287
  if(name==='history'){ rpHistoryPage=0; pollHistory(); }
2204
2288
  if(name==='audit'){ pollAuditLog(); rpIntervals.audit = setInterval(pollAuditLog, 6000); }
2205
2289
  if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
@@ -2218,7 +2302,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2218
2302
  var h=String(location.hash||'').replace('#','');
2219
2303
  var m = h.match(/^robopark\\/(.+)$/);
2220
2304
  var initial = (m && m[1]) ? m[1] : 'overview';
2221
- 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';
2222
2306
  setRpSubTab(initial);
2223
2307
  }
2224
2308
 
@@ -2513,7 +2597,111 @@ button.act:disabled{opacity:0.5;cursor:default;}
2513
2597
  })();
2514
2598
 
2515
2599
  // ── History sub-tab ──
2516
- var rpHistoryFilter = { robot: '', endReason: '', range: '7d', search: '' };
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: '' };
2517
2705
  var rpHistoryPage = 0;
2518
2706
  var RP_HISTORY_PAGE_SIZE = 25;
2519
2707
  function pollHistory(){
@@ -2587,7 +2775,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
2587
2775
  var dur = s.duration_seconds!=null ? s.duration_seconds : (started&&ended?Math.floor((ended-started)/1000):null);
2588
2776
  var reason = s.end_reason || '';
2589
2777
  var reasonCls = rpEndReasonClass(reason);
2590
- var trig = s.trigger_count!=null ? s.trigger_count : '—';
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');
2591
2780
  var lat = s.avg_latency_ms!=null ? Math.round(s.avg_latency_ms)+'ms' : '—';
2592
2781
  html += '<tr data-sid="'+esc(sid)+'">'
2593
2782
  + '<td><span class="sid" title="'+esc(sid)+'">'+esc(sid)+'</span></td>'
@@ -2596,7 +2785,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
2596
2785
  + '<td><span class="t-rel">'+esc(rpFmtRelative(ended))+'</span><span class="t-abs">'+esc(rpFmtAbs(ended))+'</span></td>'
2597
2786
  + '<td>'+esc(rpFmtDuration(dur))+'</td>'
2598
2787
  + '<td><span class="rp-end-badge '+(reasonCls||'')+'">'+esc(reason||'—')+'</span></td>'
2599
- + '<td>'+esc(trig)+'</td>'
2788
+ + '<td>'+esc(turns)+'</td>'
2789
+ + '<td class="'+(s.error_count?'bad':'ok')+'">'+esc(pipe)+'</td>'
2600
2790
  + '<td>'+esc(lat)+'</td>'
2601
2791
  + '<td><button class="rp-btn" data-act="view" data-sid="'+esc(sid)+'">View</button></td>'
2602
2792
  + '</tr>';
@@ -2607,29 +2797,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
2607
2797
  if(prev) prev.disabled = rpHistoryPage<=0;
2608
2798
  if(next) next.disabled = rpHistoryPage>=totalPages-1;
2609
2799
  }
2610
- function openSessionDetailModal(s){
2611
- if(!s) return;
2612
- function row(k,v){ return '<div class="kv"><div class="k">'+esc(k)+'</div><div class="v">'+esc(v==null?'—':v)+'</div></div>'; }
2613
- var html = '';
2614
- html += row('Session ID', s.session_id);
2615
- html += row('Robot', s.robot_id);
2616
- html += row('Room', s.room_name);
2617
- html += row('Started', rpFmtAbs(s.started_at) + ' ('+rpFmtRelative(s.started_at)+')');
2618
- html += row('Ended', rpFmtAbs(s.ended_at) + ' ('+rpFmtRelative(s.ended_at)+')');
2619
- html += row('Duration', rpFmtDuration(s.duration_seconds));
2620
- html += row('End reason', s.end_reason || '—');
2621
- html += row('Trigger count', s.trigger_count!=null?s.trigger_count:'—');
2622
- html += row('Avg latency', s.avg_latency_ms!=null?Math.round(s.avg_latency_ms)+'ms':'');
2623
- html += row('Drop rate', s.drop_rate!=null?Math.round(s.drop_rate*100)+'%':'—');
2624
- html += row('Operator', s.operator_id || s.ended_by || '');
2625
- html += row('Character', s.character_id || s.character_name || '—');
2626
- html += row('Voice stack', s.voice_stack_id || '—');
2627
- if(s.notes) html += row('Notes', s.notes);
2628
- if(s.metadata){
2629
- html += '<div class="kv"><div class="k">Metadata</div><div class="v"><pre>'+esc(typeof s.metadata==='string'?s.metadata:JSON.stringify(s.metadata,null,2))+'</pre></div></div>';
2630
- }
2631
- showModal('Session '+(s.session_id||''), html);
2632
- }
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
+ }
2633
2821
  (function(){
2634
2822
  var s=$('rp-hist-search'); if(s) s.addEventListener('input', function(){ rpHistoryFilter.search = String(s.value||'').toLowerCase(); rpHistoryPage=0; renderHistory(); });
2635
2823
  var r=$('rp-hist-robot'); if(r) r.addEventListener('change', function(){ rpHistoryFilter.robot = r.value||''; rpHistoryPage=0; renderHistory(); });
@@ -3174,7 +3362,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
3174
3362
  for(var i=0;i<top3.length;i++){
3175
3363
  var x = top3[i];
3176
3364
  var name = x.name || x.id || 'robot';
3177
- var avatar = avatarSrcForNode({label:(x.character_name||x.character_id||name)});
3365
+ var avatar = rpAvatarFor(x.character_name||x.character_id||name);
3178
3366
  thtml += '<div class="rp-card" style="padding:0.55rem 0.7rem;">'
3179
3367
  + '<div class="rp-card-top">'
3180
3368
  + (avatar?'<div class="rp-card-img"><img alt="'+esc(name)+'" src="'+esc(avatar+QS)+'"></div>':'')
@@ -5490,7 +5678,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
5490
5678
  // same Windows camera twice and make both feeds unreliable.
5491
5679
  function tryVisionOverlay(n, force){
5492
5680
  var directImg=$('pk-vision-img'); if(!directImg) return;
5493
- var directRef=n.nodeId||n.id||n.name;
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;
5494
5684
  if(!directRef){ visionOn=false; directImg.src=''; directImg.style.display='none'; return; }
5495
5685
  var directCamNote=$('pk-camnote'), directLiveNote=$('pk-golivenote'), directRes=$('pk-camres');
5496
5686
  visionOn=true;
@@ -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 —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.66",
3
+ "version": "2.8.68",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",