infinicode 2.8.107 → 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,6 +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>
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>
828
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>
829
891
  </aside>
830
892
  </div>
@@ -1171,31 +1233,49 @@ button.act:disabled{opacity:0.5;cursor:default;}
1171
1233
  <div class="rp-sub" id="rp-sub-commands">
1172
1234
  <div class="rp-command-shell">
1173
1235
  <div class="rp-config-hero">
1174
- <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>
1175
- <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>
1176
1238
  </div>
1177
- <div class="rp-command-env">
1178
- <div class="rp-field"><label>Hub LAN IP</label><input id="rp-command-lan" value="192.168.0.9" spellcheck="false"></div>
1179
- <div class="rp-field"><label>Hub Tailscale IP / DNS</label><input id="rp-command-tail" value="100.84.147.24" spellcheck="false"></div>
1180
- <div class="rp-field"><label>Site</label><input id="rp-command-site" value="Tel Aviv"></div>
1181
- <div class="rp-field"><label>Robot name</label><input id="rp-command-robot" value="bmw" spellcheck="false"></div>
1182
- <div class="rp-field"><label>Enrollment token</label><input id="rp-command-enrollment" placeholder="paste robot enrollment token" spellcheck="false"></div>
1183
- <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>
1184
1250
  </div>
1185
- <div class="rp-command-toolbar">
1186
- <input type="search" id="rp-command-search" placeholder="Search setup, logs, camera, Docker, LiveKit, npm...">
1187
- <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>
1188
- <span class="rp-command-note" id="rp-command-count"></span>
1189
- </div>
1190
- <div class="rp-command-grid" id="rp-command-grid"></div>
1191
- <form class="rp-command-custom" id="rp-command-custom-form">
1192
- <h4>Add a persistent command</h4>
1193
- <div class="rp-field"><label>Title</label><input id="rp-command-custom-title" required placeholder="Restart camera service"></div>
1194
- <div class="rp-field"><label>Category</label><input id="rp-command-custom-category" value="Custom" placeholder="Custom"></div>
1195
- <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>
1196
- <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>
1197
- <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>
1198
- </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>
1199
1279
  </div>
1200
1280
  </div>
1201
1281
 
@@ -1263,9 +1343,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
1263
1343
  var lastSchedulerPollAt = 0; // updated by pollRobopark() on every successful /api/telemetry fetch
1264
1344
  var lastFedStatusAt = 0; // updated by poll() on every successful /fed/status fetch
1265
1345
  var lastStatus = null; // most recent /fed/status snapshot (shared with Park)
1266
- var roboparkByName = {}; // robot_id -> latest telemetry row (shared with Park drawer)
1346
+ var roboparkByName = {}; // robot_id -> latest telemetry row (shared with Park drawer)
1267
1347
  var schedulerDevicesById = {}; // enrolled device rows + camera/audio inventory
1268
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
+ }
1269
1365
  function schedulerDeviceScore(device){
1270
1366
  if(!device) return -1;
1271
1367
  var status=String(device.status||'').trim().toLowerCase();
@@ -1772,6 +1868,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1772
1868
  if(!id) return;
1773
1869
  var name = row.name || id;
1774
1870
  var device = deviceById[id] || deviceByName[norm(name)] || {};
1871
+ var characterId = device.character_id || row.character_id || row.character_preset_id || '';
1775
1872
  var status = String(device.status || row.status || '').toLowerCase();
1776
1873
  var heartbeat = device.last_heartbeat || row.last_heartbeat;
1777
1874
  var heartbeatAt = heartbeat ? Date.parse(heartbeat) : NaN;
@@ -1786,6 +1883,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1786
1883
  if(existing){
1787
1884
  existing.schedulerRobotId = id;
1788
1885
  existing.schedulerDeviceId = device.id || id;
1886
+ existing.parkSlotId = characterId || existing.parkSlotId || '';
1789
1887
  existing.displayName = existing.displayName || name;
1790
1888
  existing.connected = !!existing.connected || connected;
1791
1889
  existing.lastSeenAt = heartbeatAt && Number.isFinite(heartbeatAt) ? heartbeatAt : existing.lastSeenAt;
@@ -1796,6 +1894,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1796
1894
  nodeId: 'scheduler-robot-'+id,
1797
1895
  schedulerRobotId: id,
1798
1896
  schedulerDeviceId: device.id || id,
1897
+ parkSlotId: characterId,
1799
1898
  displayName: name,
1800
1899
  role: 'robot',
1801
1900
  platform: 'RoboPark robot',
@@ -1874,16 +1973,56 @@ button.act:disabled{opacity:0.5;cursor:default;}
1874
1973
  });
