infinicode 2.8.32 โ†’ 2.8.35

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.
@@ -198,15 +198,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
198
198
  .rp-head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:0.6rem;padding:1rem 1.1rem 0.2rem;}
199
199
  .rp-totals{display:flex;gap:0.5rem;flex-wrap:wrap;}
200
200
  .rp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:0.75rem;padding:0.8rem 1.1rem;}
201
- .rp-form{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.55rem;align-items:end;background:var(--panel-2);padding:0.7rem;border-radius:11px;border:1px solid var(--glass-border);margin:0.8rem 1.1rem;}
202
- .rp-form h3{grid-column:1/-1;font-family:var(--mono);font-size:0.7rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--gold-bright);margin-bottom:0.2rem;}
203
- .rp-field{display:flex;flex-direction:column;gap:0.25rem;}
204
- .rp-field label{font-size:0.68rem;color:var(--muted);}
205
- .rp-field input,.rp-field select{font-family:var(--mono);font-size:0.72rem;background:var(--panel-solid);border:1px solid var(--glass-border);color:var(--ink);border-radius:7px;padding:0.38rem 0.5rem;}
206
- .rp-card{border-radius:13px;padding:0.75rem 0.85rem;border-left:3px solid var(--gold-bright);
207
- background:var(--panel);-webkit-backdrop-filter:blur(14px) saturate(1.25);backdrop-filter:blur(14px) saturate(1.25);
208
- border:1px solid var(--glass-border);box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi);}
209
- .rp-card h4{font-family:var(--mono);font-size:0.86rem;margin-bottom:0.5rem;word-break:break-word;}
201
+ .rp-form{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.7rem;align-items:end;background:var(--panel-2);padding:1rem 1.05rem;border-radius:14px;border:1px solid var(--glass-border);margin:1rem 1.1rem;box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi);}
202
+ .rp-form h3{grid-column:1/-1;font-family:var(--mono);font-size:0.72rem;letter-spacing:0.09em;text-transform:uppercase;color:var(--gold-bright);margin-bottom:0.35rem;padding-bottom:0.5rem;border-bottom:1px solid var(--glass-border);}
203
+ .rp-field{display:flex;flex-direction:column;gap:0.3rem;}
204
+ .rp-field label{font-size:0.68rem;color:var(--muted);letter-spacing:0.01em;}
205
+ .rp-field input,.rp-field select,.rp-field textarea{font-family:var(--mono);font-size:0.74rem;background:var(--panel-solid);border:1px solid var(--glass-border);color:var(--ink);border-radius:8px;padding:0.48rem 0.6rem;transition:border-color .15s,box-shadow .15s;}
206
+ .rp-field input:focus,.rp-field select:focus,.rp-field textarea:focus{outline:none;border-color:var(--gold-bright);box-shadow:0 0 0 3px color-mix(in srgb,var(--gold-bright) 18%,transparent);}
207
+ .rp-quickrow{grid-column:1/-1;display:flex;gap:0.6rem;flex-wrap:wrap;align-items:flex-end;padding-bottom:0.2rem;}
208
+ .rp-quickrow .rp-field{flex:1;min-width:180px;}
209
+ .rp-autonote{grid-column:1/-1;font-family:var(--mono);font-size:0.66rem;color:var(--muted);display:flex;align-items:center;gap:0.4rem;min-height:1.2em;}
210
+ .rp-autonote.ok{color:var(--ok);} .rp-autonote.bad{color:var(--bad);} .rp-autonote.busy{color:var(--info);}
211
+ .rp-advanced{grid-column:1/-1;margin-top:0.3rem;border-top:1px dashed var(--glass-border);padding-top:0.7rem;}
212
+ .rp-advanced summary{cursor:pointer;font-family:var(--mono);font-size:0.66rem;letter-spacing:0.05em;text-transform:uppercase;color:var(--muted);list-style:none;display:flex;align-items:center;gap:0.4rem;user-select:none;}
213
+ .rp-advanced summary::-webkit-details-marker{display:none;}
214
+ .rp-advanced summary::before{content:"โ–ธ";display:inline-block;transition:transform .15s;color:var(--gold);}
215
+ .rp-advanced[open] summary::before{transform:rotate(90deg);}
216
+ .rp-advanced summary:hover{color:var(--gold-bright);}
217
+ .rp-advanced-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.6rem;margin-top:0.7rem;}
218
+ .rp-btn.primary{background:linear-gradient(160deg,color-mix(in srgb,var(--gold-bright) 30%,var(--panel-2)),var(--panel-2));border-color:var(--gold-bright);color:var(--gold-bright);font-weight:600;}
219
+ .rp-btn.primary:hover{box-shadow:0 0 0 3px color-mix(in srgb,var(--gold-bright) 16%,transparent);}
220
+ .rp-card{border-radius:13px;padding:0.75rem 0.85rem;border-left:3px solid var(--gold-bright);
221
+ background:var(--panel);-webkit-backdrop-filter:blur(14px) saturate(1.25);backdrop-filter:blur(14px) saturate(1.25);
222
+ border:1px solid var(--glass-border);box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi);}
223
+ .rp-card-top{display:flex;align-items:flex-start;gap:0.55rem;}
224
+ .rp-card-img{width:42px;height:42px;border-radius:11px;overflow:hidden;flex:none;border:1px solid var(--glass-border);background:var(--panel-2);}
225
+ .rp-card-img img{width:100%;height:100%;object-fit:cover;display:block;}
226
+ .rp-card h4{font-family:var(--mono);font-size:0.86rem;margin-bottom:0.5rem;word-break:break-word;}
210
227
  .rp-metrics{display:grid;grid-template-columns:1fr 1fr;gap:0.4rem 0.6rem;}
