infinicode 2.8.108 → 2.8.113

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.
@@ -230,14 +230,42 @@ button.act:disabled{opacity:0.5;cursor:default;}
230
230
  .offline{background:var(--bad-bg);color:var(--bad);text-align:center;font-family:var(--mono);font-size:0.72rem;padding:0.4rem;display:none;
231
231
  border-bottom:1px solid var(--glass-border);}
232
232
 
233
- /* tabs + product panes */
234
- .tabs{display:flex;gap:0.2rem;padding:0 1.1rem;border-bottom:1px solid var(--glass-border);
235
- background:var(--panel);-webkit-backdrop-filter:blur(18px) saturate(1.3);backdrop-filter:blur(18px) saturate(1.3);
236
- position:sticky;top:53px;z-index:19;}
237
- .tab{font-family:var(--mono);font-size:0.74rem;padding:0.6rem 0.95rem;cursor:pointer;color:var(--muted);background:none;border:none;border-bottom:2px solid transparent;}
238
- .tab.active{color:var(--gold-bright);border-bottom-color:var(--gold-bright);text-shadow:0 0 14px color-mix(in srgb,var(--gold-bright) 55%,transparent);}
239
- .tab:focus-visible{outline:2px solid var(--gold-bright);outline-offset:-2px;}
240
- .pane{display:none;} .pane.active{display:block;}
233
+ /* global gradient-glass navigation */
234
+ :root{--global-nav-width:264px}
235
+ body{padding-left:var(--global-nav-width);transition:padding-left .24s cubic-bezier(.2,.8,.2,1)}
236
+ body.global-nav-collapsed{--global-nav-width:78px}
237
+ .global-sidebar{position:fixed;inset:0 auto 0 0;z-index:48;width:var(--global-nav-width);height:100vh;max-height:100vh;display:flex;flex-direction:column;align-self:auto;overflow:hidden;
238
+ background:radial-gradient(240px 260px at 22% 0,rgba(255,211,112,.18),transparent 72%),radial-gradient(260px 340px at 100% 88%,rgba(126,79,8,.2),transparent 70%),linear-gradient(165deg,rgba(24,20,13,.93),rgba(7,7,6,.96));
239
+ -webkit-backdrop-filter:blur(28px) saturate(1.35);backdrop-filter:blur(28px) saturate(1.35);border-right:1px solid rgba(255,222,143,.18);box-shadow:22px 0 70px rgba(0,0,0,.38),inset -1px 0 rgba(255,240,200,.035);transition:width .24s cubic-bezier(.2,.8,.2,1),transform .24s cubic-bezier(.2,.8,.2,1)}
240
+ .global-sidebar:before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.22;background-image:linear-gradient(rgba(255,224,151,.04) 1px,transparent 1px),linear-gradient(90deg,rgba(255,224,151,.03) 1px,transparent 1px);background-size:28px 28px;mask-image:linear-gradient(to bottom,black,transparent 88%)}
241
+ .global-side-head{position:relative;display:flex;align-items:center;gap:.72rem;min-height:76px;padding:.78rem;border-bottom:1px solid var(--glass-border)}
242
+ .global-side-mark{display:grid;place-items:center;flex:0 0 46px;height:46px;border-radius:15px;font-family:var(--display);font-size:1rem;font-weight:800;color:#171006;background:linear-gradient(145deg,#fff0b5,var(--gold-bright) 48%,#a66f15);box-shadow:0 10px 28px rgba(224,168,55,.25),inset 0 1px rgba(255,255,255,.7)}
243
+ .global-side-brand{min-width:0;overflow:hidden;white-space:nowrap}.global-side-brand b{display:block;font-family:var(--display);font-size:.92rem;color:var(--gold-pale)}.global-side-brand span{display:block;margin-top:.1rem;font-family:var(--mono);font-size:.52rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
244
+ .global-nav-collapse{position:absolute;right:-1px;bottom:-13px;display:grid;place-items:center;width:28px;height:28px;border-radius:9px 0 0 9px;border:1px solid var(--border-strong);border-right:0;background:#171209;color:var(--gold-bright);font-family:var(--mono);font-size:.58rem;cursor:pointer;z-index:2}
245
+ .global-nav{position:relative;flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:1rem .65rem 6.2rem;scrollbar-width:thin;scrollbar-color:rgba(232,190,92,.3) transparent}
246
+ .global-nav-group{margin:.2rem .45rem .48rem;font-family:var(--mono);font-size:.49rem;letter-spacing:.14em;text-transform:uppercase;color:#7f735e;white-space:nowrap;overflow:hidden}.global-nav-group:not(:first-child){margin-top:1.1rem}
247
+ .global-nav-item,.global-nav-sub{position:relative;width:100%;display:flex;align-items:center;gap:.7rem;min-height:46px;margin:.16rem 0;padding:.48rem .58rem;border:1px solid transparent;border-radius:13px;background:transparent;color:var(--muted);cursor:pointer;text-align:left;transition:background .15s,border-color .15s,color .15s,transform .15s;overflow:hidden}
248
+ .global-nav-sub{min-height:39px;padding:.38rem .58rem .38rem 1rem}
249
+ .global-nav-item:hover,.global-nav-sub:hover{color:var(--ink);background:rgba(255,236,186,.055);border-color:rgba(255,224,151,.09);transform:translateX(2px)}
250
+ .global-nav-item.active,.global-nav-sub.active{color:var(--gold-pale);border-color:rgba(246,215,123,.28);background:linear-gradient(100deg,rgba(232,190,92,.17),rgba(232,190,92,.035));box-shadow:inset 3px 0 0 var(--gold-bright),0 8px 28px rgba(0,0,0,.16)}
251
+ .global-nav-item.active:after,.global-nav-sub.active:after{content:"";position:absolute;right:10px;width:5px;height:5px;border-radius:50%;background:var(--gold-bright);box-shadow:0 0 10px var(--gold-bright)}
252
+ .global-sidebar:not([data-active-tab="robopark"]) .global-nav-sub.active{color:var(--muted);border-color:transparent;background:transparent;box-shadow:none}.global-sidebar:not([data-active-tab="robopark"]) .global-nav-sub.active:after{display:none}
253
+ .global-nav-icon{display:grid;place-items:center;flex:0 0 34px;height:34px;border-radius:10px;border:1px solid rgba(255,224,151,.12);background:linear-gradient(145deg,rgba(255,239,195,.09),rgba(0,0,0,.12));font-family:var(--mono);font-size:.56rem;font-weight:800;letter-spacing:.03em;color:var(--gold-bright);box-shadow:inset 0 1px rgba(255,255,255,.04)}
254
+ .global-nav-sub .global-nav-icon{flex-basis:28px;height:28px;border-radius:8px;font-size:.49rem}.global-nav-copy{min-width:0;display:block;white-space:nowrap}.global-nav-copy b{display:block;font-size:.69rem;font-weight:700;color:inherit}.global-nav-copy small{display:block;margin-top:.05rem;font-size:.52rem;color:#8f846f}
255
+ .global-side-foot{position:absolute;left:.65rem;right:.65rem;bottom:.7rem;display:flex;align-items:center;gap:.55rem;min-height:48px;padding:.58rem;border:1px solid rgba(255,224,151,.1);border-radius:13px;background:rgba(3,3,2,.48);box-shadow:inset 0 1px rgba(255,255,255,.025)}
256
+ .global-side-foot i{flex:0 0 8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 12px var(--ok)}.global-side-foot span{white-space:nowrap;overflow:hidden;font-family:var(--mono);font-size:.54rem;color:var(--muted)}
257
+ body.global-nav-collapsed .global-side-brand,body.global-nav-collapsed .global-nav-group,body.global-nav-collapsed .global-nav-copy,body.global-nav-collapsed .global-side-foot span{opacity:0;width:0;pointer-events:none}
258
+ body.global-nav-collapsed .global-side-head{padding-left:1rem}body.global-nav-collapsed .global-nav{padding-left:.65rem;padding-right:.65rem}body.global-nav-collapsed .global-nav-item,body.global-nav-collapsed .global-nav-sub{justify-content:center;padding:.42rem}body.global-nav-collapsed .global-nav-item.active:after,body.global-nav-collapsed .global-nav-sub.active:after{right:5px}body.global-nav-collapsed .global-side-foot{justify-content:center}
259
+ .global-nav-mobile{display:none;width:38px;height:38px;border-radius:11px;border:1px solid var(--glass-border);background:var(--panel-2);color:var(--gold-bright);font-family:var(--mono);font-size:.72rem;cursor:pointer}.global-nav-backdrop{display:none;position:fixed;inset:0;z-index:47;background:rgba(0,0,0,.62);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
260
+ footer.cmd{left:var(--global-nav-width);transition:left .24s cubic-bezier(.2,.8,.2,1)}
261
+ #out{max-width:min(560px,calc(100vw - var(--global-nav-width) - 2rem))}
262
+ /* The sidebar owns Control Center navigation; legacy buttons remain for routing and accessibility tests. */
263
+ .rp-subtabs{display:none!important}
264
+ .pane{display:none;} .pane.active{display:block;animation:globalPaneIn .22s ease-out}
265
+ @keyframes globalPaneIn{from{opacity:.35;transform:translateY(4px)}to{opacity:1;transform:none}}
266
+ @media(max-width:760px){
267
+ :root,body.global-nav-collapsed{--global-nav-width:0px}body{padding-left:0}.global-sidebar{width:min(300px,86vw);transform:translateX(-105%);box-shadow:30px 0 90px rgba(0,0,0,.62)}body.global-nav-open .global-sidebar{transform:translateX(0)}body.global-nav-open .global-nav-backdrop{display:block}.global-nav-collapse{display:none}.global-nav-mobile{display:grid;place-items:center}body.global-nav-collapsed .global-side-brand,body.global-nav-collapsed .global-nav-group,body.global-nav-collapsed .global-nav-copy,body.global-nav-collapsed .global-side-foot span{opacity:1;width:auto;pointer-events:auto}body.global-nav-collapsed .global-nav-item,body.global-nav-collapsed .global-nav-sub{justify-content:flex-start;padding:.48rem .58rem}body.global-nav-collapsed .global-nav-sub{padding-left:1rem}footer.cmd{left:0}#out{max-width:90vw}
268
+ }
241
269
  .rp-head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:0.6rem;padding:1rem 1.1rem 0.2rem;}
242
270
  .rp-totals{display:flex;gap:0.5rem;flex-wrap:wrap;}
243
271
  .rp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:0.75rem;padding:0.8rem 1.1rem;}
@@ -706,12 +734,18 @@ button.act:disabled{opacity:0.5;cursor:default;}
706
734
 
707
735
  /* command sheet */
708
736
  .rp-command-shell{display:grid;gap:1rem}.rp-command-env{display:grid;grid-template-columns:repeat(3,minmax(170px,1fr));gap:.65rem;padding:.85rem;border:1px solid var(--glass-border);border-radius:16px;background:var(--panel-2)}
737
+ .rp-minute{display:grid;gap:1rem;padding:1rem;border:1px solid rgba(232,190,92,.34);border-radius:20px;background:radial-gradient(circle at 90% 0,rgba(232,190,92,.13),transparent 38%),linear-gradient(145deg,rgba(29,24,15,.88),rgba(7,7,6,.94));box-shadow:0 22px 65px rgba(0,0,0,.34),inset 0 1px 0 rgba(255,239,190,.11)}
738
+ .rp-minute-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem}.rp-minute-head h4{font-family:var(--display);font-size:1.15rem;color:var(--gold-pale);margin:.15rem 0}.rp-minute-head p{font-size:.68rem;color:var(--muted);line-height:1.55;max-width:680px}.rp-minute-badge{font-family:var(--mono);font-size:.58rem;color:#0a0905;background:linear-gradient(135deg,var(--gold-bright),#fff0ad);border-radius:100px;padding:.35rem .65rem;white-space:nowrap;font-weight:800}
739
+ .rp-minute-fields{display:grid;grid-template-columns:repeat(4,minmax(150px,1fr));gap:.65rem}.rp-minute-field{position:relative;padding:.72rem;border:1px solid var(--glass-border);border-radius:14px;background:rgba(4,4,3,.42)}.rp-minute-field .step{display:block;font-family:var(--mono);font-size:.53rem;color:var(--gold-bright);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.35rem}.rp-minute-field label{font-size:.64rem;color:var(--text);display:block;margin-bottom:.35rem}.rp-minute-field small{display:block;color:var(--muted);font-size:.54rem;line-height:1.4;margin-top:.35rem}
740
+ .rp-minute-command{display:grid;grid-template-columns:1fr auto;gap:.7rem;align-items:stretch}.rp-minute-command .rp-command-code{min-height:112px}.rp-minute-copy{min-width:180px;font-size:.76rem!important;font-weight:800}.rp-minute-status{font-family:var(--mono);font-size:.61rem;color:var(--muted)}.rp-minute-status.ok{color:var(--ok)}.rp-minute-status.bad{color:var(--bad)}
741
+ .rp-minute-run{display:grid;grid-template-columns:repeat(3,1fr);gap:.55rem}.rp-minute-run div{display:flex;gap:.5rem;align-items:flex-start;padding:.62rem;border-radius:12px;background:rgba(255,255,255,.025);font-size:.61rem;color:var(--muted);line-height:1.45}.rp-minute-run b{display:grid;place-items:center;flex:0 0 22px;height:22px;border-radius:50%;background:rgba(232,190,92,.15);color:var(--gold-bright);font-family:var(--mono)}
742
+ .rp-command-advanced{border:1px solid var(--glass-border);border-radius:16px;background:rgba(7,7,6,.46);overflow:hidden}.rp-command-advanced>summary{cursor:pointer;padding:.85rem 1rem;color:var(--gold-pale);font-family:var(--mono);font-size:.69rem;list-style:none}.rp-command-advanced>summary:after{content:' +';float:right;color:var(--gold-bright)}.rp-command-advanced[open]>summary:after{content:' -'}.rp-command-advanced-body{display:grid;gap:.85rem;padding:0 1rem 1rem}
709
743
  .rp-command-toolbar{display:flex;gap:.55rem;align-items:center;flex-wrap:wrap}.rp-command-toolbar input{flex:1;min-width:220px}.rp-command-toolbar select{width:auto;min-width:170px}
710
744
  .rp-command-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:.75rem}.rp-command-card{display:flex;flex-direction:column;gap:.55rem;min-width:0;padding:.85rem;border:1px solid var(--glass-border);border-radius:16px;background:linear-gradient(145deg,rgba(31,26,18,.66),rgba(9,8,6,.76));box-shadow:inset 0 1px 0 var(--glass-hi),0 12px 32px rgba(0,0,0,.22)}
711
745
  .rp-command-head{display:flex;align-items:flex-start;justify-content:space-between;gap:.6rem}.rp-command-head b{font-family:var(--mono);font-size:.74rem;color:var(--gold-pale)}.rp-command-tag{font-family:var(--mono);font-size:.52rem;text-transform:uppercase;letter-spacing:.06em;color:var(--gold-bright);border:1px solid var(--border-strong);border-radius:100px;padding:.15rem .42rem;white-space:nowrap}.rp-command-card p{font-size:.64rem;line-height:1.5;color:var(--muted);min-height:2.9em}
712
746
  .rp-command-code{position:relative;min-height:86px;padding:.68rem .72rem;border:1px solid rgba(255,224,151,.1);border-radius:11px;background:#050504;overflow:auto}.rp-command-code code{display:block;font-family:var(--mono);font-size:.59rem;line-height:1.55;color:#f2dfab;white-space:pre-wrap;overflow-wrap:anywhere}.rp-command-actions{display:flex;gap:.4rem;align-items:center;margin-top:auto}.rp-command-actions .rp-btn:first-child{flex:1}.rp-command-empty{grid-column:1/-1;padding:2rem;text-align:center;color:var(--muted);border:1px dashed var(--glass-border);border-radius:16px}
713
747
  .rp-command-custom{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;padding:.9rem;border:1px solid var(--glass-border);border-radius:16px;background:var(--panel-2)}.rp-command-custom h4,.rp-command-custom .wide{grid-column:1/-1}.rp-command-custom h4{font-family:var(--mono);font-size:.72rem;color:var(--gold-bright)}.rp-command-custom textarea{min-height:92px;resize:vertical}.rp-command-note{font-size:.61rem;color:var(--muted)}
714
- @media(max-width:800px){.rp-command-env{grid-template-columns:1fr 1fr}.rp-command-grid{grid-template-columns:1fr}}@media(max-width:540px){.rp-command-env,.rp-command-custom{grid-template-columns:1fr}.rp-command-custom h4,.rp-command-custom .wide{grid-column:auto}}
748
+ @media(max-width:900px){.rp-minute-fields{grid-template-columns:1fr 1fr}.rp-minute-run{grid-template-columns:1fr}.rp-command-env{grid-template-columns:1fr 1fr}.rp-command-grid{grid-template-columns:1fr}}@media(max-width:540px){.rp-minute-head{display:grid}.rp-minute-fields,.rp-command-env,.rp-command-custom{grid-template-columns:1fr}.rp-minute-command{grid-template-columns:1fr}.rp-minute-copy{min-height:52px}.rp-command-custom h4,.rp-command-custom .wide{grid-column:auto}}
715
749
 
716
750
  .rp-fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:0.7rem;padding:0;}
717
751
  .rp-fleet-card{position:relative;}
@@ -732,9 +766,42 @@ button.act:disabled{opacity:0.5;cursor:default;}
732
766
  .rp-input-grow{flex:1;min-width:140px;}
733
767
  </style>
734
768
  </head>
735
- <body>
769
+ <body>
770
+ <aside class="global-sidebar" id="global-sidebar" aria-label="Global navigation">
771
+ <div class="global-side-head">
772
+ <div class="global-side-mark">RP</div>
773
+ <div class="global-side-brand"><b>RoboPark</b><span>Control center</span></div>
774
+ <button class="global-nav-collapse" id="global-nav-toggle" type="button" aria-label="Collapse navigation" aria-expanded="true">&lt;&lt;</button>
775
+ </div>
776
+ <nav class="global-nav">
777
+ <div class="global-nav-group">Fleet operations</div>
778
+ <button class="tab global-nav-item active" data-tab="fleet" id="tabbtn-fleet" type="button" title="Fleet intelligence">
779
+ <span class="global-nav-icon">FL</span><span class="global-nav-copy"><b>Fleet</b><small>Readiness and workload</small></span>
780
+ </button>
781
+ <button class="tab global-nav-item" data-tab="park" id="tabbtn-park" type="button" title="Park map">
782
+ <span class="global-nav-icon">PK</span><span class="global-nav-copy"><b>Park</b><small>Live map and camera ops</small></span>
783
+ </button>
784
+ <div class="global-nav-group">Control center</div>
785
+ <button class="tab global-nav-item" data-tab="robopark" id="tabbtn-robopark" type="button" title="Sessions and telemetry">
786
+ <span class="global-nav-icon">CC</span><span class="global-nav-copy"><b>Control center</b><small>Production oversight</small></span>
787
+ </button>
788
+ <button class="global-nav-sub" data-global-sub="overview" type="button" title="Overview"><span class="global-nav-icon">OV</span><span class="global-nav-copy"><b>Overview</b><small>Pipeline health</small></span></button>
789
+ <button class="global-nav-sub" data-global-sub="incidents" type="button" title="Incidents"><span class="global-nav-icon">IN</span><span class="global-nav-copy"><b>Incidents</b><small>Failures and blockers</small></span></button>
790
+ <button class="global-nav-sub" data-global-sub="live" type="button" title="Live sessions"><span class="global-nav-icon">LS</span><span class="global-nav-copy"><b>Live sessions</b><small>Active conversations</small></span></button>
791
+ <button class="global-nav-sub" data-global-sub="voice" type="button" title="Voice calls"><span class="global-nav-icon">VC</span><span class="global-nav-copy"><b>Voice calls</b><small>Operator voice studio</small></span></button>
792
+ <button class="global-nav-sub" data-global-sub="history" type="button" title="History"><span class="global-nav-icon">HI</span><span class="global-nav-copy"><b>History</b><small>Sessions and transcripts</small></span></button>
793
+ <button class="global-nav-sub" data-global-sub="audit" type="button" title="Audit log"><span class="global-nav-icon">AU</span><span class="global-nav-copy"><b>Audit log</b><small>Operator activity</small></span></button>
794
+ <button class="global-nav-sub" data-global-sub="fleet" type="button" title="Fleet analytics"><span class="global-nav-icon">FA</span><span class="global-nav-copy"><b>Fleet analytics</b><small>Robot telemetry</small></span></button>
795
+ <button class="global-nav-sub" data-global-sub="config" type="button" title="Characters and voice"><span class="global-nav-icon">CH</span><span class="global-nav-copy"><b>Characters</b><small>Voice stacks and prompts</small></span></button>
796
+ <button class="global-nav-sub" data-global-sub="commands" type="button" title="Command sheet"><span class="global-nav-icon">CM</span><span class="global-nav-copy"><b>Command sheet</b><small>Setup and recovery</small></span></button>
797
+ <button class="global-nav-sub" data-global-sub="setup" type="button" title="Setup"><span class="global-nav-icon">ST</span><span class="global-nav-copy"><b>Setup</b><small>Robots and servers</small></span></button>
798
+ </nav>
799
+ <div class="global-side-foot"><i></i><span>Live fleet navigation</span></div>
800
+ </aside>
801
+ <div class="global-nav-backdrop" id="global-nav-backdrop"></div>
736
802
  <div class="offline" id="offline">connection lost — retrying</div>
737
- <header class="bar">
803
+ <header class="bar">
804
+ <button class="global-nav-mobile" id="global-nav-mobile" type="button" aria-label="Open navigation" aria-expanded="false">NAV</button>
738
805
  <div class="brand">
739
806
  <span class="livedot" id="livedot"></span>
740
807
  <div>
@@ -747,12 +814,6 @@ button.act:disabled{opacity:0.5;cursor:default;}
747
814
  <span class="rp-operator-badge set" id="rp-operator-badge" role="button" tabindex="0">set operator</span>
748
815
  </header>
749
816
 
750
- <div class="tabs">
751
- <button class="tab active" data-tab="fleet" id="tabbtn-fleet">Fleet</button>
752
- <button class="tab" data-tab="park" id="tabbtn-park">Park</button>
753
- <button class="tab" data-tab="robopark" id="tabbtn-robopark">Sessions &amp; Telemetry</button>
754
- </div>
755
-
756
817
  <div class="pane active" id="pane-fleet">
757
818
  <div class="wrap">
758
819
  <section>
@@ -825,7 +886,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
825
886
  <div class="section"><h3>Live insights</h3><div class="pk-camera-insight" id="pk-camera-ops-insights"></div></div>
826
887
  <div class="section"><h3>Conversation pipeline</h3><div class="pk-camera-pipeline" id="pk-camera-ops-pipeline"></div></div>
827
888
  <div class="section"><h3>Hardware and media</h3><div class="pk-camera-insight" id="pk-camera-ops-hardware"></div></div>
828
- <div class="section"><h3>Motor operations</h3><div class="rp-field"><label>Saved sequence</label><select id="pk-camera-ops-motor-sequence"></select></div><div class="pk-ctl"><button class="rp-btn" id="pk-camera-ops-motor-discover">Discover relays</button><button class="rp-btn primary" id="pk-camera-ops-motor-run">Run sequence</button><button class="rp-btn danger" id="pk-camera-ops-motor-test">Test registered GPIO</button></div><div class="pk-motor-status" id="pk-camera-ops-motor-status">Loading robot motor profile...</div></div>
889
+ <div class="section"><h3>Motor operations</h3><div class="rp-field"><label>Saved sequence</label><select id="pk-camera-ops-motor-sequence"></select></div><div class="pk-ctl"><button class="rp-btn" id="pk-camera-ops-motor-discover">Discover relays</button><button class="rp-btn primary" id="pk-camera-ops-motor-run">Run sequence</button><button class="rp-btn danger" id="pk-camera-ops-motor-stop">STOP ALL</button><button class="rp-btn danger" id="pk-camera-ops-motor-test">Test registered GPIO</button></div><div class="pk-motor-status" id="pk-camera-ops-motor-status">Loading robot motor profile...</div></div>
829
890
  <div class="section"><h3>Operator note</h3><div class="pk-note">Video is relayed directly from the robot's RoboVision MJPEG service through the authenticated mesh. Voice remains on LiveKit, avoiding camera ownership conflicts.</div></div>
830
891
  </aside>
831
892
  </div>
@@ -1172,31 +1233,49 @@ button.act:disabled{opacity:0.5;cursor:default;}
1172
1233
  <div class="rp-sub" id="rp-sub-commands">
1173
1234
  <div class="rp-command-shell">
1174
1235
  <div class="rp-config-hero">
1175
- <div><h3>RoboPark Command Sheet</h3><p>Production one-liners for the hub, LAN and Tailscale robots, health checks, recovery, Docker, LiveKit, and package operations. Edit the environment once; every generated command below updates immediately.</p></div>
1176
- <div class="rp-config-hero-actions"><button class="rp-btn" id="rp-command-reset">Reset environment</button><button class="rp-btn primary" id="rp-command-copy-all">Copy visible commands</button></div>
1236
+ <div><h3>RoboPark Easy Setup</h3><p>Set up or repair a complete robot in about one minute. Choose three things, copy one command, and paste it on the robot.</p></div>
1237
+ <div class="rp-config-hero-actions"><button class="rp-btn" id="rp-command-reset">Reset setup</button></div>
1177
1238
  </div>
1178
- <div class="rp-command-env">
1179
- <div class="rp-field"><label>Hub LAN IP</label><input id="rp-command-lan" value="192.168.0.9" spellcheck="false"></div>
1180
- <div class="rp-field"><label>Hub Tailscale IP / DNS</label><input id="rp-command-tail" value="100.84.147.24" spellcheck="false"></div>
1181
- <div class="rp-field"><label>Site</label><input id="rp-command-site" value="Tel Aviv"></div>
1182
- <div class="rp-field"><label>Robot name</label><input id="rp-command-robot" value="bmw" spellcheck="false"></div>
1183
- <div class="rp-field"><label>Enrollment token</label><input id="rp-command-enrollment" placeholder="paste robot enrollment token" spellcheck="false"></div>
1184
- <div class="rp-field"><label>Mesh token</label><input id="rp-command-token" spellcheck="false"></div>
1239
+ <div class="rp-minute">
1240
+ <div class="rp-minute-head"><div><div class="eyebrow">Guided robot setup</div><h4>One command. Complete production runtime.</h4><p>The command installs RoboPark, removes the old supervised process safely, and starts camera, microphone, speaker, motion detection, motors, RoboVoice, and remote management.</p></div><span class="rp-minute-badge">ABOUT 1 MINUTE</span></div>
1241
+ <div class="rp-minute-fields">
1242
+ <div class="rp-minute-field"><span class="step">Step 1</span><label>Robot name</label><input id="rp-command-robot" value="bmw" spellcheck="false" placeholder="bmw"><small>Use the permanent physical robot name.</small></div>
1243
+ <div class="rp-minute-field"><span class="step">Step 2</span><label>Character</label><select id="rp-command-character"><option value="vixen">VIXEN</option></select><small>Only real character presets appear here.</small></div>
1244
+ <div class="rp-minute-field"><span class="step">Step 3</span><label>Connection</label><select id="rp-command-network"><option value="lan">Same LAN as livekit-1</option><option value="tailscale">Tailscale remote</option></select><small>Choose LAN when the robot is onsite.</small></div>
1245
+ <div class="rp-minute-field"><span class="step">Optional</span><label>Site</label><input id="rp-command-site" value="Tel Aviv"><small>Shown in fleet and session history.</small></div>
1246
+ </div>
1247
+ <div class="rp-minute-command"><pre class="rp-command-code"><code id="rp-minute-command"></code></pre><button class="rp-btn primary rp-minute-copy" id="rp-minute-copy">Copy setup command</button></div>
1248
+ <div class="rp-minute-status" id="rp-minute-status">Checking setup...</div>
1249
+ <div class="rp-minute-run"><div><b>1</b><span>Open Terminal on the Raspberry Pi.</span></div><div><b>2</b><span>Paste the copied command and press Enter.</span></div><div><b>3</b><span>Wait for <strong>enabled and started systemd unit</strong>. The robot then appears in Fleet.</span></div></div>
1185
1250
  </div>
1186
- <div class="rp-command-toolbar">
1187
- <input type="search" id="rp-command-search" placeholder="Search setup, logs, camera, Docker, LiveKit, npm...">
1188
- <select id="rp-command-category"><option value="">All categories</option><option>Provision</option><option>Health</option><option>Recovery</option><option>Media</option><option>Packages</option><option>Custom</option></select>
1189
- <span class="rp-command-note" id="rp-command-count"></span>
1190
- </div>
1191
- <div class="rp-command-grid" id="rp-command-grid"></div>
1192
- <form class="rp-command-custom" id="rp-command-custom-form">
1193
- <h4>Add a persistent command</h4>
1194
- <div class="rp-field"><label>Title</label><input id="rp-command-custom-title" required placeholder="Restart camera service"></div>
1195
- <div class="rp-field"><label>Category</label><input id="rp-command-custom-category" value="Custom" placeholder="Custom"></div>
1196
- <div class="rp-field wide"><label>Simple explanation</label><input id="rp-command-custom-description" placeholder="When to use this and what success looks like"></div>
1197
- <div class="rp-field wide"><label>Command</label><textarea id="rp-command-custom-command" required spellcheck="false" placeholder="Paste a safe command or one-liner"></textarea></div>
1198
- <div class="wide"><button class="rp-btn primary" type="submit">Save command to this browser</button> <span class="rp-command-note">Custom commands persist in localStorage on this operator browser.</span></div>
1199
- </form>
1251
+ <details class="rp-command-advanced">
1252
+ <summary>Advanced tools, addresses, diagnostics, and recovery</summary>
1253
+ <div class="rp-command-advanced-body">
1254
+ <div class="rp-command-env">
1255
+ <div class="rp-field"><label>livekit-1 LAN IP</label><input id="rp-command-lan" value="192.168.0.9" spellcheck="false"></div>
1256
+ <div class="rp-field"><label>livekit-1 Tailscale IP / DNS</label><input id="rp-command-tail" value="100.84.147.24" spellcheck="false"></div>
1257
+ <div class="rp-field"><label>Canonical device ID</label><input id="rp-command-device" placeholder="optional: dev_0422e545" spellcheck="false"></div>
1258
+ <div class="rp-field"><label>Enrollment token</label><input id="rp-command-enrollment" placeholder="optional" spellcheck="false"></div>
1259
+ <div class="rp-field"><label>Control token</label><input id="rp-command-token" type="password" spellcheck="false"></div>
1260
+ <div class="rp-field"><label>RoboVoice folder on livekit-1</label><input id="rp-command-robovoice" value="C:\ROBOVOICE-main" spellcheck="false"></div>
1261
+ <div class="rp-field"><label>Release</label><input id="rp-command-release" value="2.8.113" spellcheck="false"></div>
1262
+ </div>
1263
+ <div class="rp-command-toolbar">
1264
+ <input type="search" id="rp-command-search" placeholder="Search logs, camera, Docker, motors, npm...">
1265
+ <select id="rp-command-category"><option value="">All categories</option><option>Provision</option><option>Production</option><option>Voice</option><option>Health</option><option>Recovery</option><option>Media</option><option>Motors</option><option>Packages</option><option>Custom</option></select>
1266
+ <button class="rp-btn" id="rp-command-copy-all">Copy visible commands</button><span class="rp-command-note" id="rp-command-count"></span>
1267
+ </div>
1268
+ <div class="rp-command-grid" id="rp-command-grid"></div>
1269
+ <form class="rp-command-custom" id="rp-command-custom-form">
1270
+ <h4>Add a persistent command</h4>
1271
+ <div class="rp-field"><label>Title</label><input id="rp-command-custom-title" required placeholder="Restart camera service"></div>
1272
+ <div class="rp-field"><label>Category</label><input id="rp-command-custom-category" value="Custom" placeholder="Custom"></div>
1273
+ <div class="rp-field wide"><label>Simple explanation</label><input id="rp-command-custom-description" placeholder="When to use this and what success looks like"></div>
1274
+ <div class="rp-field wide"><label>Command</label><textarea id="rp-command-custom-command" required spellcheck="false" placeholder="Paste a safe command or one-liner"></textarea></div>
1275
+ <div class="wide"><button class="rp-btn primary" type="submit">Save command to this browser</button> <span class="rp-command-note">Saved only in this operator browser.</span></div>
1276
+ </form>
1277
+ </div>
1278
+ </details>
1200
1279
  </div>
1201
1280
  </div>
1202
1281
 
@@ -1264,9 +1343,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
1264
1343
  var lastSchedulerPollAt = 0; // updated by pollRobopark() on every successful /api/telemetry fetch
1265
1344
  var lastFedStatusAt = 0; // updated by poll() on every successful /fed/status fetch
1266
1345
  var lastStatus = null; // most recent /fed/status snapshot (shared with Park)
1267
- var roboparkByName = {}; // robot_id -> latest telemetry row (shared with Park drawer)
1346
+ var roboparkByName = {}; // robot_id -> latest telemetry row (shared with Park drawer)
1268
1347
  var schedulerDevicesById = {}; // enrolled device rows + camera/audio inventory
1269
1348
  var schedulerDevicesByName = {}; // same rows, keyed by device name (id != name, e.g. dev_8984a80e vs jaguar2)
1349
+ function indexRoboparkTelemetry(rows){
1350
+ var indexed={};
1351
+ (Array.isArray(rows)?rows:[]).forEach(function(row){
1352
+ var id=String(row.robot_id||row.id||'').trim().toLowerCase();
1353
+ var name=String(row.name||'').trim().toLowerCase();
1354
+ if(id)indexed[id]=row;
1355
+ if(name)indexed[name]=row;
1356
+ });
1357
+ roboparkByName=indexed;
1358
+ return indexed;
1359
+ }
1360
+ function schedulerDeviceByReference(reference){
1361
+ var key=String(reference||'').trim().toLowerCase();
1362
+ if(!key)return null;
1363
+ return schedulerDevicesById[reference]||schedulerDevicesById[key]||schedulerDevicesByName[key]||null;
1364
+ }
1270
1365
  function schedulerDeviceScore(device){
1271
1366
  if(!device) return -1;
1272
1367
  var status=String(device.status||'').trim().toLowerCase();
@@ -1773,6 +1868,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1773
1868
  if(!id) return;
1774
1869
  var name = row.name || id;
1775
1870
  var device = deviceById[id] || deviceByName[norm(name)] || {};
1871
+ var characterId = device.character_id || row.character_id || row.character_preset_id || '';
1776
1872
  var status = String(device.status || row.status || '').toLowerCase();
1777
1873
  var heartbeat = device.last_heartbeat || row.last_heartbeat;
1778
1874
  var heartbeatAt = heartbeat ? Date.parse(heartbeat) : NaN;
@@ -1787,6 +1883,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1787
1883
  if(existing){
1788
1884
  existing.schedulerRobotId = id;
1789
1885
  existing.schedulerDeviceId = device.id || id;
1886
+ existing.parkSlotId = characterId || existing.parkSlotId || '';
1790
1887
  existing.displayName = existing.displayName || name;
1791
1888
  existing.connected = !!existing.connected || connected;
1792
1889
  existing.lastSeenAt = heartbeatAt && Number.isFinite(heartbeatAt) ? heartbeatAt : existing.lastSeenAt;
@@ -1797,6 +1894,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1797
1894
  nodeId: 'scheduler-robot-'+id,
1798
1895
  schedulerRobotId: id,
1799
1896
  schedulerDeviceId: device.id || id,
1897
+ parkSlotId: characterId,
1800
1898
  displayName: name,
1801
1899
  role: 'robot',
1802
1900
  platform: 'RoboPark robot',
@@ -1875,16 +1973,56 @@ button.act:disabled{opacity:0.5;cursor:default;}
1875
1973
  });
1876
1974
 
1877
1975
  // ── tabs: Fleet + Park + RoboPark sessions/telemetry ──
1878
- var tabs = document.querySelectorAll('.tab');
1879
- var activeTab = 'fleet';
1880
- for (var ti=0; ti<tabs.length; ti++){
1881
- (function(btn){
1882
- btn.addEventListener('click', function(){ selectTab(btn.getAttribute('data-tab')); });
1883
- })(tabs[ti]);
1884
- }
1976
+ var tabs = document.querySelectorAll('.tab');
1977
+ var activeTab = 'fleet';
1978
+ var globalSidebar=$('global-sidebar');
1979
+ var globalNavToggle=$('global-nav-toggle');
1980
+ var globalNavMobile=$('global-nav-mobile');
1981
+ var globalNavBackdrop=$('global-nav-backdrop');
1982
+ var globalNavStorageKey='rp-global-nav-collapsed-v1';
1983
+ function globalNavIsMobile(){ return !!(window.matchMedia && window.matchMedia('(max-width:760px)').matches); }
1984
+ function setGlobalNavCollapsed(collapsed){
1985
+ if(globalNavIsMobile()) collapsed=false;
1986
+ document.body.classList.toggle('global-nav-collapsed',!!collapsed);
1987
+ if(globalNavToggle){
1988
+ globalNavToggle.innerHTML=collapsed?'&gt;&gt;':'&lt;&lt;';
1989
+ globalNavToggle.setAttribute('aria-expanded',collapsed?'false':'true');
1990
+ globalNavToggle.setAttribute('aria-label',collapsed?'Expand navigation':'Collapse navigation');
1991
+ }
1992
+ if(!globalNavIsMobile()){ try{ localStorage.setItem(globalNavStorageKey,collapsed?'1':'0'); }catch(e){} }
1993
+ }
1994
+ function closeGlobalNavMobile(){
1995
+ document.body.classList.remove('global-nav-open');
1996
+ if(globalNavMobile) globalNavMobile.setAttribute('aria-expanded','false');
1997
+ }
1998
+ function openGlobalNavMobile(){
1999
+ document.body.classList.add('global-nav-open');
2000
+ if(globalNavMobile) globalNavMobile.setAttribute('aria-expanded','true');
2001
+ }
2002
+ var initialGlobalNavCollapsed=false;
2003
+ try{ initialGlobalNavCollapsed=localStorage.getItem(globalNavStorageKey)==='1'; }catch(e){}
2004
+ setGlobalNavCollapsed(initialGlobalNavCollapsed);
2005
+ if(globalNavToggle) globalNavToggle.addEventListener('click',function(){ setGlobalNavCollapsed(!document.body.classList.contains('global-nav-collapsed')); });
2006
+ if(globalNavMobile) globalNavMobile.addEventListener('click',function(){ if(document.body.classList.contains('global-nav-open')) closeGlobalNavMobile(); else openGlobalNavMobile(); });
2007
+ if(globalNavBackdrop) globalNavBackdrop.addEventListener('click',closeGlobalNavMobile);
2008
+ document.addEventListener('keydown',function(e){ if(e.key==='Escape') closeGlobalNavMobile(); });
2009
+ var globalSubNav=document.querySelectorAll('[data-global-sub]');
2010
+ for(var gsi=0;gsi<globalSubNav.length;gsi++){
2011
+ (function(btn){ btn.addEventListener('click',function(){ selectTab('robopark'); setRpSubTab(btn.getAttribute('data-global-sub')); closeGlobalNavMobile(); }); })(globalSubNav[gsi]);
2012
+ }
2013
+ for (var ti=0; ti<tabs.length; ti++){
2014
+ (function(btn){
2015
+ btn.addEventListener('click', function(){ selectTab(btn.getAttribute('data-tab')); closeGlobalNavMobile(); });
2016
+ })(tabs[ti]);
2017
+ }
1885
2018
  function selectTab(name){
1886
- activeTab = name;
1887
- for (var i=0;i<tabs.length;i++) tabs[i].classList.toggle('active', tabs[i].getAttribute('data-tab')===name);
2019
+ activeTab = name;
2020
+ if(globalSidebar) globalSidebar.setAttribute('data-active-tab',name);
2021
+ for (var i=0;i<tabs.length;i++){
2022
+ var tabActive=tabs[i].getAttribute('data-tab')===name;
2023
+ tabs[i].classList.toggle('active',tabActive);
2024
+ if(tabActive) tabs[i].setAttribute('aria-current','page'); else tabs[i].removeAttribute('aria-current');
2025
+ }
1888
2026
  $('pane-fleet').classList.toggle('active', name==='fleet');
1889
2027
  $('pane-park').classList.toggle('active', name==='park');
1890
2028
  $('pane-robopark').classList.toggle('active', name==='robopark');
@@ -1896,6 +2034,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1896
2034
  initRpSubTabs();
1897
2035
  }
1898
2036
  if (name==='park') Park.activate(); else Park.deactivate();
2037
+ closeGlobalNavMobile();
1899
2038
  }
1900
2039
  var fleetParkBtn=$('fleet-open-park'); if(fleetParkBtn) fleetParkBtn.addEventListener('click',function(){selectTab('park');});
1901
2040
  var fleetSessionsBtn=$('fleet-open-sessions'); if(fleetSessionsBtn) fleetSessionsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('live');});