1875
1974
 
1876
1975
  // ── tabs: Fleet + Park + RoboPark sessions/telemetry ──
1877
- var tabs = document.querySelectorAll('.tab');
1878
- var activeTab = 'fleet';
1879
- for (var ti=0; ti<tabs.length; ti++){
1880
- (function(btn){
1881
- btn.addEventListener('click', function(){ selectTab(btn.getAttribute('data-tab')); });
1882
- })(tabs[ti]);
1883
- }
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
+ }
1884
2018
  function selectTab(name){
1885
- activeTab = name;
1886
- 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
+ }
1887
2026
  $('pane-fleet').classList.toggle('active', name==='fleet');
1888
2027
  $('pane-park').classList.toggle('active', name==='park');
1889
2028
  $('pane-robopark').classList.toggle('active', name==='robopark');
@@ -1895,6 +2034,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1895
2034
  initRpSubTabs();
1896
2035
  }
1897
2036
  if (name==='park') Park.activate(); else Park.deactivate();
2037
+ closeGlobalNavMobile();
1898
2038
  }
1899
2039
  var fleetParkBtn=$('fleet-open-park'); if(fleetParkBtn) fleetParkBtn.addEventListener('click',function(){selectTab('park');});
1900
2040
  var fleetSessionsBtn=$('fleet-open-sessions'); if(fleetSessionsBtn) fleetSessionsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('live');});
@@ -1905,6 +2045,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1905
2045
  function initTab(){
1906
2046
  if (initHash==='robopark'||initHash.indexOf('robopark/')===0) selectTab('robopark');
1907
2047
  else if(initHash==='park') selectTab('park');
2048
+ else selectTab('fleet');
1908
2049
  }
1909
2050
  // initTab() is called at the very bottom of this IIFE, after Park is assigned.
1910
2051
 
@@ -2167,15 +2308,26 @@ button.act:disabled{opacity:0.5;cursor:default;}
2167
2308
  if(Park && Park.updateSlots) Park.updateSlots(parkSlots);
2168
2309
  });
2169
2310
  }
2170
- function fillCharacterDropdowns(){
2311
+ function fillCharacterDropdowns(){
2171
2312
  var sels=document.querySelectorAll('.rp-char');
2172
2313
  for(var i=0;i<sels.length;i++){ var sel=sels[i]; var cur=sel.dataset.current||'';
2173
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]); }
2174
2315
  for(var j=0;j<characterPresets.length;j++){ var p=characterPresets[j];
2175
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);
2176
- }
2177
- }
2178
- }
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
+ }
2179
2331
  function fillVoiceStackDropdowns(){
2180
2332
  var sels=document.querySelectorAll('.rp-voice');
2181
2333
  for(var i=0;i<sels.length;i++){ var sel=sels[i]; var cur=sel.dataset.current||'';
@@ -2268,38 +2420,33 @@ button.act:disabled{opacity:0.5;cursor:default;}
2268
2420
  }
2269
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'; }
2270
2422
 
2271
- // ── robust CLI one-liner generation, live as you type no API call
2272
- // 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
+ }
2273
2429
  function buildAddRobotCommand(name, characterId){
2274
2430
  if(!name) return '';
2275
2431
  var network=$('rb-network')&&$('rb-network').value||'lan';
2276
- var commandEnv=rpCommandEnv();
2277
- var hubHost=network==='tailscale'?commandEnv.tail:commandEnv.lan;
2278
- var hubUrl=/^https?:\/\//i.test(hubHost)?hubHost.replace(/\\/+$/,''):'http://'+hubHost.replace(/\\/+$/,'')+':47913';
2279
- var schedulerUrl=hubUrl+'/robopark';
2280
- var argv=['robopark','add-robot',name];
2281
- if(characterId) argv.push('--character', characterId);
2432
+ var hubUrl=rpControlUrl(network);
2433
+ var argv=['sudo','robopark','setup','--robot','--name',name];
2282
2434
  var site=$('rb-site')&&$('rb-site').value.trim();if(site)argv.push('--site',site);
2283
2435
  if(hubUrl) argv.push('--hub-url', hubUrl);
2284
2436
  var meshToken=$('rb-token')&&$('rb-token').value.trim()||dashboardMeshToken()||'<mesh-token>';
2285
2437
  argv.push('--token', meshToken);
2286
- if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
2438
+ if(characterId)argv.push('--character',characterId);
2287
2439
  argv.push(network==='tailscale'?'--tailscale':'--lan','--start','--auto-start','--yes');
2288
- 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(' '));
2289
2441
  }