211
228
  .rp-m{display:flex;flex-direction:column;}
212
229
  .rp-m .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:1.1rem;color:var(--ink);}
@@ -264,8 +281,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
264
281
  transform:translateX(105%);transition:transform .32s cubic-bezier(.2,.8,.2,1);display:flex;flex-direction:column;}
265
282
  .pk-drawer.open{transform:translateX(0);}
266
283
  .pk-dh{display:flex;align-items:center;gap:0.6rem;padding:0.9rem 1rem;border-bottom:1px solid var(--glass-border);}
267
- .pk-av{width:40px;height:40px;border-radius:11px;background:radial-gradient(circle at 30% 25%,rgba(255,214,112,0.28),rgba(0,0,0,0.25));
268
- border:1px solid var(--border-strong);display:grid;place-items:center;font-size:1.2rem;}
284
+ .pk-av{width:40px;height:40px;border-radius:11px;background:radial-gradient(circle at 30% 25%,rgba(255,214,112,0.28),rgba(0,0,0,0.25));
285
+ border:1px solid var(--border-strong);display:grid;place-items:center;font-size:1.2rem;overflow:hidden;}
286
+ .pk-av.hasimg{background:var(--panel-2);}
287
+ .pk-av img{width:100%;height:100%;object-fit:cover;display:block;}
269
288
  .pk-dh .nm{font-size:1.02rem;font-weight:700;font-family:var(--mono);}
270
289
  .pk-dh .sub{font-family:var(--mono);font-size:0.64rem;color:var(--muted);}
271
290
  .pk-dh .x{margin-left:auto;background:var(--panel-2);border:1px solid var(--glass-border);color:var(--muted);border-radius:9px;width:30px;height:30px;cursor:pointer;font-size:1rem;}
@@ -379,26 +398,47 @@ button.act:disabled{opacity:0.5;cursor:default;}
379
398
  <!-- Add LiveKit server -->
380
399
  <div class="rp-form" id="rp-addserver">
381
400
  <h3>Add LiveKit server</h3>