@@ -1906,6 +2045,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1906
2045
  function initTab(){
1907
2046
  if (initHash==='robopark'||initHash.indexOf('robopark/')===0) selectTab('robopark');
1908
2047
  else if(initHash==='park') selectTab('park');
2048
+ else selectTab('fleet');
1909
2049
  }
1910
2050
  // initTab() is called at the very bottom of this IIFE, after Park is assigned.
1911
2051
 
@@ -2168,15 +2308,26 @@ button.act:disabled{opacity:0.5;cursor:default;}
2168
2308
  if(Park && Park.updateSlots) Park.updateSlots(parkSlots);
2169
2309
  });
2170
2310
  }
2171
- function fillCharacterDropdowns(){
2311
+ function fillCharacterDropdowns(){
2172
2312
  var sels=document.querySelectorAll('.rp-char');
2173
2313
  for(var i=0;i<sels.length;i++){ var sel=sels[i]; var cur=sel.dataset.current||'';
2174
2314
  var opts=sel.querySelectorAll('option'); for(var k=opts.length-1;k>=0;k--){ if(opts[k].value) opts[k].parentNode.removeChild(opts[k]); }
2175
2315
  for(var j=0;j<characterPresets.length;j++){ var p=characterPresets[j];
2176
2316
  var o=document.createElement('option'); o.value=p.id||p.preset_id||p.character_id||''; o.textContent=(p.name||p.id||p.preset_id||'preset')+(p.description?(' — '+p.description):''); if((o.value)===cur) o.selected=true; sel.appendChild(o);
2177
- }
2178
- }
2179
- }
2317
+ }
2318
+ }
2319
+ var commandCharacter=$('rp-command-character');
2320
+ if(commandCharacter){
2321
+ var commandCurrent=commandCharacter.dataset.current||commandCharacter.value||'vixen';
2322
+ commandCharacter.innerHTML=characterPresets.map(function(p){
2323
+ var id=p.id||p.preset_id||p.character_id||'',name=p.name||id||'character';
2324
+ return '<option value="'+esc(id)+'"'+(id===commandCurrent?' selected':'')+'>'+esc(name)+'</option>';
2325
+ }).join('');
2326
+ if(!commandCharacter.value&&characterPresets.length)commandCharacter.value=characterPresets[0].id||characterPresets[0].preset_id||characterPresets[0].character_id||'';
2327
+ commandCharacter.dataset.current=commandCharacter.value;
2328
+ renderOneMinuteSetup();
2329
+ }
2330
+ }
2180
2331
  function fillVoiceStackDropdowns(){
2181
2332
  var sels=document.querySelectorAll('.rp-voice');
2182
2333
  for(var i=0;i<sels.length;i++){ var sel=sels[i]; var cur=sel.dataset.current||'';
@@ -2269,38 +2420,33 @@ button.act:disabled{opacity:0.5;cursor:default;}
2269
2420
  }
2270
2421
  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'; }
2271
2422
 
2272
- // ── robust CLI one-liner generation, live as you type no API call
2273
- // needed to see it; 'robopark add-robot' mints its own token when run ──
2423
+ // Robust RoboVoice-first one-liners update live as the operator edits the form.
2424
+ function rpControlUrl(network){
2425
+ var env=rpCommandEnv(),host=network==='tailscale'?env.tail:env.lan;
2426
+ host=String(host||'').replace(/^https?:\\/\\//i,'').replace(/\\/+$/,'').replace(/:47913$/,'');
2427
+ return 'http://'+host+':47913';
2428
+ }
2274
2429
  function buildAddRobotCommand(name, characterId){
2275
2430
  if(!name) return '';
2276
2431
  var network=$('rb-network')&&$('rb-network').value||'lan';
2277
- var commandEnv=rpCommandEnv();
2278
- var hubHost=network==='tailscale'?commandEnv.tail:commandEnv.lan;
2279
- var hubUrl=/^https?:\/\//i.test(hubHost)?hubHost.replace(/\\/+$/,''):'http://'+hubHost.replace(/\\/+$/,'')+':47913';
2280
- var schedulerUrl=hubUrl+'/robopark';
2281
- var argv=['robopark','add-robot',name];
2282
- if(characterId) argv.push('--character', characterId);
2432
+ var hubUrl=rpControlUrl(network);
2433
+ var argv=['sudo','robopark','setup','--robot','--name',name];
2283
2434
  var site=$('rb-site')&&$('rb-site').value.trim();if(site)argv.push('--site',site);
2284
2435
  if(hubUrl) argv.push('--hub-url', hubUrl);
2285
2436
  var meshToken=$('rb-token')&&$('rb-token').value.trim()||dashboardMeshToken()||'<mesh-token>';
2286
2437
  argv.push('--token', meshToken);
2287
- if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
2438
+ if(characterId)argv.push('--character',characterId);
2288
2439
  argv.push(network==='tailscale'?'--tailscale':'--lan','--start','--auto-start','--yes');
2289
- return argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' ');
2440
+ return rpCompleteRobotCommand(argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' '));
2290
2441
  }