2290
2442
  function buildPadRobotCommand(name,characterId,enrollmentToken){
2291
- var env=rpCommandEnv(),hubHost=env.lan||'192.168.0.9';
2292
- var hubUrl=/^https?:\/\//i.test(hubHost)?hubHost.replace(/\\\/+$/,''):'http://'+hubHost.replace(/\\\/+$/,'')+':47913';
2443
+ var env=rpCommandEnv(),network=env.network||'lan',hubUrl=rpControlUrl(network);
2293
2444
  var meshToken=env.token||dashboardMeshToken()||'<mesh-token>',site=env.site||'Tel Aviv';
2294
- var argv;
2295
- if(enrollmentToken){
2296
- 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'];
2297
- }else{
2298
- argv=['robopark','add-robot',name];
2299
- if(characterId)argv.push('--character',characterId);
2300
- argv.push('--site',site,'--hub-url',hubUrl,'--token',meshToken,'--scheduler-url',hubUrl+'/robopark','--lan','--start','--auto-start','--yes');
2301
- }
2302
- 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(' '));
2303
2450
  }
2304
2451
  function refreshRobotCliCommand(){
2305
2452
  var name=$('rb-name').value.trim(); var cid=$('rb-char').value||'';
@@ -2394,7 +2541,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2394
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>'
2395
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>'
2396
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>'
2397
- +'<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>'
2398
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>';
2399
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);
2400
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);}); });
@@ -2571,8 +2718,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2571
2718
  lastSchedulerPollAt = Date.now();
2572
2719
  var grid = $('rp-grid'), totals = $('rp-totals');
2573
2720
  // stash per-robot rows so the Park drawer can show live session stats
2574
- roboparkByName = {};
2575
- robotsFrom(d).forEach(function(x){ roboparkByName[String(robotName(x)).toLowerCase()] = x; if(x.name) roboparkByName[String(x.name).toLowerCase()] = x; });
2721
+ indexRoboparkTelemetry(robotsFrom(d));
2576
2722
  // C4: also feed lastRobots for the Incidents sub-tab
2577
2723
  lastRobots = robotsFrom(d) || [];
2578
2724
  // push the same map into the Park so the canvas can color robots
@@ -2694,43 +2840,57 @@ button.act:disabled{opacity:0.5;cursor:default;}
2694
2840
 
2695
2841
  // ── Sub-tab routing ──
2696
2842
  // Command sheet: generate production commands from one saved environment.
2697
- var RP_COMMAND_ENV_KEY='rp-command-env-v1';
2843
+ var RP_COMMAND_ENV_KEY='rp-command-env-v2';
2698
2844
  var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
2699
2845
  var rpVisibleCommands=[];
2700
- 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'};}
2701
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;}}
2702
2848
  function rpCommandEnv(){
2703
2849
  var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2704
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();}
2705
- 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'};
2706
2852
  }
2707
2853
  function rpSaveCommandEnv(){try{localStorage.setItem(RP_COMMAND_ENV_KEY,JSON.stringify(rpCommandEnv()));}catch(e){}}