382
- <div class="rp-field"><label>ID</label><input id="srv-id" placeholder="livekit-1-server"></div>
383
- <div class="rp-field"><label>Name</label><input id="srv-name" placeholder="LiveKit Server 1"></div>
384
- <div class="rp-field"><label>URL (ws:// or wss://)</label><input id="srv-url" placeholder="ws://livekit-1.tail2086c6.ts.net:7880"></div>
385
- <div class="rp-field"><label>API key</label><input id="srv-key" placeholder="devkey"></div>
386
- <div class="rp-field"><label>API secret</label><input type="password" id="srv-secret" placeholder="secret"></div>
387
- <div class="rp-field"><label>Max sessions</label><input id="srv-max" type="number" value="8"></div>
388
- <button class="rp-btn" id="srv-add" style="height:fit-content">Add server</button>
401
+ <div class="rp-quickrow">
402
+ <div class="rp-field"><label>Name</label><input id="srv-name" placeholder="LiveKit Server 1"></div>
403
+ <button class="rp-btn primary" id="srv-detect" style="height:fit-content" title="Probes this hub's own host + common local ports for a running LiveKit instance">๐Ÿ” Auto-detect &amp; fill</button>
404
+ <button class="rp-btn" id="srv-add" style="height:fit-content">Add server</button>
405
+ </div>
406
+ <div class="rp-autonote" id="srv-autonote"></div>
407
+ <details class="rp-advanced">
408
+ <summary>Advanced โ€” ID, URL, credentials, capacity</summary>
409
+ <div class="rp-advanced-grid">
410
+ <div class="rp-field"><label>ID</label><input id="srv-id" placeholder="auto-filled from name"></div>
411
+ <div class="rp-field"><label>URL (ws:// or wss://)</label><input id="srv-url" placeholder="ws://host:7880 โ€” auto-detect fills this"></div>
412
+ <div class="rp-field"><label>API key</label><input id="srv-key" placeholder="devkey" value="devkey"></div>
413
+ <div class="rp-field"><label>API secret</label><input type="password" id="srv-secret" placeholder="secretsecretsecretsecretsecretsecret"></div>
414
+ <div class="rp-field"><label>Max sessions</label><input id="srv-max" type="number" value="8"></div>
415
+ </div>
416
+ </details>
389
417
  </div>
390
418
 
391
419
  <!-- Add robot / device -->
392
420
  <div class="rp-form" id="rp-addrobot">
393
421
  <h3>Add robot (mint enrollment token)</h3>
394
- <div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
395
- <div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">โ€” auto โ€”</option></select></div>
396
- <div class="rp-field"><label>Tailscale IP (optional)</label><input id="rb-tsip" placeholder="100.x.x.x"></div>
397
- <div class="rp-field"><label>LAN IP (optional)</label><input id="rb-lanip" placeholder="192.168.x.x"></div>
398
- <div class="rp-field"><label>Motor server URL (optional)</label><input id="rb-motor" placeholder="http://...:5000"></div>
399
- <div class="rp-field"><label>LiveKit URL (optional)</label><input id="rb-lk" placeholder="ws://...:7880"></div>
400
- <div class="rp-field"><label>Voice stack</label><select id="rb-voice"><option value="">โ€” default โ€”</option></select></div>
401
- <button class="rp-btn" id="rb-add" style="height:fit-content">Add robot</button>
422
+ <div class="rp-quickrow">
423
+ <div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
424
+ <div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">โ€” auto (matched from name) โ€”</option></select></div>
425
+ <button class="rp-btn primary" id="rb-add" style="height:fit-content">Add robot</button>
426
+ </div>
427
+ <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>
428
+ <div class="rp-quickrow" id="rb-clicmd-row" style="display:none;">
429
+ <div class="rp-field" style="flex:1;"><label>run this ON THE ROBOT (no need to click Add โ€” this mints its own token)</label><input id="rb-clicmd" readonly style="cursor:text;"></div>
430
+ <button class="rp-btn" id="rb-clicmd-copy" style="height:fit-content">๐Ÿ“‹ Copy</button>
431
+ </div>
432
+ <details class="rp-advanced">
433
+ <summary>Advanced โ€” pre-set network/voice config before the robot connects</summary>
434
+ <div class="rp-advanced-grid">
435
+ <div class="rp-field"><label>Tailscale IP</label><input id="rb-tsip" placeholder="auto-detected once connected"></div>
436
+ <div class="rp-field"><label>LAN IP</label><input id="rb-lanip" placeholder="auto-detected once connected"></div>
437
+ <div class="rp-field"><label>Motor server URL</label><input id="rb-motor" placeholder="http://...:5000"></div>
438
+ <div class="rp-field"><label>LiveKit URL</label><input id="rb-lk" placeholder="โ€” uses the registered server above โ€”"></div>
439
+ <div class="rp-field"><label>Voice stack override</label><select id="rb-voice"><option value="">โ€” from character preset โ€”</option></select></div>
440
+ </div>
441
+ </details>
402
442
  </div>
403
443
 
404
444
  <div class="rp-grid" id="rp-grid"></div>
@@ -937,17 +977,74 @@ button.act:disabled{opacity:0.5;cursor:default;}
937
977
  }
938
978
  }
939
979
 