2291
2442
  function buildPadRobotCommand(name,characterId,enrollmentToken){
2292
- var env=rpCommandEnv(),hubHost=env.lan||'192.168.0.9';
2293
- var hubUrl=/^https?:\/\//i.test(hubHost)?hubHost.replace(/\\\/+$/,''):'http://'+hubHost.replace(/\\\/+$/,'')+':47913';
2443
+ var env=rpCommandEnv(),network=env.network||'lan',hubUrl=rpControlUrl(network);
2294
2444
  var meshToken=env.token||dashboardMeshToken()||'<mesh-token>',site=env.site||'Tel Aviv';
2295
- var argv;
2296
- if(enrollmentToken){
2297
- argv=['sudo','robopark','setup','--robot','--name',name,'--site',site,'--hub-url',hubUrl,'--token',meshToken,'--scheduler-port','8080','--port','47913','--lan','--enrollment-token',enrollmentToken,'--start','--auto-start','--yes'];
2298
- }else{
2299
- argv=['robopark','add-robot',name];
2300
- if(characterId)argv.push('--character',characterId);
2301
- argv.push('--site',site,'--hub-url',hubUrl,'--token',meshToken,'--scheduler-url',hubUrl+'/robopark','--lan','--start','--auto-start','--yes');
2302
- }
2303
- return argv.map(function(a){return /[^a-zA-Z0-9_./:=,-]/.test(a)?'"'+String(a).replace(/"/g,'\\"')+'"':String(a);}).join(' ');
2445
+ var argv=['sudo','robopark','setup','--robot','--name',name,'--site',site,'--hub-url',hubUrl,'--token',meshToken,'--port','47913',network==='tailscale'?'--tailscale':'--lan'];
2446
+ if(enrollmentToken)argv.push('--enrollment-token',enrollmentToken);
2447
+ if(characterId)argv.push('--character',characterId);
2448
+ argv.push('--start','--auto-start','--yes');
2449
+ return rpCompleteRobotCommand(argv.map(function(a){return /[^a-zA-Z0-9_./:=,-]/.test(a)?'"'+String(a).replace(/"/g,'\\"')+'"':String(a);}).join(' '));
2304
2450
  }
2305
2451
  function refreshRobotCliCommand(){
2306
2452
  var name=$('rb-name').value.trim(); var cid=$('rb-char').value||'';
@@ -2395,7 +2541,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2395
2541
  +'<div class="rp-config-editor-intro"><span class="mark">AI</span><div><b>'+(editing?'Edit character preset':'Create character preset')+'</b><span>The persona prompt and default voice stack are frozen into each session record for reliable history and replay.</span></div></div>'
2396
2542
  +'<div class="rp-config-section"><div class="rp-config-section-head">Identity <span>fleet-facing appearance</span></div><div class="rp-config-fields"><div class="rp-field"><label>Preset ID</label><input id="cfg-cp-id" value="'+esc(p.id||'')+'" placeholder="jaguar"'+(editing?' disabled':'')+'></div><div class="rp-field"><label>Display name</label><input id="cfg-cp-name" value="'+esc(p.name||'')+'" placeholder="JAGUAR"></div><div class="rp-field wide"><label>Description</label><input id="cfg-cp-description" value="'+esc(p.description||'')+'" placeholder="Short operator-facing description"></div><div class="rp-field wide"><label>Avatar image path</label><input id="cfg-cp-img" value="'+esc(p.img||'')+'" placeholder="/static/robopark/jaguar.png"></div></div></div>'
2397
2543
  +'<div class="rp-config-section"><div class="rp-config-section-head">Persona intelligence <span>ROBOVOICE system prompt</span></div><div class="rp-config-fields"><div class="rp-field wide"><label>System prompt</label><textarea id="cfg-cp-prompt" placeholder="Define personality, constraints, speaking style, and robot behavior…">'+esc(p.system_prompt||'')+'</textarea></div></div></div>'
2398
- +'<div class="rp-config-section"><div class="rp-config-section-head">Voice &amp; embodiment <span>pipeline and physical capabilities</span></div><div class="rp-config-fields"><div class="rp-field"><label>Default voice stack</label><select id="cfg-cp-stack">'+stackOptions+'</select></div><div class="rp-field"><label>Motor capabilities</label><input id="cfg-cp-motors" value="'+esc((p.motors||[]).join(', '))+'" placeholder="drive, wave, look"></div><div class="rp-field"><label>Park pad X (0–1)</label><input id="cfg-cp-nx" type="number" min="0" max="1" step="0.01" value="'+esc(p.nx!=null?p.nx:0.5)+'"></div><div class="rp-field"><label>Park pad Y (0–1)</label><input id="cfg-cp-ny" type="number" min="0" max="1" step="0.01" value="'+esc(p.ny!=null?p.ny:0.5)+'"></div></div></div>'
2544
+ +'<div class="rp-config-section"><div class="rp-config-section-head">Voice &amp; embodiment <span>pipeline and physical permissions</span></div><div class="rp-config-fields"><div class="rp-field"><label>Default voice stack</label><select id="cfg-cp-stack">'+stackOptions+'</select></div><div class="rp-field"><label>Agent-allowed motor sequences</label><input id="cfg-cp-motors" value="'+esc((p.motors||[]).join(', '))+'" placeholder="greeting-motion, wave"><small>Exact saved sequence IDs. Empty means this character cannot invoke motors through agent tools.</small></div><div class="rp-field"><label>Park pad X (0–1)</label><input id="cfg-cp-nx" type="number" min="0" max="1" step="0.01" value="'+esc(p.nx!=null?p.nx:0.5)+'"></div><div class="rp-field"><label>Park pad Y (0–1)</label><input id="cfg-cp-ny" type="number" min="0" max="1" step="0.01" value="'+esc(p.ny!=null?p.ny:0.5)+'"></div></div></div>'
2399
2545
  +'<div class="rp-config-editor-foot"><button class="rp-btn" id="cfg-cp-cancel">Cancel</button><button class="rp-btn primary" id="cfg-cp-save">'+(editing?'Save changes':'Create character')+'</button></div></div>';
2400
2546
  showModal(editing?'Edit character · '+(p.name||p.id):'New character preset',html); $('rp-modal').classList.add('config-modal'); $('cfg-cp-cancel').addEventListener('click',closeModal);
2401
2547
  $('cfg-cp-save').addEventListener('click',function(){ var body=characterDraft(); if(!body.name){ showOut('Character name is required'); return; } if(!editing&&!body.id) body.id=body.name.toLowerCase().replace(/[^a-z0-9_-]+/g,'-').replace(/^-|-$/g,'').slice(0,48); var btn=$('cfg-cp-save');btn.disabled=true;btn.textContent='Saving…';rpAction(editing?'PUT':'POST',editing?'/robopark/api/character-presets/'+encodeURIComponent(id):'/robopark/api/character-presets',body).then(function(){closeModal();showOut((editing?'Updated ':'Created ')+body.name);return Promise.all([loadCharacterPresets(),loadParkSlots()]);}).then(function(){pollRobopark();if(Park&&Park.refreshDrawer)Park.refreshDrawer();}).catch(function(e){btn.disabled=false;btn.textContent=editing?'Save changes':'Create character';showOut('character preset: '+e.message);}); });
@@ -2572,8 +2718,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2572
2718
  lastSchedulerPollAt = Date.now();
2573
2719
  var grid = $('rp-grid'), totals = $('rp-totals');
2574
2720
  // stash per-robot rows so the Park drawer can show live session stats
2575
- roboparkByName = {};
2576
- robotsFrom(d).forEach(function(x){ roboparkByName[String(robotName(x)).toLowerCase()] = x; if(x.name) roboparkByName[String(x.name).toLowerCase()] = x; });
2721
+ indexRoboparkTelemetry(robotsFrom(d));
2577
2722
  // C4: also feed lastRobots for the Incidents sub-tab
2578
2723
  lastRobots = robotsFrom(d) || [];
2579
2724
  // push the same map into the Park so the canvas can color robots
@@ -2695,43 +2840,57 @@ button.act:disabled{opacity:0.5;cursor:default;}
2695
2840
 
2696
2841
  // ── Sub-tab routing ──
2697
2842
  // Command sheet: generate production commands from one saved environment.
2698
- var RP_COMMAND_ENV_KEY='rp-command-env-v1';
2843
+ var RP_COMMAND_ENV_KEY='rp-command-env-v2';
2699
2844
  var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
2700
2845
  var rpVisibleCommands=[];
2701
- function rpCommandDefaults(){return {lan:'192.168.0.9',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:dashboardMeshToken()};}
2846
+ function rpCommandDefaults(){return {lan:'192.168.0.9',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',character:'vixen',device:'',enrollment:'',token:dashboardMeshToken(),robovoice:'C:\\ROBOVOICE-main',release:'2.8.113',network:'lan'};}
2702
2847
  function rpLoadStoredJson(key,fallback){try{var value=JSON.parse(localStorage.getItem(key)||'null');return value&&typeof value==='object'?value:fallback;}catch(e){return fallback;}}
2703
2848
  function rpCommandEnv(){
2704
2849
  var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2705
2850
  function value(name){var input=$('rp-command-'+name),raw=input?input.value:(s[name]!=null?s[name]:d[name]);return String(raw==null?'':raw).trim();}
2706
- return {lan:value('lan'),tail:value('tail'),site:value('site'),robot:value('robot'),enrollment:value('enrollment'),token:value('token')};
2851
+ return {lan:value('lan'),tail:value('tail'),site:value('site'),robot:value('robot'),character:value('character'),device:value('device'),enrollment:value('enrollment'),token:value('token'),robovoice:value('robovoice'),release:value('release'),network:value('network')||'lan'};
2707
2852
  }
2708
2853
  function rpSaveCommandEnv(){try{localStorage.setItem(RP_COMMAND_ENV_KEY,JSON.stringify(rpCommandEnv()));}catch(e){}}
2709
2854
  function rpShellArg(value){var text=String(value||'');return /^[A-Za-z0-9._:\/-]+$/.test(text)?text:'"'+text.replace(/"/g,'\\"')+'"';}
2855
+ function rpCompleteRobotCommand(setupCommand){var release=rpCommandEnv().release||'2.8.113';return 'sudo npm install -g infinicode@'+rpShellArg(release)+' --force && hash -r && (sudo robopark stop || true) && '+setupCommand;}
2856
+ function rpRobotSetupCommand(){
2857
+ var e=rpCommandEnv(),control=rpControlUrl(e.network),argv=['sudo','robopark','setup','--robot','--name',e.robot,'--site',e.site,'--hub-url',control,'--token',e.token,'--port','47913',e.network==='tailscale'?'--tailscale':'--lan'];
2858
+ if(e.enrollment)argv.push('--enrollment-token',e.enrollment);if(e.character)argv.push('--character',e.character);argv.push('--start','--auto-start','--yes');
2859
+ return rpCompleteRobotCommand(argv.map(function(a){return rpShellArg(a);}).join(' '));
2860
+ }
2710
2861
  function rpCommandCatalog(){
2711
- var e=rpCommandEnv(),lan='http://'+e.lan,tail='http://'+e.tail,enroll=e.enrollment?' --enrollment-token '+rpShellArg(e.enrollment):'';
2862
+ var e=rpCommandEnv(),control=rpControlUrl(e.network),device=e.device||e.robot,enroll=e.enrollment?' --enrollment-token '+rpShellArg(e.enrollment):'',auth='?token='+encodeURIComponent(e.token);
2863
+ var api=control+'/robopark/api',release=e.release||'2.8.113';
2712
2864
  return [
2713
- {id:'hub-up',category:'Provision',title:'Start production hub and full dashboard',description:'Run elevated on livekit-1. Starts the mesh hub, full Park control center, scheduler proxy, LAN discovery, supervision, and updates.',command:'infinicode serve --hub --name livekit-1 --port 47913 --token '+rpShellArg(e.token)+' --lan --dashboard --scheduler-url http://127.0.0.1:8080 --auto-update --supervised'},
2714
- {id:'robot-lan',category:'Provision',title:'Provision LAN robot',description:'One command for a Linux robot on the hub LAN. Installs the supervised mesh, RoboVision, and preview runtime.',command:'sudo robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+lan+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --lan --start --auto-start --yes'},
2715
- {id:'robot-tail',category:'Provision',title:'Provision Tailscale robot',description:'One command for a Windows or Linux robot reaching the hub through Tailscale.',command:'robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+tail+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --tailscale --start --auto-start --yes'},
2716
- {id:'operator-tail',category:'Provision',title:'Connect operator MCP over Tailscale',description:'Connects an operator workstation and coding agents to the production mesh.',command:'infinicode mcp --lan --tailscale --seed '+tail+':47913 --name dev-laptop --token '+rpShellArg(e.token)+' --mesh-port 47913'},
2717
- {id:'hub-ui',category:'Health',title:'Open full production control center',description:'Opens the full Park dashboard through the hub Tailscale address.',command:'Start-Process "'+tail+':47913/?token='+encodeURIComponent(e.token)+'#park"'},
2718
- {id:'hub-build',category:'Health',title:'Verify current hub UI',description:'Shows the installed version and dashboard build marker after npm update or restart.',command:'infinicode --version; Invoke-RestMethod "http://127.0.0.1:47913/fed/ui-build?token='+encodeURIComponent(e.token)+'"'},
2865
+ {id:'hub-up',category:'Provision',title:'Install and start RoboVoice-first control host',description:'Run in elevated PowerShell on livekit-1. Starts the private compatibility data service, proven RoboVoice stack, mesh relay, and full Control Center.',command:'npm install -g infinicode@'+rpShellArg(release)+' --force; robopark serve --port 8080 --no-open; robopark agent-up --path '+rpShellArg(e.robovoice)+' --scheduler-url http://127.0.0.1:8080 --token '+rpShellArg(e.token)+'; infinicode serve --hub --name livekit-1 --port 47913 --token '+rpShellArg(e.token)+' --lan --dashboard --scheduler-url http://127.0.0.1:8080 --auto-update --supervised'},
2866
+ {id:'robot-install',category:'Provision',title:'Install or repair selected robot',description:'Run on the Pi. One control URL, one token, one supervised runtime; camera, audio, motion, motor server, and mesh start together.',command:rpRobotSetupCommand()},
2867
+ {id:'operator-tail',category:'Provision',title:'Connect operator agents to control mesh',description:'Connects Codex/OpenCode tools through the livekit-1 control URL without exposing the private data service.',command:'infinicode mcp --lan --tailscale --seed '+rpControlUrl('tailscale').replace(/^http:\\/\\//,'')+' --name dev-laptop --token '+rpShellArg(e.token)+' --mesh-port 47913'},
2868
+ {id:'production-on',category:'Production',title:'Arm always-on motion conversations',description:'Enables local motion-triggered RoboVoice calls for the canonical device.',command:'Invoke-RestMethod -Method Patch -ContentType "application/json" -Body (@{production_mode=$true} | ConvertTo-Json) "'+api+'/devices/'+encodeURIComponent(device)+auth+'"'},
2869
+ {id:'production-off',category:'Production',title:'Disarm production conversations',description:'Stops new motion-triggered calls without taking camera monitoring or remote control offline.',command:'Invoke-RestMethod -Method Patch -ContentType "application/json" -Body (@{production_mode=$false} | ConvertTo-Json) "'+api+'/devices/'+encodeURIComponent(device)+auth+'"'},
2870
+ {id:'session-start',category:'Voice',title:'Start one production conversation',description:'Runs the same robot motion-to-RoboVoice path used onsite.',command:'Invoke-RestMethod -Method Post "'+api+'/robots/'+encodeURIComponent(device)+'/pipeline-test/start'+auth+'" | ConvertTo-Json -Depth 8'},
2871
+ {id:'session-stop',category:'Voice',title:'Stop current conversation',description:'Ends the selected robot session while leaving production mode armed.',command:'Invoke-RestMethod -Method Post "'+api+'/robots/'+encodeURIComponent(device)+'/pipeline-test/stop'+auth+'" | ConvertTo-Json -Depth 8'},
2872
+ {id:'voice-health',category:'Voice',title:'Verify RoboVoice configuration API',description:'Confirms characters and voice stacks are visible through the single control URL.',command:'Invoke-RestMethod "'+api+'/character-presets'+auth+'"; Invoke-RestMethod "'+api+'/voice-stacks'+auth+'"'},
2873
+ {id:'hub-ui',category:'Health',title:'Open full production control center',description:'Opens Park through the selected LAN or Tailscale control URL.',command:'Start-Process "'+control+'/?token='+encodeURIComponent(e.token)+'#park"'},
2874
+ {id:'hub-build',category:'Health',title:'Verify deployed Control Center release',description:'Compares installed CLI, UI build, and expected pinned release.',command:'infinicode --version; Write-Host "expected '+release+'"; Invoke-RestMethod "'+control+'/fed/ui-build'+auth+'"'},
2719
2875
  {id:'hub-ports',category:'Health',title:'Inspect hub ports on Windows',description:'Lists processes owning the control center, scheduler, and LiveKit ports without changing them.',command:'Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -in 47913,8080,7880,7881,7882,7883 } | Sort-Object LocalPort | Format-Table LocalAddress,LocalPort,OwningProcess -AutoSize'},
2720
- {id:'scheduler-health',category:'Health',title:'Check scheduler through hub',description:'Confirms the hub proxy can reach the scheduler and return robot inventory.',command:'Invoke-RestMethod "'+tail+':47913/robopark/api/robots?token='+encodeURIComponent(e.token)+'" | ConvertTo-Json -Depth 6'},
2721
- {id:'mesh-status',category:'Health',title:'Check mesh nodes',description:'Lists connected hub, robot, and operator nodes.',command:'Invoke-RestMethod "'+tail+':47913/fed/status?token='+encodeURIComponent(e.token)+'" | ConvertTo-Json -Depth 6'},
2876
+ {id:'control-health',category:'Health',title:'Check robots through control URL',description:'Confirms the management API returns canonical robot and device inventory.',command:'Invoke-RestMethod "'+api+'/devices'+auth+'" | ConvertTo-Json -Depth 8'},
2877
+ {id:'mesh-status',category:'Health',title:'Check mesh nodes',description:'Lists connected hub, robot, and operator nodes.',command:'Invoke-RestMethod "'+control+'/fed/status'+auth+'" | ConvertTo-Json -Depth 6'},
2722
2878
  {id:'pi-runtime',category:'Health',title:'Follow robot runtime logs',description:'Live systemd logs for the selected robot unified runtime.',command:'sudo journalctl -u robopark-robot-runtime-'+e.robot+'.service -f'},
2723
2879
  {id:'pi-ports',category:'Health',title:'Inspect robot service ports',description:'Checks mesh, camera, motion webhook, and audio listeners on a Linux robot.',command:"sudo ss -ltnp | grep -E ':(47913|5000|5057|8000)'"},
2880
+ {id:'camera-relay',category:'Media',title:'Open always-on robot camera relay',description:'Opens RoboVision video through authenticated livekit-1 mesh relay; no voice session is required.',command:'Start-Process "'+control+'/fed/media/robots/'+encodeURIComponent(e.robot)+'/video_feed'+auth+'"'},
2724
2881
  {id:'camera-inventory',category:'Media',title:'Read RoboVision media inventory',description:'Returns the real cameras, microphones, and speakers detected by RoboVision.',command:'curl -s http://127.0.0.1:5000/api/media/inventory'},
2725
2882
  {id:'audio-inventory',category:'Media',title:'Read RoboVision audio devices',description:'Returns the microphone and speaker inventory used by the robot audio server.',command:'curl -s http://127.0.0.1:8000/devices'},
2726
2883
  {id:'camera-devices',category:'Media',title:'List valid Linux capture devices',description:'Shows V4L2 devices and capabilities so metadata-only nodes are not selected as cameras.',command:'v4l2-ctl --list-devices; for d in /dev/video*; do echo "--- $d"; v4l2-ctl -d "$d" --all 2>/dev/null | grep -E "Card type|Video Capture|Device Caps"; done'},
2727
2884
  {id:'camera-preview',category:'Media',title:'Open direct RoboVision camera feed',description:'Opens the robot MJPEG feed locally for camera-only diagnosis.',command:'xdg-open http://127.0.0.1:5000/video_feed'},
2728
- {id:'robot-restart',category:'Recovery',title:'Clean restart selected robot',description:'Stops stale supervised processes, then recreates the complete LAN runtime.',command:'sudo robopark stop && sudo robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+lan+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --lan --start --auto-start --yes'},
2885
+ {id:'motor-discover',category:'Motors',title:'Discover registered robot relays',description:'Queues safe registry discovery through the canonical robot supervisor without pulsing GPIO.',command:'Invoke-RestMethod -Method Post "'+api+'/robots/'+encodeURIComponent(device)+'/motors/discover'+auth+'" | ConvertTo-Json -Depth 8'},
2886
+ {id:'motor-stop',category:'Motors',title:'Emergency all motors off',description:'Immediately queues all registered relays off for the selected robot.',command:'Invoke-RestMethod -Method Post "'+api+'/robots/'+encodeURIComponent(device)+'/motors/stop'+auth+'" | ConvertTo-Json -Depth 8'},
2887
+ {id:'robot-restart',category:'Recovery',title:'Clean repair selected robot',description:'Stops the supervised runtime, installs the pinned release, and recreates the complete runtime from one control URL.',command:rpRobotSetupCommand()},
2729
2888
  {id:'robot-status',category:'Recovery',title:'Inspect robot service failure',description:'Shows service state and the latest 150 log lines after a failed start.',command:'sudo systemctl status robopark-robot-runtime-'+e.robot+'.service --no-pager; sudo journalctl -u robopark-robot-runtime-'+e.robot+'.service -n 150 --no-pager'},
2730
2889
  {id:'docker-health',category:'Health',title:'Inspect Docker and LiveKit containers',description:'Shows container state, health, ports, and names on the hub.',command:'docker ps -a --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\\t{{.Image}}"'},
2731
2890
  {id:'docker-restart',category:'Recovery',title:'Restart Docker Desktop service',description:'Run from elevated PowerShell only when Docker Desktop is stuck starting.',command:'Restart-Service com.docker.service; Start-Process -FilePath "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"'},
2732
2891
  {id:'tailscale-status',category:'Health',title:'Check Tailscale routes and Serve',description:'Confirms the node is connected and shows any active Serve proxy.',command:'tailscale status; tailscale serve status'},
2733
- {id:'npm-version',category:'Packages',title:'Check installed and published versions',description:'Compares this device with the npm latest tag.',command:'infinicode --version; npm view infinicode version'},
2734
- {id:'npm-update',category:'Packages',title:'Update InfiniCode and RoboPark',description:'Installs the current published production package globally.',command:'npm install -g infinicode@latest --force'},
2892
+ {id:'npm-version',category:'Packages',title:'Check installed, pinned, and published versions',description:'Prevents accidental downgrade to an older latest tag.',command:'infinicode --version; Write-Host "pinned '+release+'"; npm view infinicode version'},
2893
+ {id:'npm-update',category:'Packages',title:'Install pinned RoboPark release',description:'Installs the exact release selected above instead of an ambiguous latest tag.',command:'npm install -g infinicode@'+rpShellArg(release)+' --force'},
2735
2894
  {id:'npm-cache',category:'Packages',title:'Repair npm metadata cache',description:'Use when a new version returns ETARGET. Verify metadata before clearing anything.',command:'npm cache verify; npm view infinicode dist-tags --json'}
2736
2895
  ];
2737
2896
  }
@@ -2739,6 +2898,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
2739
2898
  function rpSaveCustomCommands(list){try{localStorage.setItem(RP_CUSTOM_COMMANDS_KEY,JSON.stringify(list));}catch(e){showOut('Could not save custom command: '+e.message);}}
2740
2899
  function rpLegacyCopy(value){var area=document.createElement('textarea');area.value=value;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();try{document.execCommand('copy');showOut('command copied');}catch(e){showOut('Copy failed: select the command manually');}area.remove();}
2741
2900
  function rpCopyCommand(text){var value=String(text||'');if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(value).then(function(){showOut('command copied');}).catch(function(){rpLegacyCopy(value);});}else rpLegacyCopy(value);}
2901
+ function renderOneMinuteSetup(){
2902
+ var code=$('rp-minute-command'),button=$('rp-minute-copy'),status=$('rp-minute-status');if(!code||!button||!status)return;
2903
+ var e=rpCommandEnv(),host=e.network==='tailscale'?e.tail:e.lan,issues=[];
2904
+ if(!e.robot)issues.push('Enter the robot name');else if(!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(e.robot))issues.push('Robot name may use letters, numbers, dash, and underscore only');
2905
+ if(!e.character)issues.push('Choose a character');
2906
+ if(!host)issues.push('Set the livekit-1 address under Advanced tools');
2907
+ if(!e.token)issues.push('Open this dashboard with its token, or enter the control token under Advanced tools');
2908
+ var command=rpRobotSetupCommand();code.textContent=command;button.disabled=issues.length>0;button.dataset.command=command;
2909
+ status.className='rp-minute-status '+(issues.length?'bad':'ok');status.textContent=issues.length?issues[0]:'Ready: copy this command and run it once on '+e.robot+'.';
2910
+ }
2742
2911
  function renderCommandSheet(){
2743
2912
  var grid=$('rp-command-grid');if(!grid)return;
2744
2913
  var search=String(($('rp-command-search')||{}).value||'').toLowerCase().trim(),category=String(($('rp-command-category')||{}).value||'').toLowerCase();
@@ -2749,25 +2918,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
2749
2918
  grid.innerHTML=rpVisibleCommands.map(function(x){return '<article class="rp-command-card"><div class="rp-command-head"><b>'+esc(x.title)+'</b><span class="rp-command-tag">'+esc(x.category)+'</span></div><p>'+esc(x.description)+'</p><pre class="rp-command-code"><code>'+esc(x.command)+'</code></pre><div class="rp-command-actions"><button class="rp-btn primary" data-command-copy="'+esc(x.id)+'">Copy command</button>'+(x.custom?'<button class="rp-btn danger" data-command-delete="'+esc(x.id)+'">Delete</button>':'')+'</div></article>';}).join('');
2750
2919
  }
2751
2920
  function initCommandSheet(){
2752
- var fields=['lan','tail','site','robot','enrollment','token'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2753
- fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=name==='token'&&TOKEN?TOKEN:(saved[name]!=null?saved[name]:defaults[name]);input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
2754
- var search=$('rp-command-search'),category=$('rp-command-category'),grid=$('rp-command-grid'),reset=$('rp-command-reset'),copyAll=$('rp-command-copy-all'),form=$('rp-command-custom-form');
2921
+ var fields=['lan','tail','site','robot','character','device','enrollment','token','robovoice','release','network'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2922
+ fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;var initial=name==='token'&&TOKEN?TOKEN:(saved[name]!=null?saved[name]:defaults[name]);if(name==='character')input.dataset.current=initial;input.value=initial;var refresh=function(){if(name==='character')input.dataset.current=input.value;rpSaveCommandEnv();renderOneMinuteSetup();renderCommandSheet();};input.addEventListener('input',refresh);input.addEventListener('change',refresh);});
2923
+ var search=$('rp-command-search'),category=$('rp-command-category'),grid=$('rp-command-grid'),reset=$('rp-command-reset'),copyAll=$('rp-command-copy-all'),form=$('rp-command-custom-form'),minuteCopy=$('rp-minute-copy');
2755
2924
  if(search)search.addEventListener('input',renderCommandSheet);if(category)category.addEventListener('change',renderCommandSheet);
2756
- if(reset)reset.addEventListener('click',function(){try{localStorage.removeItem(RP_COMMAND_ENV_KEY);}catch(e){}fields.forEach(function(name){var input=$('rp-command-'+name);if(input)input.value=defaults[name];});renderCommandSheet();showOut('command environment reset');});
2925
+ if(reset)reset.addEventListener('click',function(){try{localStorage.removeItem(RP_COMMAND_ENV_KEY);}catch(e){}fields.forEach(function(name){var input=$('rp-command-'+name);if(input){input.value=defaults[name];if(name==='character')input.dataset.current=defaults[name];}});renderOneMinuteSetup();renderCommandSheet();showOut('setup reset');});
2926
+ if(minuteCopy)minuteCopy.addEventListener('click',function(){if(minuteCopy.disabled)return;rpCopyCommand(minuteCopy.dataset.command||'');minuteCopy.textContent='Copied - paste on robot';setTimeout(function(){minuteCopy.textContent='Copy setup command';},2200);});
2757
2927
  if(copyAll)copyAll.addEventListener('click',function(){rpCopyCommand(rpVisibleCommands.map(function(x){return '# '+x.title+'\\n'+x.command;}).join('\\n\\n'));});
2758
2928
  if(grid)grid.addEventListener('click',function(ev){var copy=ev.target.closest('[data-command-copy]'),del=ev.target.closest('[data-command-delete]');if(copy){var id=copy.getAttribute('data-command-copy'),item=rpVisibleCommands.find(function(x){return x.id===id;});if(item)rpCopyCommand(item.command);}if(del){var removeId=del.getAttribute('data-command-delete');rpSaveCustomCommands(rpCustomCommands().filter(function(x){return String(x.id)!==removeId;}));renderCommandSheet();showOut('custom command deleted');}});
2759
2929
  if(form)form.addEventListener('submit',function(ev){ev.preventDefault();var title=$('rp-command-custom-title').value.trim(),command=$('rp-command-custom-command').value.trim();if(!title||!command)return;var list=rpCustomCommands();list.push({id:'custom-'+Date.now(),title:title,category:$('rp-command-custom-category').value.trim()||'Custom',description:$('rp-command-custom-description').value.trim(),command:command});rpSaveCustomCommands(list);$('rp-command-custom-title').value='';$('rp-command-custom-description').value='';$('rp-command-custom-command').value='';renderCommandSheet();showOut('custom command saved in this browser');});
2760
- renderCommandSheet();
2930
+ renderOneMinuteSetup();renderCommandSheet();
2761
2931
  }
2762
2932
  initCommandSheet();
2763
2933
 
2764
2934
  var rpSubTab = 'overview';
2765
2935
  var rpIntervals = {}; // sub-tab name -> setInterval handle
2766
2936
  var rpHistoryAll = []; // full recent-session list (we paginate client-side)
2767
- function setRpSubTab(name){
2768
- if(!name) return;
2769
- rpSubTab = name;
2770
- var btns = document.querySelectorAll('.rp-subtab');
2937
+ function setRpSubTab(name){
2938
+ if(!name) return;
2939
+ rpSubTab = name;
2940
+ var globalBtns=document.querySelectorAll('[data-global-sub]');
2941
+ for(var gi=0;gi<globalBtns.length;gi++){
2942
+ var globalActive=globalBtns[gi].getAttribute('data-global-sub')===name;
2943
+ globalBtns[gi].classList.toggle('active',globalActive);
2944
+ if(globalActive) globalBtns[gi].setAttribute('aria-current','page'); else globalBtns[gi].removeAttribute('aria-current');
2945
+ }
2946
+ var btns = document.querySelectorAll('.rp-subtab');
2771
2947
  for(var i=0;i<btns.length;i++){
2772
2948
  var b=btns[i], sub=b.getAttribute('data-sub');
2773
2949
  var isActive = sub===name;
@@ -2792,7 +2968,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2792
2968
  if(name==='history'){ rpHistoryPage=0; pollHistory(); }
2793
2969
  if(name==='audit'){ pollAuditLog(); rpIntervals.audit = setInterval(pollAuditLog, 6000); }
2794
2970
  if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
2795
- if(name==='commands'){ renderCommandSheet(); }
2971
+ if(name==='commands'){ renderOneMinuteSetup(); renderCommandSheet(); }
2796
2972
  if(name==='setup'){ pollSettings(); loadCharacterPresets(); loadVoiceStacks(); pollRuntimeHealth(); }
2797
2973
  // remember last sub-tab in URL hash
2798
2974
  try{ history.replaceState(null,'','#robopark/'+name); }catch(e){}
@@ -3056,14 +3232,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
3056
3232
  if(!filtered.length){ tbody.innerHTML=''; wrap.style.display='none'; empty.style.display='block'; }
3057
3233
  else { empty.style.display='none'; wrap.style.display='block'; }
3058
3234
  var html='';
3059
- for(var i=0;i<filtered.length;i++){
3060
- var s = filtered[i];
3061
- var rid = s.robot_id || '';
3062
- var sid = s.session_id || '';
3235
+ for(var i=0;i<filtered.length;i++){
3236
+ var s = filtered[i];
3237
+ var rid = s.robot_id || '';
3238
+ var sessionDevice=schedulerDeviceByReference(rid);
3239
+ var robotLabel=(sessionDevice&&sessionDevice.name)||s.robot_name||s.name||rid;
3240
+ var characterLabel=(sessionDevice&&sessionDevice.character_id)||s.character_name||s.character_id||'';
3241
+ var sid = s.session_id || '';
3063
3242
  var started = s.started_at ? Date.parse(s.started_at) : null;
3064
3243
  var elapsed = started ? Math.floor((Date.now()-started)/1000) : null;
3065
3244
  html += '<tr>'
3066
- + '<td><b>'+esc(rid)+'</b></td>'
3245
+ + '<td><b>'+esc(robotLabel)+'</b><span class="t-abs">'+esc(rid)+(characterLabel?(' · '+esc(characterLabel)):'')+'</span></td>'
3067
3246
  + '<td><span class="sid" title="'+esc(sid)+'">'+esc(sid)+'</span></td>'
3068
3247
  + '<td><span class="t-rel"><span class="live-dot"></span>'+esc(rpFmtRelative(started))+'</span><span class="t-abs">'+esc(rpFmtAbs(started))+'</span></td>'
3069
3248
  + '<td data-started="'+started+'">'+esc(rpFmtDuration(elapsed))+'</td>'
@@ -3099,7 +3278,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
3099
3278
  if(!b) return;
3100
3279
  var act = b.getAttribute('data-act');
3101
3280
  if(act==='end-session'){ endSessionFromTable(b.getAttribute('data-sid'), b.getAttribute('data-robot')); }
3102
- if(act==='open-robot'){ var rid=b.getAttribute('data-robot'); if(rid){ setRpSubTab('fleet'); } }
3281
+ if(act==='open-robot'){ var rid=b.getAttribute('data-robot'); if(rid){ selectTab('park'); setTimeout(function(){Park.openDrawerForRobot(rid);},0); } }
3103
3282
  });
3104
3283
  })();
3105
3284
 
@@ -4320,13 +4499,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
4320
4499
  // looks up each robot node by its slot aliases and attaches a
4321
4500
  // status + telemetry field used by drawRobot() to drive the
4322
4501
  // status animations.
4323
- function updateTelemetry(map){
4324
- _telemetryByName = map || {};
4325
- }
4326
- function lookupTelemetry(node){
4327
- if(!node) return null;
4502
+ function updateTelemetry(map){
4503
+ _telemetryByName = map || {};
4504
+ if(lastStatus) onStatus(lastStatus);
4505
+ }
4506
+ function lookupTelemetry(node){
4507
+ if(!node) return null;
4328
4508
  // The hub is never a per-robot telemetry row — only robot slots are.
4329
- if(node.kind !== 'robot') return null;
4509
+ if(node.kind !== 'robot') return null;
4510
+ var exactIds=[node.schedulerDeviceId,node.schedulerRobotId];
4511
+ for(var ei=0;ei<exactIds.length;ei++){
4512
+ var exactKey=String(exactIds[ei]||'').toLowerCase();
4513
+ if(exactKey&&_telemetryByName[exactKey]) return _telemetryByName[exactKey];
4514
+ }
4330
4515
  // Try every alias the slot was built with: slot.label, slot.id,
4331
4516
  // slot.sub, slot.keys, and the matched mesh node's displayName.
4332
4517
  var slot = null;
@@ -4402,11 +4587,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
4402
4587
  }
4403
4588
 
4404
4589
  function layout(src){
4405
- var list=(src||[]).slice(), used={}, out=[];
4406
- function prevHit(id){ for(var p=0;p<nodes.length;p++){ if(nodes[p].id===id) return nodes[p]._hit; } return null; }
4407
- function bind(slot, wx, wy, kind){
4408
- var match=null;
4409
- for(var i=0;i<list.length;i++){ var n=list[i]; if(used[n.nodeId]) continue;
4590
+ var list=(src||[]).slice(), used={}, out=[];
4591
+ function prevHit(id){ for(var p=0;p<nodes.length;p++){ if(nodes[p].id===id) return nodes[p]._hit; } return null; }
4592
+ function bind(slot, wx, wy, kind){
4593
+ var match=null;
4594
+ if(kind!=='hub'){
4595
+ for(var exactIndex=0;exactIndex<list.length;exactIndex++){
4596
+ var exactNode=list[exactIndex];
4597
+ if(used[exactNode.nodeId]) continue;
4598
+ if(String(exactNode.parkSlotId||'').toLowerCase()===String(slot.id||'').toLowerCase()){
4599
+ match=exactNode;
4600
+ break;
4601
+ }
4602
+ }
4603
+ }
4604
+ for(var i=0;i<list.length;i++){ var n=list[i]; if(used[n.nodeId]) continue;
4605
+ if(match) break;
4410
4606
  var nm=(n.displayName||n.nodeId||'').toLowerCase(), isHub=kindOf(n)==='hub';
4411
4607
  if(kind==='hub'){ if(isHub){ match=n; break; } }
4412
4608
  else if(!isHub){ for(var k=0;k<slot.keys.length;k++){ if(slot.keys[k] && nm.indexOf(slot.keys[k])>-1){ match=n; break; } } if(match) break; }
@@ -4421,9 +4617,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
4421
4617
  load: (match&&match.load!=null)?match.load:0,
4422
4618
  platform: match?(match.platform||''):'', version: match?(match.version||'?'):'—',
4423
4619
  site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
4424
- schedulerRobotId: match?(match.schedulerRobotId||match.schedulerDeviceId||''):'',
4425
- schedulerDeviceId: match?(match.schedulerDeviceId||match.schedulerRobotId||''):'',
4426
- telemKeys: [slot.label.toLowerCase()].concat(slot.keys||[]).concat(match?[(match.displayName||'').toLowerCase()]:[]),
4620
+ schedulerRobotId: match?(match.schedulerRobotId||match.schedulerDeviceId||''):'',
4621
+ schedulerDeviceId: match?(match.schedulerDeviceId||match.schedulerRobotId||''):'',
4622
+ parkSlotId: match?(match.parkSlotId||slot.id):slot.id,
4623
+ telemKeys: [slot.label.toLowerCase(),String(slot.id||'').toLowerCase()]
4624
+ .concat(slot.keys||[])
4625
+ .concat(match?(match.telemKeys||[]):[])
4626
+ .concat(match?[(match.displayName||'').toLowerCase(),String(match.schedulerDeviceId||'').toLowerCase(),String(match.schedulerRobotId||'').toLowerCase()]:[])
4627
+ .filter(function(value,index,all){return value&&all.indexOf(value)===index;}),
4427
4628
  // slotId is the stable handle we use to persist operator-
4428
4629
  // chosen positions. slotNx/slotNy are the preset's default
4429
4630
  // coordinates so the user can always reset back.
@@ -4448,12 +4649,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
4448
4649
  SLOTS.forEach(function(s){ var w=W2(s.nx,s.ny); out.push(bind(s,w[0],w[1],'robot')); });
4449
4650
  // Any extra live node that matched no pad → line it along the near edge.
4450
4651
  var ex=0;
4451
- list.forEach(function(n){ if(used[n.nodeId]) return; used[n.nodeId]=1;
4452
- var w=W2(0.20+ex*0.13, 0.90); ex++;
4453
- out.push({ id:n.nodeId, kind:kindOf(n), label:(n.displayName||n.nodeId), name:(n.displayName||n.nodeId),
4454
- sub:n.role||'', connected:!!n.connected, load:(n.load!=null?n.load:0), platform:n.platform||'', version:n.version||'?',
4455
- site:n.site||'', caps:n.capabilities||[], telemKeys:[(n.displayName||'').toLowerCase()], wx:w[0], wy:w[1], _hit:prevHit(n.nodeId) });
4456
- });
4652
+ list.forEach(function(n){ if(used[n.nodeId]) return; used[n.nodeId]=1;
4653
+ var w=W2(0.20+ex*0.13, 0.90); ex++;
4654
+ var extraNode={ id:n.nodeId, kind:kindOf(n), label:(n.displayName||n.nodeId), name:(n.displayName||n.nodeId),
4655
+ sub:n.role||'', connected:!!n.connected, load:(n.load!=null?n.load:0), platform:n.platform||'', version:n.version||'?',
4656
+ site:n.site||'', caps:n.capabilities||[], schedulerRobotId:n.schedulerRobotId||'', schedulerDeviceId:n.schedulerDeviceId||'', parkSlotId:n.parkSlotId||'',
4657
+ telemKeys:[(n.displayName||'').toLowerCase()].concat(n.telemKeys||[]), wx:w[0], wy:w[1], _hit:prevHit(n.nodeId) };
4658
+ if(extraNode.kind==='robot'){
4659
+ var extraTele=lookupTelemetry(extraNode), extraStatus=computeStatus(extraTele,extraNode);
4660
+ extraNode.telemetry=extraTele; extraNode.status=extraStatus.status; extraNode.readiness=extraStatus.readiness;
4661
+ extraNode.readinessMessage=extraStatus.readinessMessage; extraNode.sessionAge=extraStatus.sessionAge; extraNode.activityAge=extraStatus.activityAge;
4662
+ }
4663
+ out.push(extraNode);
4664
+ });
4457
4665
  return out;
4458
4666
  }
4459
4667
 
@@ -6796,7 +7004,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6796
7004
  // dashboard would only know about local (this-session) drags
6797
7005
  // and would reset to slot defaults on every reload.
6798
7006
  try{
6799
- getJson('/api/character-presets'+QS).then(function(list){
7007
+ getJson('/robopark/api/character-presets').then(function(list){
6800
7008
  if(Array.isArray(list)){
6801
7009
  for(var pi=0; pi<list.length; pi++){
6802
7010
  var p = list[pi];
@@ -6895,7 +7103,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
6895
7103
  micAnalyser=null;
6896
7104
  }
6897
7105
 
6898
- // ── setup flow for unjoined park pads ──
7106
+ // Resolve a Park pad to a real character preset. Mesh and scheduler node
7107
+ // ids are infrastructure identities and must never be emitted as presets.
7108
+ function resolvePadCharacterPreset(n){
7109
+ var device=schedulerDevice(n)||{},row=schedRow(n)||{};
7110
+ var candidates=[device.character_id,row.character_id,n.character_id,n.characterId,n.label,n.name,n.id];
7111
+ for(var ci=0;ci<candidates.length;ci++){
7112
+ var candidate=String(candidates[ci]||'').trim().toLowerCase();if(!candidate)continue;
7113
+ for(var pi=0;pi<characterPresets.length;pi++){
7114
+ var preset=characterPresets[pi],presetId=String(preset.id||'').trim(),presetName=String(preset.name||'').trim().toLowerCase();
7115
+ if(candidate===presetId.toLowerCase()||candidate===presetName)return presetId;
7116
+ }
7117
+ }
7118
+ return '';
7119
+ }
7120
+
7121
+ // ── setup flow for unjoined park pads ──
6899
7122
  function submitPadSetup(n){
6900
7123
  var name=$('pk-setup-name').value.trim() || (n.label||n.id||'robot').toString().toLowerCase();
6901
7124
  var body={
@@ -6912,7 +7135,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6912
7135
  $('pk-setup-token').value=token;
6913
7136
  $('pk-setup-result').style.display='block';
6914
7137
  var command=$('pk-setup-clicmd'),label=$('pk-setup-clicmd-label');
6915
- if(command)command.value=buildPadRobotCommand(name,body.character_id||n.id,token);
7138
+ if(command)command.value=buildPadRobotCommand(name,body.character_id,token);
6916
7139
  if(label)label.textContent='run this ON THE ROBOT (uses the token minted above)';
6917
7140
  showOut('robot added: '+esc((j&&j.name)||name)+'\\ntoken: '+esc(token));
6918
7141
  pollRoboparkQuiet();
@@ -7036,12 +7259,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
7036
7259
  if(visionRetryTimer){clearTimeout(visionRetryTimer);visionRetryTimer=null;}
7037
7260
  var img=$('pk-vision-img'); if(img){ img.src=''; img.style.display='none'; }
7038
7261
  }
7039
- function pollRoboparkQuiet(){
7040
- fetch('/robopark/api/telemetry'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
7041
- var list=(Array.isArray(d)?d:(d&&d.robots)||[]); roboparkByName={};
7042
- list.forEach(function(x){ roboparkByName[String(x.robot_id||x.id||x.name||'robot').toLowerCase()]=x; if(x.name) roboparkByName[String(x.name).toLowerCase()]=x; });
7043
- refreshDrawer();
7044
- }).catch(function(){});
7262
+ function pollRoboparkQuiet(){
7263
+ fetch('/robopark/api/telemetry'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
7264
+ var list=(Array.isArray(d)?d:(d&&d.robots)||[]);
7265
+ lastSchedulerPollAt=Date.now();
7266
+ lastRobots=list;
7267
+ indexRoboparkTelemetry(list);
7268
+ updateTelemetry(roboparkByName);
7269
+ refreshDrawer();
7270
+ }).catch(function(){});
7045
7271
  }
7046
7272
  function schedulerDevice(n){
7047
7273
  // Deliberately does not call schedRow: telemetry can contain a legacy
@@ -7438,11 +7664,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
7438
7664
  var SHELL_OUTPUT_LIMIT = 64 * 1024;
7439
7665
 
7440
7666
  function buildDrawer(n){
7441
- var b=$('pk-body');
7442
- if(n.kind==='robot' && !n.connected){
7443
- var defaultName=(n.label||n.id||'robot').toString().toLowerCase().replace(/\\s+/g,'');
7444
- var presetOptions='', voiceOptions='';
7445
- for(var i=0;i<characterPresets.length;i++){ var p=characterPresets[i]; presetOptions += '<option value="'+esc(p.id||'')+'"'+((p.id===n.id)?' selected':'')+'>'+esc(p.name||p.id||'')+'</option>'; }
7667
+ var b=$('pk-body');
7668
+ if(n.kind==='robot' && !n.connected){
7669
+ var defaultName=(n.label||n.id||'robot').toString().toLowerCase().replace(/\\s+/g,'');
7670
+ var defaultCharacter=resolvePadCharacterPreset(n);
7671
+ var presetOptions='', voiceOptions='';
7672
+ for(var i=0;i<characterPresets.length;i++){ var p=characterPresets[i]; presetOptions += '<option value="'+esc(p.id||'')+'"'+((p.id===defaultCharacter)?' selected':'')+'>'+esc(p.name||p.id||'')+'</option>'; }
7446
7673
  for(var v=0;v<voiceStacks.length;v++){ var s=voiceStacks[v]; voiceOptions += '<option value="'+esc(s.id||'')+'">'+esc(s.name||s.id||'')+'</option>'; }
7447
7674
  b.innerHTML =
7448
7675
  sect('🅿 park pad · '+esc(n.label),
@@ -7468,9 +7695,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
7468
7695
  + '</div>'
7469
7696
  + '<div class="pk-sect" id="pk-setup-result" style="display:none;"><h4>Enrollment token (copy once)</h4><input id="pk-setup-token" readonly style="margin-bottom:0.4rem;"><button class="rp-btn" id="pk-setup-copy">Copy</button></div>');
7470
7697
  var setupBtn=$('pk-setup-btn'); if(setupBtn) setupBtn.addEventListener('click', function(){ submitPadSetup(n); });
7471
- 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'); } });
7698
+ 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'); } });
7472
7699
  function refreshPadCliCommand(){
7473
- var nm=$('pk-setup-name').value.trim()||defaultName; var cid=$('pk-setup-char').value||n.id;
7700
+ var nm=$('pk-setup-name').value.trim()||defaultName; var cid=$('pk-setup-char').value||'';
7474
7701
  var el=$('pk-setup-clicmd'),label=$('pk-setup-clicmd-label');if(el)el.value=buildPadRobotCommand(nm,cid,'');if(label)label.textContent='or run this ON THE ROBOT (mints its own token)';
7475
7702
  }
7476
7703
  var padNameEl=$('pk-setup-name'); if(padNameEl) padNameEl.addEventListener('input', refreshPadCliCommand);
@@ -7520,7 +7747,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7520
7747
  + buildShellSection(schedId(n))
7521
7748
  + sect('🎭 character / voice','<div class="pk-note" id="pk-eff-note" style="margin-bottom:0.45rem;">loading effective config…</div><div class="rp-field"><label>Character preset</label><select class="rp-char" id="pk-eff-char" data-robot="'+esc(schedId(n))+'">'+effPresetOptions+'</select></div><div class="rp-field"><label>Voice stack</label><select class="rp-voice" id="pk-eff-voice" data-robot="'+esc(schedId(n))+'">'+effVoiceOptions+'</select></div><div class="rp-config-actions" style="margin-top:.45rem"><button class="rp-btn primary" id="pk-eff-save" data-robot="'+esc(schedId(n))+'">Apply to robot</button><button class="rp-btn" id="pk-edit-character">Edit character</button><button class="rp-btn" id="pk-edit-stack">Edit stack</button><button class="rp-btn" id="pk-new-stack">New stack</button></div>');
7522
7749
  b.innerHTML += sect('Microphone setup suite','<div class="pk-mic-suite"><div class="pk-mic-health" id="pk-mic-health"><div class="pk-mic-check"><div class="k">Hardware lock</div><div class="v">USB mic hw:3,0</div></div><div class="pk-mic-check"><div class="k">PCM capture</div><div class="v">not tested</div></div><div class="pk-mic-check"><div class="k">Production track</div><div class="v">waiting</div></div></div><div class="rp-form" style="grid-template-columns:1fr 1fr;gap:.4rem"><div class="rp-field"><label>Ground-truth capture</label><select id="pk-mic-duration"><option value="2">2 seconds</option><option value="4" selected>4 seconds</option><option value="8">8 seconds</option></select></div><div class="rp-field"><label>Locked input</label><div class="pk-note" style="margin:.45rem 0 0">USB microphone (plughw:3,0)</div></div></div><div class="pk-ctl"><button class="rp-btn primary" id="pk-mic-groundtruth">Run raw mic capture</button><button class="rp-btn" id="pk-mic-roundtrip">Run speaker + mic test</button><button class="rp-btn" id="pk-mic-apply">Reapply production profile</button></div><div class="pk-note" id="pk-mic-test-state">Raw capture bypasses LiveKit and tests the exact ALSA path. Round-trip then verifies the physical speaker-to-mic path.</div><div id="pk-mic-test-result"></div></div>');
7523
- b.innerHTML += sect('Motor registry + sequence canvas','<div class="rp-field"><label>Robot-local motor server</label><input id="pk-motor-url" value="http://127.0.0.1:8001" placeholder="http://127.0.0.1:8001"></div><div class="pk-motor-registry" id="pk-motor-registry"></div><button class="rp-btn" id="pk-motor-add" style="margin-top:.4rem">Add registered motor</button><div class="rp-form" style="grid-template-columns:1fr 1fr;margin-top:.7rem"><div class="rp-field"><label>Saved sequence</label><select id="pk-motor-sequence"></select></div><div class="rp-field"><label>Sequence name</label><input id="pk-motor-sequence-name" placeholder="Greeting motion"></div></div><div class="pk-motor-canvas" id="pk-motor-canvas"></div><div class="pk-ctl"><button class="rp-btn" id="pk-motor-step-add">Add timed node</button><button class="rp-btn primary" id="pk-motor-save">Save motor profile</button><button class="rp-btn" id="pk-motor-run">Run selected sequence</button><button class="rp-btn danger" id="pk-motor-test-all">Test all registered GPIO</button></div><label class="pk-prodtoggle"><input type="checkbox" id="pk-motor-greeting"><span>Run this sequence automatically with the greeting pipeline</span></label><div class="pk-note">Full GPIO test pulses registered relays only: 300 ms on, 200 ms pause, one at a time.</div><div class="pk-motor-status" id="pk-motor-status">Loading motor profile...</div>');
7750
+ b.innerHTML += sect('Motor registry + sequences','<div class="pk-note">Safe workflow: discover wired relays, build a short sequence, save, then test. Unknown GPIO pins are never pulsed.</div><div class="rp-field"><label>Robot-local motor server</label><input id="pk-motor-url" value="http://127.0.0.1:8001" readonly></div><div class="pk-ctl"><button class="rp-btn primary" id="pk-motor-discover">Discover wired relays</button><button class="rp-btn" id="pk-motor-add">Add relay manually</button></div><div class="pk-motor-registry" id="pk-motor-registry"></div><div class="rp-form" style="grid-template-columns:1fr 1fr;margin-top:.7rem"><div class="rp-field"><label>Saved sequence</label><select id="pk-motor-sequence"></select></div><div class="rp-field"><label>Sequence name</label><input id="pk-motor-sequence-name" placeholder="Greeting motion"></div></div><div class="pk-ctl"><button class="rp-btn" id="pk-motor-duplicate">Duplicate sequence</button><button class="rp-btn danger" id="pk-motor-delete">Delete sequence</button></div><div class="pk-motor-canvas" id="pk-motor-canvas"></div><div class="pk-ctl"><button class="rp-btn" id="pk-motor-step-add">+ Add pulse</button><button class="rp-btn primary" id="pk-motor-save">Save</button><button class="rp-btn" id="pk-motor-run">Run sequence</button><button class="rp-btn danger" id="pk-motor-stop">STOP ALL</button><button class="rp-btn danger" id="pk-motor-test-all">Test every registered relay</button></div><label class="pk-prodtoggle"><input type="checkbox" id="pk-motor-greeting"><span>Run this sequence with the greeting</span></label><div class="pk-note">Safety limits: registered BCM 2-27 only, one relay at a time, maximum 60 seconds per sequence.</div><div class="pk-motor-status" id="pk-motor-status">Loading motor profile...</div>');
7524
7751
  b.innerHTML += sect('Production camera + audio','<div class="pk-note" id="pk-device-search" style="margin-bottom:.55rem">Hardware locked for launch. Camera, microphone, and speaker cannot drift between restarts.</div><div class="rp-field"><label>Camera input</label><select id="pk-video-device" disabled></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input" disabled></select></div><div class="rp-field"><label>Speaker output</label><select id="pk-audio-output" disabled></select></div><button class="rp-btn primary" id="pk-device-test-speaker" style="margin-top:0.35rem">Test USB speaker now</button><div class="rp-field"><label>Preset greeting phrases <span class="pk-note">one per line, cached per voice</span></label><textarea id="pk-greeting-phrases" rows="3" placeholder="Hi, I am Jaguar! / Welcome to RoboPark."></textarea></div><button class="rp-btn" id="pk-device-save" style="margin-top:0.35rem">Save greetings + enforce hardware</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
7525
7752
  // Put live hardware selection beside the camera and audio tests,
7526
7753
  // ahead of persona and service administration in the Park drawer.
@@ -7547,13 +7774,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
7547
7774
  var micRound=$('pk-mic-roundtrip');if(micRound)micRound.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMicRoundtrip(n);});
7548
7775
  var micApply=$('pk-mic-apply');if(micApply)micApply.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveDeviceSelectors(n);});
7549
7776
  var motorAdd=$('pk-motor-add');if(motorAdd)motorAdd.addEventListener('click',function(){motorEditorAddMotor();});
7777
+ var motorDiscover=$('pk-motor-discover');if(motorDiscover)motorDiscover.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}discoverMotorRelays(n,false);});
7550
7778
  var motorStep=$('pk-motor-step-add');if(motorStep)motorStep.addEventListener('click',function(){motorEditorAddStep();});
7779
+ var motorDuplicate=$('pk-motor-duplicate');if(motorDuplicate)motorDuplicate.addEventListener('click',function(){duplicateMotorSequence();});
7780
+ var motorDelete=$('pk-motor-delete');if(motorDelete)motorDelete.addEventListener('click',function(){deleteMotorSequence();});
7551
7781
  var motorSave=$('pk-motor-save');if(motorSave)motorSave.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveMotorProfile(n);});
7552
7782
  var motorRun=$('pk-motor-run');if(motorRun)motorRun.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMotorSequence(n);});
7553
7783
  var motorTestAll=$('pk-motor-test-all');if(motorTestAll)motorTestAll.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}testAllMotorRelays(n);});
7784
+ var motorStop=$('pk-motor-stop');if(motorStop)motorStop.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}stopAllMotors(n);});
7554
7785
  var motorSeq=$('pk-motor-sequence');if(motorSeq)motorSeq.addEventListener('change',function(){selectMotorSequence(motorSeq.value);});
7555
7786
  var motorRegistry=$('pk-motor-registry');if(motorRegistry)motorRegistry.addEventListener('click',function(ev){var button=ev.target.closest('[data-motor-action]');if(!button)return;var index=Number(button.dataset.index);if(button.dataset.motorAction==='remove'){motorProfile.registry.splice(index,1);renderMotorEditor();}else if(button.dataset.motorAction==='test'){testMotorRelay(n,index);}});
7556
- var motorCanvas=$('pk-motor-canvas');if(motorCanvas)motorCanvas.addEventListener('click',function(ev){var button=ev.target.closest('[data-step-remove]');if(button){captureMotorEditor();motorEditorSequence().steps.splice(Number(button.dataset.stepRemove),1);renderMotorEditor();}});
7787
+ var motorCanvas=$('pk-motor-canvas');if(motorCanvas)motorCanvas.addEventListener('click',function(ev){var button=ev.target.closest('[data-step-remove],[data-step-move]');if(!button)return;captureMotorEditor();var sequence=motorEditorSequence(),index=Number(button.dataset.stepRemove!=null?button.dataset.stepRemove:button.dataset.stepIndex);if(button.dataset.stepRemove!=null)sequence.steps.splice(index,1);else{var direction=button.dataset.stepMove==='up'?-1:1,target=index+direction;if(target>=0&&target<sequence.steps.length){var item=sequence.steps.splice(index,1)[0];sequence.steps.splice(target,0,item);}}renderMotorEditor();});
7557
7788
  loadMotorProfile(n);
7558
7789
  startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
7559
7790
  _bindShellSection(n);
@@ -7632,7 +7863,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7632
7863
  var sequence=motorEditorSequence(),name=$('pk-motor-sequence-name'),greeting=$('pk-motor-greeting'),url=$('pk-motor-url');
7633
7864
  if(name)name.value=sequence.name||'';if(greeting)greeting.checked=motorProfile.greeting_sequence_id===sequence.id;if(url)url.value=motorProfile.motor_server_url||'http://127.0.0.1:8001';
7634
7865
  var motorOptions=(motorProfile.registry||[]).map(function(motor){return '<option value="'+esc(motor.id)+'">'+esc(motor.name+' | BCM '+motor.gpio)+'</option>';}).join('');
7635
- canvas.innerHTML=(sequence.steps||[]).map(function(step,index){return '<div class="pk-motor-node"><div class="pk-motor-node-num">'+(index+1)+'</div><label>Relay<select data-step-motor>'+motorOptions.replace('value="'+esc(step.motor_id)+'"','value="'+esc(step.motor_id)+'" selected')+'</select></label><label>Delay ms<input data-step-delay type="number" min="0" max="30000" step="50" value="'+esc(step.delay_ms||0)+'"></label><label>Pulse ms<input data-step-duration type="number" min="50" max="10000" step="50" value="'+esc(step.duration_ms||300)+'"></label><button class="rp-btn danger" data-step-remove="'+index+'">Remove</button></div>';}).join('')||'<div class="pk-note">No timed nodes. Add a node to build the sequence.</div>';
7866
+ canvas.innerHTML=(sequence.steps||[]).map(function(step,index){return '<div class="pk-motor-node"><div class="pk-motor-node-num">Step '+(index+1)+'</div><label>Relay<select data-step-motor>'+motorOptions.replace('value="'+esc(step.motor_id)+'"','value="'+esc(step.motor_id)+'" selected')+'</select></label><label>Wait before pulse (ms)<input data-step-delay type="number" min="0" max="30000" step="50" value="'+esc(step.delay_ms||0)+'"></label><label>Pulse length (ms)<input data-step-duration type="number" min="50" max="10000" step="50" value="'+esc(step.duration_ms||300)+'"></label><div class="pk-ctl"><button class="rp-btn" data-step-move="up" data-step-index="'+index+'" '+(index===0?'disabled':'')+'>Left</button><button class="rp-btn" data-step-move="down" data-step-index="'+index+'" '+(index===(sequence.steps.length-1)?'disabled':'')+'>Right</button><button class="rp-btn danger" data-step-remove="'+index+'">Remove</button></div></div>';}).join('')||'<div class="pk-note">No pulses yet. Click + Add pulse.</div>';
7636
7867
  }
7637
7868
  function selectMotorSequence(id){
7638
7869
  captureMotorEditor();
@@ -7641,6 +7872,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
7641
7872
  }
7642
7873
  function motorEditorAddMotor(){captureMotorEditor();var number=motorProfile.registry.length+1;motorProfile.registry.push({id:'motor-'+number,name:'Motor '+number,gpio:Math.min(27,Math.max(2,number+4)),active_high:true,max_duration_ms:3000});renderMotorEditor();}
7643
7874
  function motorEditorAddStep(){captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register a motor before adding a timed node','bad');return;}motorEditorSequence().steps.push({motor_id:motorProfile.registry[0].id,delay_ms:0,duration_ms:300});renderMotorEditor();}
7875
+ function duplicateMotorSequence(){captureMotorEditor();var source=motorEditorSequence(),copy=JSON.parse(JSON.stringify(source));copy.id=slugify(source.name+' copy')+'-'+Date.now();copy.name=(source.name||'Sequence')+' copy';motorProfile.sequences.push(copy);motorSequenceId=copy.id;renderMotorEditor();motorStatus('Sequence duplicated. Save to persist it.');}
7876
+ function deleteMotorSequence(){captureMotorEditor();if(motorProfile.sequences.length<=1){motorStatus('Keep at least one sequence. Clear its pulses instead.','bad');return;}if(!window.confirm('Delete this saved sequence?'))return;var index=motorProfile.sequences.findIndex(function(item){return item.id===motorSequenceId;});if(index<0)return;motorProfile.sequences.splice(index,1);if(motorProfile.greeting_sequence_id===motorSequenceId)motorProfile.greeting_sequence_id=null;motorSequenceId=motorProfile.sequences[0].id;renderMotorEditor();motorStatus('Sequence removed. Save to persist the change.');}
7877
+ function validateMotorProfile(requireRunnable){
7878
+ var ids={},gpios={},errors=[];
7879
+ (motorProfile.registry||[]).forEach(function(motor){if(!motor.id)errors.push('Every relay needs an id');if(ids[motor.id])errors.push('Duplicate relay id: '+motor.id);ids[motor.id]=true;if(!Number.isInteger(motor.gpio)||motor.gpio<2||motor.gpio>27)errors.push('GPIO for '+motor.name+' must be BCM 2-27');if(gpios[motor.gpio])errors.push('GPIO '+motor.gpio+' is assigned twice');gpios[motor.gpio]=true;});
7880
+ (motorProfile.sequences||[]).forEach(function(sequence){var total=0;(sequence.steps||[]).forEach(function(step){if(!ids[step.motor_id])errors.push(sequence.name+' uses an unknown relay');total+=Number(step.delay_ms||0)+Number(step.duration_ms||0);});if(total>60000)errors.push(sequence.name+' exceeds 60 seconds');});
7881
+ if(requireRunnable&&!motorEditorSequence().steps.length)errors.push('Add at least one pulse before running this sequence');
7882
+ if(errors.length){motorStatus(errors[0],'bad');throw new Error(errors[0]);}
7883
+ return true;
7884
+ }
7644
7885
  function motorStatus(message,kind){['pk-motor-status','pk-camera-ops-motor-status'].forEach(function(id){var host=$(id);if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}});}
7645
7886
  function motorErrorMessage(error){var message=error&&error.message?error.message:String(error||'unknown error');return /(?:\s|^)404(?:\s|$)|not found/i.test(message)?'Motor API is not loaded on the running livekit-1 scheduler. Update and restart the scheduler, then retry.':message;}
7646
7887
  function loadMotorProfile(n){
@@ -7649,13 +7890,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
7649
7890
  }
7650
7891
  function saveMotorProfile(n){
7651
7892
  captureMotorEditor();var robotId=schedId(n),url=motorProfile.motor_server_url;
7893
+ try{validateMotorProfile(false);}catch(error){return Promise.reject(error);}
7652
7894
  var parsedUrl=null;try{parsedUrl=new URL(url);}catch(e){}
7653
7895
  if(!parsedUrl||parsedUrl.protocol!=='http:'||(parsedUrl.hostname!=='127.0.0.1'&&parsedUrl.hostname!=='localhost')||!parsedUrl.port){motorStatus('Motor server must be robot-local: http://127.0.0.1:PORT','bad');return Promise.reject(new Error('motor server must be localhost'));}
7654
7896
  motorStatus('Validating and saving motor profile...');
7655
7897
  return rpAction('PATCH','/robopark/api/devices/'+encodeURIComponent(robotId),{motor_server_url:url}).then(function(){return rpAction('PUT','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile',{registry:motorProfile.registry,sequences:motorProfile.sequences,greeting_sequence_id:motorProfile.greeting_sequence_id});}).then(function(data){motorProfile.registry=data.registry||[];motorProfile.sequences=data.sequences||[];motorProfile.greeting_sequence_id=data.greeting_sequence_id||null;renderMotorEditor();motorStatus('Profile saved. Greeting motion will start on first TTS playback.','ok');return data;}).catch(function(error){motorStatus('Save failed: '+error.message,'bad');throw error;});
7656
7898
  }
7657
7899
  function runMotorSequence(n){
7658
- var robotId=schedId(n),sequence=motorEditorSequence();motorStatus('Saving and queueing '+sequence.name+'...');
7900
+ var robotId=schedId(n),sequence=motorEditorSequence();try{captureMotorEditor();validateMotorProfile(true);}catch(error){return;}motorStatus('Saving and queueing '+sequence.name+'...');
7659
7901
  saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-sequences/'+encodeURIComponent(sequence.id)+'/run',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-sequence');}).then(function(result){motorStatus(result.ok?'Sequence completed on robot.':'Sequence failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('Run failed: '+error.message,'bad');});
7660
7902
  }
7661
7903
  function testMotorRelay(n,index){
@@ -7667,9 +7909,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
7667
7909
  return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/discover',{}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-discover');}).then(function(result){
7668
7910
  if(!result.ok)throw new Error(result.error||'robot motor discovery failed');
7669
7911
  if(!result.registry||!result.registry.length)throw new Error('Robot motor server has no registered relays. Register the wired BCM pins once; unknown pins will not be pulsed.');
7670
- motorProfile.registry=result.registry;renderMotorEditor();motorStatus('Discovered '+result.registry.length+' robot-local relay(s); saving centrally...','ok');return saveMotorProfile(n);
7912
+ motorProfile.registry=result.registry;motorProfile.sequences=result.sequences||motorProfile.sequences;motorProfile.greeting_sequence_id=result.greeting_sequence_id||null;renderMotorEditor();motorStatus('Discovered and saved '+result.registry.length+' wired relay(s).','ok');return result;
7671
7913
  });
7672
7914
  }
7915
+ function stopAllMotors(n){
7916
+ var robotId=schedId(n);motorStatus('Sending all-off command...');
7917
+ return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/stop',{}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-stop');}).then(function(result){motorStatus(result.ok?'All registered relays are OFF.':'Stop failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('Stop failed: '+motorErrorMessage(error),'bad');});
7918
+ }
7673
7919
  function testAllMotorRelays(n){
7674
7920
  captureMotorEditor();var ready=motorProfile.registry.length?Promise.resolve():discoverMotorRelays(n,true);
7675
7921
  ready.then(function(){if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return null;var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');return saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');});}).catch(function(error){motorStatus('GPIO test failed: '+motorErrorMessage(error),'bad');});
@@ -7877,9 +8123,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
7877
8123
  var host=$('pk-camera-ops'),img=$('pk-camera-ops-img');if(host)host.classList.remove('open');document.body.classList.remove('camera-ops-open');if(cameraOpsRetryTimer){clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=null;}if(img){img.src='';img.dataset.live='false';}if(cameraOpsTimer){clearInterval(cameraOpsTimer);cameraOpsTimer=null;}cameraOpsNode=null;if(document.fullscreenElement&&document.exitFullscreen)document.exitFullscreen().catch(function(){});
7878
8124
  }
7879
8125
  (function bindCameraOperations(){
7880
- var close=$('pk-camera-ops-close'),refresh=$('pk-camera-ops-refresh'),nativeBtn=$('pk-camera-ops-native'),trigger=$('pk-camera-ops-trigger'),prod=$('pk-camera-ops-production'),end=$('pk-camera-ops-end'),motorDiscover=$('pk-camera-ops-motor-discover'),motorRun=$('pk-camera-ops-motor-run'),motorTest=$('pk-camera-ops-motor-test'),motorSelect=$('pk-camera-ops-motor-sequence');
8126
+ var close=$('pk-camera-ops-close'),refresh=$('pk-camera-ops-refresh'),nativeBtn=$('pk-camera-ops-native'),trigger=$('pk-camera-ops-trigger'),prod=$('pk-camera-ops-production'),end=$('pk-camera-ops-end'),motorDiscover=$('pk-camera-ops-motor-discover'),motorRun=$('pk-camera-ops-motor-run'),motorStop=$('pk-camera-ops-motor-stop'),motorTest=$('pk-camera-ops-motor-test'),motorSelect=$('pk-camera-ops-motor-sequence');
7881
8127
  if(close)close.addEventListener('click',closeCameraOperations);if(refresh)refresh.addEventListener('click',reloadCameraOperations);if(nativeBtn)nativeBtn.addEventListener('click',function(){var host=$('pk-camera-ops');if(host&&host.requestFullscreen)host.requestFullscreen().catch(function(e){showOut('fullscreen: '+e.message);});});if(trigger)trigger.addEventListener('click',function(){if(cameraOpsNode)triggerSession(schedId(cameraOpsNode));});if(prod)prod.addEventListener('click',function(){if(cameraOpsNode){var d=schedulerDevice(cameraOpsNode)||{};setProductionMode(cameraOpsNode,!d.production_mode);setTimeout(refreshCameraOperations,400);}});if(end)end.addEventListener('click',function(){if(cameraOpsNode)endSession(schedId(cameraOpsNode));});document.addEventListener('keydown',function(e){if(e.key==='Escape'&&cameraOpsNode)closeCameraOperations();});
7882
- if(motorDiscover)motorDiscover.addEventListener('click',function(){if(cameraOpsNode)discoverMotorRelays(cameraOpsNode,false).catch(function(error){motorStatus('Discovery failed: '+error.message,'bad');});});if(motorRun)motorRun.addEventListener('click',function(){if(cameraOpsNode)runMotorSequence(cameraOpsNode);});if(motorTest)motorTest.addEventListener('click',function(){if(cameraOpsNode)testAllMotorRelays(cameraOpsNode);});if(motorSelect)motorSelect.addEventListener('change',function(){motorSequenceId=motorSelect.value;renderMotorEditor();refreshCameraOperations();});
8128
+ if(motorDiscover)motorDiscover.addEventListener('click',function(){if(cameraOpsNode)discoverMotorRelays(cameraOpsNode,false).catch(function(error){motorStatus('Discovery failed: '+error.message,'bad');});});if(motorRun)motorRun.addEventListener('click',function(){if(cameraOpsNode)runMotorSequence(cameraOpsNode);});if(motorStop)motorStop.addEventListener('click',function(){if(cameraOpsNode)stopAllMotors(cameraOpsNode);});if(motorTest)motorTest.addEventListener('click',function(){if(cameraOpsNode)testAllMotorRelays(cameraOpsNode);});if(motorSelect)motorSelect.addEventListener('change',function(){motorSequenceId=motorSelect.value;renderMotorEditor();refreshCameraOperations();});
7883
8129
  })();
7884
8130
 
7885
8131
  // ── cam: target a LiveKit track; fall back to a styled preview ──
@@ -8266,12 +8512,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
8266
8512
  }
8267
8513
 
8268
8514
  $('pk-close') && $('pk-close').addEventListener('click', closeDrawer);
8269
- return { onStatus:onStatus, activate:activate, deactivate:deactivate, refreshDrawer:refreshDrawer, openDrawerForRobot:openDrawerForRobot, updateSlots:updateSlots, updateTelemetry:updateTelemetry };
8515
+ return { onStatus:onStatus, activate:activate, deactivate:deactivate, refreshDrawer:refreshDrawer, refreshTelemetry:pollRoboparkQuiet, openDrawerForRobot:openDrawerForRobot, updateSlots:updateSlots, updateTelemetry:updateTelemetry };
8270
8516
  })();
8271
8517
 
8272
8518
  poll();
8273
8519
  setInterval(poll, 2000);
8274
- setInterval(function(){ if (activeTab==='robopark') pollRobopark(); }, 3000);
8520
+ setInterval(function(){
8521
+ if(activeTab==='robopark') pollRobopark();
8522
+ else if(activeTab==='park' && Park && Park.refreshTelemetry) Park.refreshTelemetry();
8523
+ }, 3000);
8275
8524
  setInterval(loadParkSlots, 7000);
8276
8525
  // Pre-load character presets and voice stacks on startup so the Park
8277
8526
  // robot-detail drawer has populated dropdowns even if the user never visits the robopark tab.