2708
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
+ }
2709
2861
  function rpCommandCatalog(){
2710
- 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';
2711
2864
  return [
2712
- {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'},
2713
- {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'},
2714
- {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'},
2715
- {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'},
2716
- {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"'},
2717
- {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+'"'},
2718
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'},
2719
- {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'},
2720
- {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'},
2721
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'},
2722
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+'"'},
2723
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'},
2724
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'},
2725
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'},
2726
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'},
2727
- {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()},
2728
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'},
2729
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}}"'},
2730
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"'},
2731
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'},
2732
- {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'},
2733
- {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'},
2734
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'}
2735
2895
  ];
2736
2896
  }
@@ -2738,6 +2898,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
2738
2898
  function rpSaveCustomCommands(list){try{localStorage.setItem(RP_CUSTOM_COMMANDS_KEY,JSON.stringify(list));}catch(e){showOut('Could not save custom command: '+e.message);}}
2739
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();}
2740
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
+ }
2741
2911
  function renderCommandSheet(){
2742
2912
  var grid=$('rp-command-grid');if(!grid)return;
2743
2913
  var search=String(($('rp-command-search')||{}).value||'').toLowerCase().trim(),category=String(($('rp-command-category')||{}).value||'').toLowerCase();
@@ -2748,25 +2918,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
2748
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('');
2749
2919
  }
2750
2920
  function initCommandSheet(){
2751
- var fields=['lan','tail','site','robot','enrollment','token'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2752
- 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();});});
2753
- 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');
2754
2924
  if(search)search.addEventListener('input',renderCommandSheet);if(category)category.addEventListener('change',renderCommandSheet);
2755
- 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);});
2756
2927
  if(copyAll)copyAll.addEventListener('click',function(){rpCopyCommand(rpVisibleCommands.map(function(x){return '# '+x.title+'\\n'+x.command;}).join('\\n\\n'));});
2757
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');}});
2758
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');});
2759
- renderCommandSheet();
2930
+ renderOneMinuteSetup();renderCommandSheet();
2760
2931
  }
2761
2932
  initCommandSheet();
2762
2933
 
2763
2934
  var rpSubTab = 'overview';
2764
2935
  var rpIntervals = {}; // sub-tab name -> setInterval handle
2765
2936
  var rpHistoryAll = []; // full recent-session list (we paginate client-side)
2766
- function setRpSubTab(name){
2767
- if(!name) return;
2768
- rpSubTab = name;
2769
- 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');
2770
2947
  for(var i=0;i<btns.length;i++){
2771
2948
  var b=btns[i], sub=b.getAttribute('data-sub');
2772
2949
  var isActive = sub===name;
@@ -2791,7 +2968,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2791
2968
  if(name==='history'){ rpHistoryPage=0; pollHistory(); }
2792
2969
  if(name==='audit'){ pollAuditLog(); rpIntervals.audit = setInterval(pollAuditLog, 6000); }
2793
2970
  if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
2794
- if(name==='commands'){ renderCommandSheet(); }
2971
+ if(name==='commands'){ renderOneMinuteSetup(); renderCommandSheet(); }
2795
2972
  if(name==='setup'){ pollSettings(); loadCharacterPresets(); loadVoiceStacks(); pollRuntimeHealth(); }
2796
2973
  // remember last sub-tab in URL hash
2797
2974
  try{ history.replaceState(null,'','#robopark/'+name); }catch(e){}
@@ -3055,14 +3232,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
3055
3232
  if(!filtered.length){ tbody.innerHTML=''; wrap.style.display='none'; empty.style.display='block'; }
3056
3233
  else { empty.style.display='none'; wrap.style.display='block'; }
3057
3234
  var html='';
3058
- for(var i=0;i<filtered.length;i++){
3059
- var s = filtered[i];
3060
- var rid = s.robot_id || '';
3061
- 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 || '';
3062
3242
  var started = s.started_at ? Date.parse(s.started_at) : null;
3063
3243
  var elapsed = started ? Math.floor((Date.now()-started)/1000) : null;
3064
3244
  html += '<tr>'
3065
- + '<td><b>'+esc(rid)+'</b></td>'
3245
+ + '<td><b>'+esc(robotLabel)+'</b><span class="t-abs">'+esc(rid)+(characterLabel?(' · '+esc(characterLabel)):'')+'</span></td>'
3066
3246
  + '<td><span class="sid" title="'+esc(sid)+'">'+esc(sid)+'</span></td>'
3067
3247
  + '<td><span class="t-rel"><span class="live-dot"></span>'+esc(rpFmtRelative(started))+'</span><span class="t-abs">'+esc(rpFmtAbs(started))+'</span></td>'
3068
3248
  + '<td data-started="'+started+'">'+esc(rpFmtDuration(elapsed))+'</td>'
@@ -3098,7 +3278,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
3098
3278
  if(!b) return;
3099
3279
  var act = b.getAttribute('data-act');
3100
3280
  if(act==='end-session'){ endSessionFromTable(b.getAttribute('data-sid'), b.getAttribute('data-robot')); }
3101
- 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); } }
3102
3282
  });