980
+ function slugify(s){ return (s||'').toLowerCase().replace(/[^a-z0-9_-]+/g,'-').replace(/^-|-$/g,'').slice(0,40); }
981
+
982
+ // โ”€โ”€ LiveKit auto-detect: probe this hub's own host + localhost on the
983
+ // standard LiveKit port. A no-cors fetch that *resolves* (even opaquely)
984
+ // means something answered HTTP there; one that *throws* means nothing did.
985
+ // Best-effort only โ€” the Advanced section stays editable so the user can
986
+ // correct a false positive/negative before submitting. โ”€โ”€
987
+ function detectLocalLiveKit(){
988
+ var note=$('srv-autonote'); note.className='rp-autonote busy'; note.textContent='probing for a local LiveKit instanceโ€ฆ';
989
+ var hosts=[]; var h=location.hostname||'localhost';
990
+ hosts.push(h); if(h!=='localhost') hosts.push('localhost');
991
+ var tried=0, found=null;
992
+ function tryNext(i){
993
+ if(i>=hosts.length){
994
+ if(found){
995
+ $('srv-url').value='ws://'+found+':7880';
996
+ if(!$('srv-key').value) $('srv-key').value='devkey';
997
+ note.className='rp-autonote ok'; note.textContent='โœ“ found a LiveKit instance at '+found+':7880 โ€” filled URL + default dev key (verify the secret in Advanced before adding).';
998
+ } else {
999
+ note.className='rp-autonote bad'; note.textContent='โœ— no local LiveKit found on '+hosts.join(', ')+':7880 โ€” fill the URL manually in Advanced.';
1000
+ }
1001
+ return;
1002
+ }
1003
+ var host=hosts[i];
1004
+ fetch('http://'+host+':7880/', {mode:'no-cors', signal: AbortSignal.timeout ? AbortSignal.timeout(2500) : undefined})
1005
+ .then(function(){ found=host; tryNext(hosts.length); })
1006
+ .catch(function(){ tryNext(i+1); });
1007
+ }
1008
+ tryNext(0);
1009
+ }
1010
+
940
1011
  function addServer(){
941
1012
  var body={
942
- id:$('srv-id').value.trim(), name:$('srv-name').value.trim(), url:$('srv-url').value.trim(),
943
- api_key:$('srv-key').value.trim(), api_secret:$('srv-secret').value,
1013
+ id:$('srv-id').value.trim()||slugify($('srv-name').value)+'-server', name:$('srv-name').value.trim(), url:$('srv-url').value.trim(),
1014
+ api_key:$('srv-key').value.trim()||'devkey', api_secret:$('srv-secret').value,
944
1015
  max_sessions:parseInt($('srv-max').value,10)||8
945
1016
  };
946
- if(!body.id||!body.name||!body.url){ showOut('Server ID, name, and URL are required'); return; }
1017
+ if(!body.name||!body.url){ showOut('Server name and URL are required (use Auto-detect, or fill URL under Advanced)'); return; }
947
1018
  body.webhook_url=body.url;
948
1019
  rpAction('POST','/robopark/api/servers',body).then(function(j){ showOut('server added: '+esc((j&&j.id)||body.id)); clearServerForm(); }).catch(function(e){ showOut('add server: '+e.message); });
949
1020
  }