3103
3283
  })();
3104
3284
 
@@ -4319,13 +4499,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
4319
4499
  // looks up each robot node by its slot aliases and attaches a
4320
4500
  // status + telemetry field used by drawRobot() to drive the
4321
4501
  // status animations.
4322
- function updateTelemetry(map){
4323
- _telemetryByName = map || {};
4324
- }
4325
- function lookupTelemetry(node){
4326
- 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;
4327
4508
  // The hub is never a per-robot telemetry row — only robot slots are.
4328
- 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
+ }
4329
4515
  // Try every alias the slot was built with: slot.label, slot.id,
4330
4516
  // slot.sub, slot.keys, and the matched mesh node's displayName.
4331
4517
  var slot = null;
@@ -4401,11 +4587,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
4401
4587
  }
4402
4588
 
4403
4589
  function layout(src){
4404
- var list=(src||[]).slice(), used={}, out=[];
4405
- function prevHit(id){ for(var p=0;p<nodes.length;p++){ if(nodes[p].id===id) return nodes[p]._hit; } return null; }
4406
- function bind(slot, wx, wy, kind){
4407
- var match=null;
4408
- 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;
4409
4606
  var nm=(n.displayName||n.nodeId||'').toLowerCase(), isHub=kindOf(n)==='hub';
4410
4607
  if(kind==='hub'){ if(isHub){ match=n; break; } }
4411
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; }
@@ -4420,9 +4617,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
4420
4617
  load: (match&&match.load!=null)?match.load:0,
4421
4618
  platform: match?(match.platform||''):'', version: match?(match.version||'?'):'—',
4422
4619
  site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
4423
- schedulerRobotId: match?(match.schedulerRobotId||match.schedulerDeviceId||''):'',
4424
- schedulerDeviceId: match?(match.schedulerDeviceId||match.schedulerRobotId||''):'',
4425
- 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;}),
4426
4628
  // slotId is the stable handle we use to persist operator-
4427
4629
  // chosen positions. slotNx/slotNy are the preset's default
4428
4630
  // coordinates so the user can always reset back.
@@ -4447,12 +4649,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
4447
4649
  SLOTS.forEach(function(s){ var w=W2(s.nx,s.ny); out.push(bind(s,w[0],w[1],'robot')); });
4448
4650
  // Any extra live node that matched no pad → line it along the near edge.
4449
4651
  var ex=0;
4450
- list.forEach(function(n){ if(used[n.nodeId]) return; used[n.nodeId]=1;
4451
- var w=W2(0.20+ex*0.13, 0.90); ex++;
4452
- out.push({ id:n.nodeId, kind:kindOf(n), label:(n.displayName||n.nodeId), name:(n.displayName||n.nodeId),
4453
- sub:n.role||'', connected:!!n.connected, load:(n.load!=null?n.load:0), platform:n.platform||'', version:n.version||'?',
4454
- site:n.site||'', caps:n.capabilities||[], telemKeys:[(n.displayName||'').toLowerCase()], wx:w[0], wy:w[1], _hit:prevHit(n.nodeId) });
4455
- });
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
+ });
4456
4665
  return out;
4457
4666
  }
4458
4667
 
@@ -6795,7 +7004,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6795
7004
  // dashboard would only know about local (this-session) drags
6796
7005
  // and would reset to slot defaults on every reload.
6797
7006
  try{
6798
- getJson('/api/character-presets'+QS).then(function(list){
7007
+ getJson('/robopark/api/character-presets').then(function(list){
6799
7008
  if(Array.isArray(list)){
6800
7009
  for(var pi=0; pi<list.length; pi++){
6801
7010
  var p = list[pi];
@@ -6894,7 +7103,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
6894
7103
  micAnalyser=null;
6895
7104
  }
6896
7105
 
6897
- // ── 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 ──
6898
7122
  function submitPadSetup(n){
6899
7123
  var name=$('pk-setup-name').value.trim() || (n.label||n.id||'robot').toString().toLowerCase();
6900
7124
  var body={
@@ -6911,7 +7135,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6911
7135
  $('pk-setup-token').value=token;
6912
7136
  $('pk-setup-result').style.display='block';
6913
7137
  var command=$('pk-setup-clicmd'),label=$('pk-setup-clicmd-label');
6914
- if(command)command.value=buildPadRobotCommand(name,body.character_id||n.id,token);
7138
+ if(command)command.value=buildPadRobotCommand(name,body.character_id,token);
6915
7139
  if(label)label.textContent='run this ON THE ROBOT (uses the token minted above)';
6916
7140
  showOut('robot added: '+esc((j&&j.name)||name)+'\\ntoken: '+esc(token));
6917
7141
  pollRoboparkQuiet();
@@ -7035,12 +7259,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
7035
7259
  if(visionRetryTimer){clearTimeout(visionRetryTimer);visionRetryTimer=null;}
7036
7260
  var img=$('pk-vision-img'); if(img){ img.src=''; img.style.display='none'; }
7037
7261
  }
7038
- function pollRoboparkQuiet(){
7039
- fetch('/robopark/api/telemetry'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
7040
- var list=(Array.isArray(d)?d:(d&&d.robots)||[]); roboparkByName={};
7041
- 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; });
7042
- refreshDrawer();
7043
- }).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(){});
7044
7271
  }
7045
7272
  function schedulerDevice(n){
7046
7273
  // Deliberately does not call schedRow: telemetry can contain a legacy
@@ -7437,11 +7664,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
7437
7664
  var SHELL_OUTPUT_LIMIT = 64 * 1024;
7438
7665
 
7439
7666
  function buildDrawer(n){
7440
- var b=$('pk-body');
7441
- if(n.kind==='robot' && !n.connected){
7442
- var defaultName=(n.label||n.id||'robot').toString().toLowerCase().replace(/\\s+/g,'');
7443
- var presetOptions='', voiceOptions='';
7444
- 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>'; }
7445
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>'; }
7446
7674
  b.innerHTML =
7447
7675
  sect('🅿 park pad · '+esc(n.label),
@@ -7467,9 +7695,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
7467
7695
  + '</div>'
7468
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>');
7469
7697
  var setupBtn=$('pk-setup-btn'); if(setupBtn) setupBtn.addEventListener('click', function(){ submitPadSetup(n); });
7470
- 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'); } });
7471
7699
  function refreshPadCliCommand(){
7472
- 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||'';
7473
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)';
7474
7702
  }
7475
7703
  var padNameEl=$('pk-setup-name'); if(padNameEl) padNameEl.addEventListener('input', refreshPadCliCommand);
@@ -7519,7 +7747,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7519
7747
  + buildShellSection(schedId(n))
7520
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>');
7521
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>');
7522
- 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>');
7523
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>');
7524
7752
  // Put live hardware selection beside the camera and audio tests,
7525
7753
  // ahead of persona and service administration in the Park drawer.
@@ -7546,13 +7774,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
7546
7774
  var micRound=$('pk-mic-roundtrip');if(micRound)micRound.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMicRoundtrip(n);});
7547
7775
  var micApply=$('pk-mic-apply');if(micApply)micApply.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveDeviceSelectors(n);});
7548
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);});
7549
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();});
7550
7781
  var motorSave=$('pk-motor-save');if(motorSave)motorSave.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}saveMotorProfile(n);});
7551
7782
  var motorRun=$('pk-motor-run');if(motorRun)motorRun.addEventListener('click',function(){if(rpReadOnly){showOut('read-only node');return;}runMotorSequence(n);});
7552
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);});
7553
7785
  var motorSeq=$('pk-motor-sequence');if(motorSeq)motorSeq.addEventListener('change',function(){selectMotorSequence(motorSeq.value);});
7554
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);}});
7555
- 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();});
7556
7788
  loadMotorProfile(n);
7557
7789
  startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
7558
7790
  _bindShellSection(n);
@@ -7631,7 +7863,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7631
7863
  var sequence=motorEditorSequence(),name=$('pk-motor-sequence-name'),greeting=$('pk-motor-greeting'),url=$('pk-motor-url');
7632
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';
7633
7865
  var motorOptions=(motorProfile.registry||[]).map(function(motor){return '<option value="'+esc(motor.id)+'">'+esc(motor.name+' | BCM '+motor.gpio)+'</option>';}).join('');