950
- function clearServerForm(){ $('srv-id').value=''; $('srv-name').value=''; $('srv-url').value=''; $('srv-key').value=''; $('srv-secret').value=''; $('srv-max').value='8'; }
1021
+ function clearServerForm(){ $('srv-id').value=''; $('srv-name').value=''; $('srv-url').value=''; $('srv-key').value=''; $('srv-secret').value=''; $('srv-max').value='8'; $('srv-autonote').textContent=''; $('srv-autonote').className='rp-autonote'; }
1022
+
1023
+ // โ”€โ”€ robust CLI one-liner generation, live as you type โ€” no API call
1024
+ // needed to see it; 'robopark add-robot' mints its own token when run โ”€โ”€
1025
+ function buildAddRobotCommand(name, characterId){
1026
+ if(!name) return '';
1027
+ var hubUrl=location.origin;
1028
+ var schedulerUrl=(lastStatus && lastStatus.control && lastStatus.control.schedulerUrl) || '';
1029
+ var argv=['robopark','add-robot',name];
1030
+ if(characterId) argv.push('--character', characterId);
1031
+ if(hubUrl) argv.push('--hub-url', hubUrl);
1032
+ if(TOKEN) argv.push('--token', TOKEN);
1033
+ if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
1034
+ argv.push('--start');
1035
+ return argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' ');
1036
+ }
1037
+ function refreshRobotCliCommand(){
1038
+ var name=$('rb-name').value.trim(); var cid=$('rb-char').value||'';
1039
+ var row=$('rb-clicmd-row'), inp=$('rb-clicmd');
1040
+ if(!name){ row.style.display='none'; return; }
1041
+ inp.value=buildAddRobotCommand(name, cid);
1042
+ row.style.display='flex';
1043
+ }
1044
+ var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
1045
+ var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
1046
+ 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'); });
1047
+
951
1048
  function addRobot(){
952
1049
  var body={
953
1050
  name:$('rb-name').value.trim(), character_id:$('rb-char').value||null,
@@ -958,7 +1055,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
958
1055
  if(!body.name){ showOut('Robot name is required'); return; }
959
1056
  rpAction('POST','/robopark/api/devices',body).then(function(j){ showOut('robot added: '+esc((j&&j.name)||body.name)+'\\ntoken: '+esc((j&&j.enrollment_token)||'(none)')); clearRobotForm(); pollRobopark(); }).catch(function(e){ showOut('add robot: '+e.message); });
960
1057
  }
961
- function clearRobotForm(){ $('rb-name').value=''; $('rb-char').value=''; $('rb-tsip').value=''; $('rb-lanip').value=''; $('rb-motor').value=''; $('rb-lk').value=''; $('rb-voice').value=''; }
1058
+ function clearRobotForm(){ $('rb-name').value=''; $('rb-char').value=''; $('rb-tsip').value=''; $('rb-lanip').value=''; $('rb-motor').value=''; $('rb-lk').value=''; $('rb-voice').value=''; refreshRobotCliCommand(); }
1059
+ var srvDetectBtn=$('srv-detect'); if(srvDetectBtn) srvDetectBtn.addEventListener('click', detectLocalLiveKit);
962
1060
  function triggerSession(id){
963
1061
  rpAction('POST','/robopark/api/robots/'+encodeURIComponent(id)+'/trigger-session',{confidence:0.9}).then(function(){ showOut('triggered motion for '+id); pollRobopark(); Park.refreshDrawer(); }).catch(function(e){ showOut('trigger-session ('+id+'): '+e.message); });
964
1062
  }
@@ -1112,13 +1210,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
1112
1210
  var html='';
1113
1211
  for (var i=0;i<robots.length;i++){
1114
1212
  var x = robots[i];
1115
- var name = robotName(x);
1116
- var lat = x.avg_latency_ms!=null?Math.round(x.avg_latency_ms)+'ms':'โ€”';
1213
+ var name = robotName(x);
1214
+ var avatar = avatarSrcForNode({label:(x.character_name||x.character_id||x.character_preset_id||name)});
1215
+ var lat = x.avg_latency_ms!=null?Math.round(x.avg_latency_ms)+'ms':'โ€”';
1117
1216
  var dcls = x.drop_rate!=null?(x.drop_rate>=0.2?'bad':(x.drop_rate>=0.05?'warn':'ok')):'';
1118
1217
  var drop = x.drop_rate!=null?Math.round(x.drop_rate*100)+'%':'โ€”';
1119
1218
  var active = x.active_session||x.in_session||x.session_active;
1120
1219
  var idAttr = esc(name);
1121
- html += '<div class="rp-card" data-name="'+esc(name)+'"><h4>'+esc(name)+(active?' <span class="chip" style="color:var(--ok)">live</span>':'')+'</h4><div class="rp-metrics">'
1220
+ html += '<div class="rp-card" data-name="'+esc(name)+'"><div class="rp-card-top">'+(avatar?'<div class="rp-card-img"><img alt="'+esc(name)+'" src="'+esc(avatar+QS)+'"></div>':'')+'<div style="min-width:0;flex:1;"><h4>'+esc(name)+(active?' <span class="chip" style="color:var(--ok)">live</span>':'')+'</h4></div></div><div class="rp-metrics">'
1122
1221
  + '<div class="rp-m"><span class="v">'+(x.trigger_count!=null?x.trigger_count:0)+'</span><span class="l">triggers</span></div>'
1123
1222
  + '<div class="rp-m"><span class="v">'+(x.sessions_total!=null?x.sessions_total:0)+'</span><span class="l">sessions</span></div>'
1124
1223
  + '<div class="rp-m"><span class="v">'+lat+'</span><span class="l">avg latency</span></div>'
@@ -1184,10 +1283,59 @@ button.act:disabled{opacity:0.5;cursor:default;}
1184
1283
  if(/robot|pi-client|scheduler/.test(caps) && r!=='peer') return 'robot';
1185
1284
  return 'peer';
1186
1285
  }
1187
- function loadColor(l){ return l>=75?getVar('--bad'):l>=45?getVar('--warn'):getVar('--ok'); }
1188
- function getVar(v){ try{ return getComputedStyle(document.documentElement).getPropertyValue(v).trim()||'#ccc'; }catch(e){ return '#ccc'; } }
1189
-
1190
- // Bind mesh nodes onto the fixed park pads; unmatched pads render as
1286
+ function loadColor(l){ return l>=75?getVar('--bad'):l>=45?getVar('--warn'):getVar('--ok'); }
1287
+ function getVar(v){ try{ return getComputedStyle(document.documentElement).getPropertyValue(v).trim()||'#ccc'; }catch(e){ return '#ccc'; } }
1288
+ function normalizeAvatarPath(path){
1289
+ var p=(path==null?'':String(path)).trim();
1290
+ if(!p) return '';
1291
+ if(/^https?:\/\//i.test(p) || p.indexOf('data:')===0 || p.indexOf('/')===0) return p;
1292
+ return '/' + p.replace(/^\.?\//, '');
1293
+ }
1294
+ function slotAliases(slot){
1295
+ var out=[], seen={};
1296
+ function add(v){
1297
+ var s=(v==null?'':String(v)).trim().toLowerCase();
1298
+ if(!s || seen[s]) return;
1299
+ seen[s]=1;
1300
+ out.push(s);
1301
+ }
1302
+ add(slot && slot.label);
1303
+ add(slot && slot.id);
1304
+ add(slot && slot.name);
1305
+ add(slot && slot.sub);
1306
+ (slot && slot.keys ? slot.keys : []).forEach(add);
1307
+ return out;
1308
+ }
1309
+ function slotForNode(n){
1310
+ var text=((n && (n.label || n.name || n.id || ''))+'').toLowerCase();
1311
+ if(!text) return null;
1312
+ var best=null;
1313
+ for(var i=0;i<SLOTS.length;i++){
1314
+ var slot=SLOTS[i], aliases=slotAliases(slot);
1315
+ for(var j=0;j<aliases.length;j++){
1316
+ var key=aliases[j];
1317
+ if(!key) continue;
1318
+ if(text===key || text.indexOf(key)>-1 || key.indexOf(text)>-1) return slot;
1319
+ if(!best && key.length>2 && text.indexOf(key.replace(/[^a-z0-9]/g,''))>-1) best=slot;
1320
+ }
1321
+ }
1322
+ return best;
1323
+ }
1324
+ function avatarSrcForNode(n){
1325
+ var slot=slotForNode(n);
1326
+ return normalizeAvatarPath(slot && slot.img);
1327
+ }
1328
+ function avatarImageForNode(n){
1329
+ var slot=slotForNode(n);
1330
+ if(!slot) return null;
1331
+ var aliases=slotAliases(slot);
1332
+ var src=normalizeAvatarPath(slot.img);
1333
+ if(src) aliases.unshift(src);
1334
+ for(var i=0;i<aliases.length;i++){ var img=imgs[aliases[i]]; if(img) return img; }
1335
+ return null;
1336
+ }
1337
+
1338
+ // Bind mesh nodes onto the fixed park pads; unmatched pads render as
1191
1339
  // offline placeholders so the whole park is always visible.
1192
1340
  function layout(src){
1193
1341
  var list=(src||[]).slice(), used={}, out=[];
@@ -1395,12 +1543,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
1395
1543
  var p=iso(n.wx,n.wy), z=cam.z, bob=n.connected?Math.sin((performance.now()-t0)/500+n.wx)*2.3*z:0, bx=p.x, by=p.y-4*z-bob;
1396
1544
  if(n.connected) ring(bx,p.y+3*z,18*z,n.load/100,loadColor(n.load)); else padMark(bx,p.y+3*z,z);
1397
1545
  X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(bx,p.y+3*z,13*z,6*z,0,0,7); X.fill();
1398
- var col=n.connected?getVar('--gold-bright'):'rgba(150,143,120,0.5)';
1399
- X.globalAlpha=n.connected?1:0.7;
1400
- var img = n.connected && imgs[n.label];
1401
- if(img && img.complete && img.naturalWidth>0){
1402
- var iw=26*z, ih=26*z*img.naturalHeight/img.naturalWidth;
1403
- X.save();
1546
+ var col=n.connected?getVar('--gold-bright'):'rgba(150,143,120,0.5)';
1547
+ X.globalAlpha=n.connected?1:0.7;
1548
+ var img = avatarImageForNode(n);
1549
+ if(img && img.complete && img.naturalWidth>0){
1550
+ var iw=26*z, ih=26*z*img.naturalHeight/img.naturalWidth;
1551
+ X.save();
1404
1552
  X.beginPath(); X.roundRect(bx-13*z,by-38*z,iw,ih+12*z,6*z); X.clip();
1405
1553
  X.drawImage(img, bx-13*z, by-44*z, iw, ih+18*z);
1406
1554
  X.restore();
@@ -1479,9 +1627,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
1479
1627
  };
1480
1628
  document.head.appendChild(s);
1481
1629
  }
1482
- function loadImages(){
1483
- SLOTS.forEach(function(s){ if(!s.img) return; var i=new Image(); i.src=s.img+QS; i.onload=function(){ imgs[s.label]=i; }; i.onerror=function(){ imgs[s.label]=null; }; });
1484
- }
1630
+ function loadImages(){
1631
+ imgs={};
1632
+ var seen={};
1633
+ SLOTS.forEach(function(s){
1634
+ var src=normalizeAvatarPath(s && s.img);
1635
+ if(!src || seen[src]) return;
1636
+ seen[src]=1;
1637
+ var i=new Image();
1638
+ i.decoding='async';
1639
+ var aliases=slotAliases(s);
1640
+ aliases.push(src);
1641
+ i.onload=function(){ aliases.forEach(function(k){ imgs[k]=i; }); };
1642
+ i.onerror=function(){ aliases.forEach(function(k){ if(!(k in imgs)) imgs[k]=null; }); };
1643
+ i.src=src+QS;
1644
+ });
1645
+ }
1485
1646
  function updateSlots(newSlots){
1486
1647
  SLOTS = newSlots.slice();
1487
1648
  loadImages();
@@ -1500,11 +1661,20 @@ button.act:disabled{opacity:0.5;cursor:default;}
1500
1661
 
1501
1662
  // โ”€โ”€ robot drawer โ”€โ”€
1502
1663
  function sect(title, inner){ return '<div class="pk-sect"><h4>'+title+'</h4>'+inner+'</div>'; }
1503
- function openDrawer(id){
1504
- var n=byId[id]; if(!n) return; drawerId=id;
1505
- $('pk-drawer').classList.add('open');
1506
- $('pk-av').textContent = n.kind==='hub'?'๐Ÿ–ฅ':(n.kind==='robot'?'๐Ÿค–':'๐Ÿงฉ');
1507
- $('pk-name').textContent=n.label||n.name;
1664
+ function openDrawer(id){
1665
+ var n=byId[id]; if(!n) return; drawerId=id;
1666
+ $('pk-drawer').classList.add('open');
1667
+ var av=$('pk-av'), avSrc=avatarSrcForNode(n), avImg=avatarImageForNode(n);
1668
+ if(av){
1669
+ if(avSrc){
1670
+ av.classList.add('hasimg');
1671
+ av.innerHTML = '<img alt="'+esc(n.label||n.name||'robot')+'" src="'+esc(avImg && avImg.src ? avImg.src : avSrc+QS)+'">';
1672
+ } else {
1673
+ av.classList.remove('hasimg');
1674
+ av.textContent = n.kind==='hub'?'๐Ÿ–ฅ':(n.kind==='robot'?'๐Ÿค–':'๐Ÿงฉ');
1675
+ }
1676
+ }
1677
+ $('pk-name').textContent=n.label||n.name;
1508
1678
  $('pk-sub').textContent = n.connected ? (n.name+' ยท '+(n.platform||'?')+' ยท v'+n.version+(n.site?(' ยท '+n.site):'')) : 'pad reserved ยท not joined';
1509
1679
  var st=$('pk-state'); st.textContent=n.connected?'live':'offline'; st.className='pk-pill '+(n.connected?'live':'idle');
1510
1680
  buildDrawer(n);
@@ -1656,19 +1826,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
1656
1826
  + '<div class="pk-kv"><div class="v">'+esc(n.label)+'</div><div class="l">pad</div></div>'
1657
1827
  + '<div class="pk-kv"><div class="v" style="color:var(--muted)">idle</div><div class="l">status</div></div>'
1658
1828
  + '</div>'
1659
- + '<div class="rp-form" style="padding:0.55rem;grid-template-columns:1fr;gap:0.5rem;margin:0;border-color:var(--border-strong);">'
1829
+ + '<div class="rp-form" style="padding:0.7rem;grid-template-columns:1fr;gap:0.55rem;margin:0;border-color:var(--border-strong);">'
1660
1830
  + '<div class="rp-field"><label>Robot name</label><input id="pk-setup-name" value="'+esc(defaultName)+'"></div>'
1661
1831
  + '<div class="rp-field"><label>Character preset</label><select id="pk-setup-char"><option value="">โ€” auto โ€”</option>'+presetOptions+'</select></div>'
1662
- + '<div class="rp-field"><label>Tailscale IP</label><input id="pk-setup-tsip" placeholder="100.x.x.x"></div>'
1663
- + '<div class="rp-field"><label>LAN IP</label><input id="pk-setup-lanip" placeholder="192.168.x.x"></div>'
1832
+ + '<div class="rp-quickrow" style="padding:0;"><button class="rp-btn primary" id="pk-setup-btn" data-preset="'+esc(n.id)+'" style="width:fit-content;">Mint enrollment token &amp; add robot</button></div>'
1833
+ + '<div class="rp-field"><label>or run this ON THE ROBOT (mints its own token)</label><input id="pk-setup-clicmd" readonly></div>'
1834
+ + '<div class="rp-quickrow" style="padding:0;"><button class="rp-btn" id="pk-setup-clicmd-copy" style="width:fit-content;">๐Ÿ“‹ Copy command</button></div>'
1835
+ + '<details class="rp-advanced"><summary>Advanced โ€” pre-set network/voice config</summary>'
1836
+ + '<div class="rp-advanced-grid">'
1837
+ + '<div class="rp-field"><label>Tailscale IP</label><input id="pk-setup-tsip" placeholder="auto-detected once connected"></div>'
1838
+ + '<div class="rp-field"><label>LAN IP</label><input id="pk-setup-lanip" placeholder="auto-detected once connected"></div>'
1664
1839
  + '<div class="rp-field"><label>Motor server URL</label><input id="pk-setup-motor" placeholder="http://...:5000"></div>'
1665
- + '<div class="rp-field"><label>LiveKit URL</label><input id="pk-setup-lk" placeholder="ws://...:7880"></div>'
1666
- + '<div class="rp-field"><label>Voice stack</label><select id="pk-setup-voice"><option value="">โ€” default โ€”</option>'+voiceOptions+'</select></div>'
1667
- + '<button class="rp-btn" id="pk-setup-btn" data-preset="'+esc(n.id)+'" style="width:fit-content;">Mint enrollment token & add robot</button>'
1840
+ + '<div class="rp-field"><label>LiveKit URL</label><input id="pk-setup-lk" placeholder="โ€” uses the registered server โ€”"></div>'
1841
+ + '<div class="rp-field"><label>Voice stack override</label><select id="pk-setup-voice"><option value="">โ€” from character preset โ€”</option>'+voiceOptions+'</select></div>'
1842
+ + '</div></details>'
1668
1843
  + '</div>'
1669
1844
  + '<div class="pk-sect" id="pk-setup-result" style="display:none;"><h4>Enrollment token (copy once)</h4><input id="pk-setup-token" class="rp-char" readonly style="margin-bottom:0.4rem;"><button class="rp-btn" id="pk-setup-copy">Copy</button></div>');
1670
1845
  var setupBtn=$('pk-setup-btn'); if(setupBtn) setupBtn.addEventListener('click', function(){ submitPadSetup(n); });
1671
1846
  var copyBtn=$('pk-setup-copy'); if(copyBtn) copyBtn.addEventListener('click', function(){ var t=$('pk-setup-token'); if(t){ t.select(); try{document.execCommand('copy');}catch(e){} showOut('token copied'); } });
1847
+ function refreshPadCliCommand(){
1848
+ var nm=$('pk-setup-name').value.trim()||defaultName; var cid=$('pk-setup-char').value||n.id;
1849
+ var el=$('pk-setup-clicmd'); if(el) el.value=buildAddRobotCommand(nm, cid);
1850
+ }
1851
+ var padNameEl=$('pk-setup-name'); if(padNameEl) padNameEl.addEventListener('input', refreshPadCliCommand);
1852
+ var padCharEl=$('pk-setup-char'); if(padCharEl) padCharEl.addEventListener('change', refreshPadCliCommand);
1853
+ var padCliCopy=$('pk-setup-clicmd-copy'); if(padCliCopy) padCliCopy.addEventListener('click', function(){ var i=$('pk-setup-clicmd'); i.select(); try{document.execCommand('copy');}catch(e){} showOut('command copied โ€” run it on the robot itself'); });
1854
+ refreshPadCliCommand();
1672
1855
  return;
1673
1856
  }
1674
1857
  if(n.kind==='robot'){
@@ -103,6 +103,7 @@ export interface FleetStatus {
103
103
  control: {
104
104
  readonly: boolean;
105
105
  schedulerLinked: boolean;
106
+ schedulerUrl?: string;
106
107
  };
107
108
  ts: number;
108
109
  }
@@ -717,6 +717,7 @@ export class Federation {
717
717
  control: {
718
718
  readonly: Boolean(this.deps.options.schedulerReadOnly),
719
719
  schedulerLinked: Boolean(this.deps.options.schedulerUrl),
720
+ schedulerUrl: this.deps.options.schedulerUrl,
720
721
  },
721
722
  ts: Date.now(),
722
723
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.32",
3
+ "version": "2.8.35",
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",