7634
- 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>';
7635
7867
  }
7636
7868
  function selectMotorSequence(id){
7637
7869
  captureMotorEditor();
@@ -7640,20 +7872,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
7640
7872
  }
7641
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();}
7642
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();}
7643
- function motorStatus(message,kind){var host=$('pk-motor-status');if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}}
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
+ }
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||'');}});}
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;}
7644
7887
  function loadMotorProfile(n){
7645
7888
  var robotId=schedId(n);motorStatus('Loading saved motor profile...');
7646
- getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||data.motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();if(motorProfile.registry.length){motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');return;}motorStatus('No central registry; discovering robot-local relays...');return discoverMotorRelays(n,true);}).catch(function(error){motorStatus('Motor profile unavailable: '+error.message,'bad');});
7889
+ getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||data.motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();if(motorProfile.registry.length){motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');return;}motorStatus('No central registry; discovering robot-local relays...');return discoverMotorRelays(n,true);}).catch(function(error){motorStatus('Motor profile unavailable: '+motorErrorMessage(error),'bad');});
7647
7890
  }
7648
7891
  function saveMotorProfile(n){
7649
7892
  captureMotorEditor();var robotId=schedId(n),url=motorProfile.motor_server_url;
7893
+ try{validateMotorProfile(false);}catch(error){return Promise.reject(error);}
7650
7894
  var parsedUrl=null;try{parsedUrl=new URL(url);}catch(e){}
7651
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'));}
7652
7896
  motorStatus('Validating and saving motor profile...');
7653
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;});
7654
7898
  }
7655
7899
  function runMotorSequence(n){
7656
- 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+'...');
7657
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');});
7658
7902
  }
7659
7903
  function testMotorRelay(n,index){
@@ -7665,12 +7909,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
7665
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){
7666
7910
  if(!result.ok)throw new Error(result.error||'robot motor discovery failed');
7667
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.');
7668
- 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;
7669
7913
  });
7670
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
+ }
7671
7919
  function testAllMotorRelays(n){
7672
7920
  captureMotorEditor();var ready=motorProfile.registry.length?Promise.resolve():discoverMotorRelays(n,true);
7673
- 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: '+error.message,'bad');});
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');});
7674
7922
  }
7675
7923
 
7676
7924
  var pipelineHistoryPage=1;
@@ -7862,20 +8110,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
7862
8110
  var stages=[[currentStageLabel,true,'ACTIVE'],['Robot online',!!n.connected,'ready'],['Camera streaming',cameraLive,'ready'],['Motion detector armed',!!device.production_mode,'armed'],['Scheduler session',active,'active']],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s,index){return '<div class="'+(index===0?'fleet-stage-focus '+esc(currentStageStatus):'')+'"><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?s[2]:'waiting')+'</b></div>';}).join('');
7863
8111
  var inv=device.device_inventory||{},mh=inv.media_health||{},hardware=$('pk-camera-ops-hardware');if(hardware)hardware.innerHTML='<span>CPU</span><b>'+(hw.cpu&&hw.cpu.percent!=null?Math.round(hw.cpu.percent)+'%':'--')+'</b><span>Memory</span><b>'+(hw.memory&&hw.memory.percent!=null?Math.round(hw.memory.percent)+'%':'--')+'</b><span>Temperature</span><b>'+(hw.temperatureC!=null?Math.round(hw.temperatureC)+'C':'--')+'</b><span>Camera inputs</span><b>'+((inv.video||[]).length||0)+'</b><span>Microphones</span><b>'+((inv.audio_input||[]).length||0)+'</b><span>Speaker outputs</span><b>'+((inv.audio_output||[]).length||0)+'</b><span>Camera permission</span><b class="'+(mh.camera_access===false?'bad':'ok')+'">'+(mh.camera_access===false?'DENIED':mh.camera_access===true?'granted':'--')+'</b><span>Audio permission</span><b class="'+(mh.audio_access===false?'bad':'ok')+'">'+(mh.audio_access===false?'DENIED':mh.audio_access===true?'granted':'--')+'</b><span>Camera worker</span><b class="'+(mh.camera_stalled?'bad':'ok')+'">'+(mh.camera_stalled?'STALLED':mh.camera_worker?'streaming':'--')+'</b><span>Frame age</span><b>'+(mh.camera_frame_age_seconds!=null?Number(mh.camera_frame_age_seconds).toFixed(1)+'s':'--')+'</b><span>Selected camera</span><b>'+esc(device.video_device||'auto')+'</b><span>Selected microphone</span><b>'+esc(device.audio_device||'default')+'</b>';
7864
8112
  var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
8113
+ var motorSelect=$('pk-camera-ops-motor-sequence');if(motorSelect){var selected=motorSelect.value||motorSequenceId;motorSelect.innerHTML=(motorProfile.sequences||[]).map(function(sequence){return '<option value="'+esc(sequence.id)+'">'+esc(sequence.name||sequence.id)+'</option>';}).join('')||'<option value="">No saved sequences</option>';if(selected)motorSelect.value=selected;}
7865
8114
  }
7866
8115
  function reloadCameraOperations(){
7867
8116
  if(!cameraOpsNode)return;var img=$('pk-camera-ops-img'),empty=$('pk-camera-ops-empty');if(!img)return;img.dataset.live='false';if(empty){empty.style.display='block';empty.textContent='opening RoboVision camera...';}
7868
8117
  img.onload=function(){img.dataset.live='true';if(cameraOpsRetryTimer){clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=null;}if(empty)empty.style.display='none';refreshCameraOperations();};img.onerror=function(){var failedNode=cameraOpsNode;img.dataset.live='false';if(empty){empty.style.display='block';empty.textContent='camera relay unavailable / retrying';}refreshCameraOperations();if(cameraOpsRetryTimer)clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=setTimeout(function(){if(cameraOpsNode&&cameraOpsNode===failedNode)reloadCameraOperations();},3000);};img.src=cameraOpsFeedUrl(cameraOpsNode);refreshCameraOperations();
7869
8118
  }
7870
8119
  function openCameraOperations(n){
7871
- cameraOpsNode=n;var host=$('pk-camera-ops');if(!host)return;host.classList.add('open');document.body.classList.add('camera-ops-open');var name=n.displayName||n.name||'robot',avatar=rpAvatarFor(name),av=$('pk-camera-ops-avatar');$('pk-camera-ops-name').textContent=name+' / live operations';$('pk-camera-ops-sub').textContent='RoboVision mesh relay / '+(n.connected?'connected':'offline');if(av){if(avatar){av.src=avatar+QS;av.style.display='block';}else{av.removeAttribute('src');av.style.display='none';}}reloadCameraOperations();if(cameraOpsTimer)clearInterval(cameraOpsTimer);cameraOpsTimer=setInterval(refreshCameraOperations,1000);
8120
+ cameraOpsNode=n;var host=$('pk-camera-ops');if(!host)return;host.classList.add('open');document.body.classList.add('camera-ops-open');var name=n.displayName||n.name||'robot',avatar=rpAvatarFor(name),av=$('pk-camera-ops-avatar');$('pk-camera-ops-name').textContent=name+' / live operations';$('pk-camera-ops-sub').textContent='RoboVision mesh relay / '+(n.connected?'connected':'offline');if(av){if(avatar){av.src=avatar+QS;av.style.display='block';}else{av.removeAttribute('src');av.style.display='none';}}loadMotorProfile(n);reloadCameraOperations();if(cameraOpsTimer)clearInterval(cameraOpsTimer);cameraOpsTimer=setInterval(refreshCameraOperations,1000);
7872
8121
  }
7873
8122
  function closeCameraOperations(){
7874
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(){});
7875
8124
  }
7876
8125
  (function bindCameraOperations(){
7877
- 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');
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');
7878
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();});
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();});
7879
8129
  })();
7880
8130
 
7881
8131
  // ── cam: target a LiveKit track; fall back to a styled preview ──
@@ -8262,12 +8512,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
8262
8512
  }
8263
8513
 
8264
8514
  $('pk-close') && $('pk-close').addEventListener('click', closeDrawer);
8265
- 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 };
8266
8516
  })();
8267
8517
 
8268
8518
  poll();
8269
8519
  setInterval(poll, 2000);
8270
- 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);
8271
8524
  setInterval(loadParkSlots, 7000);
8272
8525
  // Pre-load character presets and voice stacks on startup so the Park
8273
8526
  // robot-detail drawer has populated dropdowns even if the user never visits the robopark tab.