infinicode 2.8.69 → 2.8.71

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.
@@ -132,8 +132,56 @@ section h2 .count{color:var(--muted);}
132
132
  .tbadge{font-family:var(--mono);font-size:0.6rem;padding:0.08em 0.45em;border-radius:5px;border:1px solid var(--glass-border);color:var(--muted);}
133
133
  .tbadge.ok{color:var(--ok);} .tbadge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 40%,transparent);}
134
134
  .tbadge.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent);background:var(--bad-bg);}
135
- .chip.alert{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent);background:var(--bad-bg);}
136
- .chip.alert b{color:var(--bad);}
135
+ .chip.alert{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent);background:var(--bad-bg);}
136
+ .chip.alert b{color:var(--bad);}
137
+
138
+ /* fleet operations overview */
139
+ .fleet-ops{display:flex;flex-direction:column;gap:.8rem;margin-bottom:1rem;}
140
+ .fleet-hero{position:relative;overflow:hidden;display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;padding:1.05rem 1.15rem;border:1px solid var(--glass-border);border-radius:18px;background:radial-gradient(520px 170px at 5% 0%,rgba(246,215,123,.15),transparent 70%),linear-gradient(145deg,rgba(29,24,14,.8),rgba(7,6,4,.72));box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi);}
141
+ .fleet-hero:after{content:"";position:absolute;width:180px;height:180px;right:9%;top:-120px;border:1px solid rgba(246,215,123,.13);border-radius:50%;box-shadow:0 0 0 30px rgba(246,215,123,.025),0 0 0 62px rgba(246,215,123,.015);}
142
+ .fleet-hero .eyebrow{font-family:var(--mono);font-size:.56rem;letter-spacing:.17em;text-transform:uppercase;color:var(--gold);}
143
+ .fleet-hero h1{font-family:var(--display);font-size:clamp(1.45rem,2.5vw,2.25rem);line-height:1.05;color:var(--gold-pale);font-weight:500;margin:.18rem 0;}
144
+ .fleet-hero p{font-size:.72rem;color:var(--muted);max-width:62ch;}
145
+ .fleet-hero-actions{display:flex;gap:.45rem;flex-wrap:wrap;justify-content:flex-end;position:relative;z-index:1;}
146
+ .fleet-kpis{display:grid;grid-template-columns:repeat(6,minmax(105px,1fr));gap:.55rem;}
147
+ .fleet-kpi{padding:.75rem .8rem;border:1px solid var(--glass-border);border-radius:14px;background:linear-gradient(150deg,rgba(34,28,16,.58),rgba(8,7,5,.64));box-shadow:inset 0 1px 0 var(--glass-hi);min-width:0;}
148
+ .fleet-kpi .label{font-family:var(--mono);font-size:.5rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
149
+ .fleet-kpi .value{font-family:var(--display);font-size:1.55rem;line-height:1.15;color:var(--gold-pale);font-variant-numeric:tabular-nums;}
150
+ .fleet-kpi .detail{font-family:var(--mono);font-size:.52rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
151
+ .fleet-kpi.ok .value{color:var(--ok)}.fleet-kpi.warn .value{color:var(--warn)}.fleet-kpi.bad .value{color:var(--bad)}
152
+ .fleet-insights{display:grid;grid-template-columns:1.05fr 1.35fr 1.25fr;gap:.65rem;}
153
+ .fleet-panel{min-width:0;border:1px solid var(--glass-border);border-radius:16px;background:rgba(12,10,7,.68);box-shadow:inset 0 1px 0 var(--glass-hi);padding:.8rem;}
154
+ .fleet-panel-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding-bottom:.55rem;margin-bottom:.55rem;border-bottom:1px solid var(--glass-border);}
155
+ .fleet-panel-head b{font-family:var(--display);font-size:.92rem;color:var(--gold-pale);font-weight:500}.fleet-panel-head span{font-family:var(--mono);font-size:.5rem;color:var(--muted)}
156
+ .fleet-readiness{display:grid;grid-template-columns:104px 1fr;gap:.75rem;align-items:center;}
157
+ .fleet-donut{width:100px;height:100px;border-radius:50%;position:relative;display:grid;place-items:center;background:conic-gradient(var(--ok) 0 var(--ready-angle,0deg),var(--warn) var(--ready-angle,0deg) var(--warn-angle,0deg),var(--bad) var(--warn-angle,0deg) 360deg);box-shadow:0 0 30px rgba(246,215,123,.08);}
158
+ .fleet-donut:after{content:"";position:absolute;inset:15px;border-radius:50%;background:#0c0a07;border:1px solid var(--glass-border)}.fleet-donut strong{position:relative;z-index:1;font-family:var(--display);font-size:1.15rem;color:var(--gold-pale)}
159
+ .fleet-legend{display:grid;gap:.32rem}.fleet-legend div{display:flex;align-items:center;justify-content:space-between;gap:.5rem;font-family:var(--mono);font-size:.56rem;color:var(--muted)}.fleet-legend i{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:.35rem}.fleet-legend b{color:var(--ink)}
160
+ .fleet-bars{display:grid;gap:.4rem;max-height:154px;overflow:auto}.fleet-bar-row{display:grid;grid-template-columns:minmax(58px,90px) 1fr 48px;gap:.45rem;align-items:center;font-family:var(--mono);font-size:.54rem;color:var(--muted)}.fleet-bar-row .nm{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ink)}.fleet-bar-row .track{height:7px;border-radius:10px;background:rgba(255,255,255,.045);overflow:hidden}.fleet-bar-row .fill{height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--gold-deep),var(--gold-bright));box-shadow:0 0 9px rgba(246,215,123,.2)}.fleet-bar-row .val{text-align:right}
161
+ .fleet-attention{display:grid;gap:.36rem;max-height:154px;overflow:auto}.fleet-alert-row{display:grid;grid-template-columns:7px 1fr auto;gap:.48rem;align-items:start;padding:.36rem .42rem;border-radius:8px;background:rgba(255,255,255,.018);font-size:.61rem}.fleet-alert-row i{width:7px;height:7px;border-radius:50%;margin-top:.28rem;background:var(--warn)}.fleet-alert-row.bad i{background:var(--bad)}.fleet-alert-row.ok i{background:var(--ok)}.fleet-alert-row b{display:block;color:var(--ink);font-size:.62rem}.fleet-alert-row span{color:var(--muted);font-size:.56rem}.fleet-alert-row em{font-family:var(--mono);font-style:normal;font-size:.5rem;color:var(--muted);white-space:nowrap}
162
+ .fleet-release{grid-column:1/-1;display:flex;align-items:center;gap:.55rem;flex-wrap:wrap;padding:.55rem .7rem}.fleet-release .version-pill{font-family:var(--mono);font-size:.56rem;border:1px solid var(--glass-border);border-radius:100px;padding:.18rem .5rem;color:var(--muted)}.fleet-release .version-pill.current{color:var(--ok);border-color:rgba(111,224,166,.28)}
163
+ .node .fleet-card-actions{display:flex;gap:.35rem;flex-wrap:wrap;margin-top:.65rem;padding-top:.55rem;border-top:1px solid var(--glass-border)}
164
+ @media(max-width:1150px){.fleet-kpis{grid-template-columns:repeat(3,1fr)}.fleet-insights{grid-template-columns:1fr 1fr}.fleet-insights .fleet-panel:last-of-type{grid-column:1/-1}}
165
+ @media(max-width:720px){.fleet-hero{align-items:flex-start;flex-direction:column}.fleet-hero-actions{justify-content:flex-start}.fleet-kpis{grid-template-columns:repeat(2,1fr)}.fleet-insights{display:block}.fleet-panel{margin-bottom:.65rem}.fleet-readiness{grid-template-columns:90px 1fr}.fleet-donut{width:86px;height:86px}}
166
+
167
+ /* fullscreen RoboVision operations view */
168
+ .pk-camera-ops{position:fixed;inset:0;z-index:80;display:none;background:#030302;color:var(--ink);overflow:hidden;}
169
+ .pk-camera-ops.open{display:grid;grid-template-rows:64px 1fr;}
170
+ .pk-camera-ops-head{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;border-bottom:1px solid var(--glass-border);background:rgba(12,10,7,.9);backdrop-filter:blur(22px);}
171
+ .pk-camera-ops-head .avatar{width:40px;height:40px;object-fit:contain;filter:drop-shadow(0 7px 14px rgba(0,0,0,.5))}.pk-camera-ops-head .title{font-family:var(--display);font-size:1.08rem;color:var(--gold-pale)}.pk-camera-ops-head .sub{font-family:var(--mono);font-size:.54rem;color:var(--muted)}
172
+ .pk-camera-ops-actions{margin-left:auto;display:flex;gap:.4rem;align-items:center;flex-wrap:wrap;justify-content:flex-end}
173
+ .pk-camera-ops-grid{display:grid;grid-template-columns:minmax(0,1fr) 320px;min-height:0;}
174
+ .pk-camera-stage{position:relative;min-width:0;min-height:0;background:radial-gradient(circle at 50% 50%,#17130b,#030302 72%);overflow:hidden;display:grid;place-items:center;}
175
+ .pk-camera-stage img{width:100%;height:100%;object-fit:contain;display:block}.pk-camera-stage .empty{position:absolute;border:0;font-family:var(--mono);font-size:.68rem;letter-spacing:.08em;text-transform:uppercase}
176
+ .pk-camera-stage:after{content:"";position:absolute;inset:18px;border:1px solid rgba(246,215,123,.13);pointer-events:none;clip-path:polygon(0 0,13% 0,13% 1px,1px 1px,1px 15%,0 15%,0 0,100% 0,100% 15%,calc(100% - 1px) 15%,calc(100% - 1px) 1px,87% 1px,87% 0,100% 0,100% 100%,87% 100%,87% calc(100% - 1px),calc(100% - 1px) calc(100% - 1px),calc(100% - 1px) 85%,100% 85%,100% 100%,0 100%,0 85%,1px 85%,1px calc(100% - 1px),13% calc(100% - 1px),13% 100%,0 100%)}
177
+ .pk-camera-rec{position:absolute;left:34px;top:31px;display:flex;align-items:center;gap:.45rem;padding:.28rem .48rem;border-radius:7px;background:rgba(0,0,0,.58);backdrop-filter:blur(8px);font-family:var(--mono);font-size:.57rem;color:#fff}.pk-camera-rec i{width:7px;height:7px;border-radius:50%;background:var(--bad);box-shadow:0 0 10px var(--bad);animation:pulse 1.4s infinite}
178
+ .pk-camera-clock{position:absolute;right:34px;top:31px;padding:.28rem .48rem;border-radius:7px;background:rgba(0,0,0,.58);font-family:var(--mono);font-size:.57rem;color:var(--gold-pale)}
179
+ .pk-camera-overlay{position:absolute;left:34px;bottom:31px;width:min(660px,calc(100% - 68px));display:grid;grid-template-columns:repeat(4,1fr);gap:.45rem}.pk-camera-overlay .metric{padding:.55rem .6rem;border:1px solid rgba(246,215,123,.18);border-radius:10px;background:rgba(5,4,3,.68);backdrop-filter:blur(12px)}.pk-camera-overlay b{display:block;font-family:var(--display);font-size:.98rem;color:var(--gold-pale)}.pk-camera-overlay span{font-family:var(--mono);font-size:.46rem;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
180
+ .pk-camera-side{min-height:0;overflow:auto;padding:.8rem;background:linear-gradient(180deg,rgba(20,16,9,.94),rgba(7,6,4,.98));border-left:1px solid var(--glass-border)}
181
+ .pk-camera-side .section{padding:.7rem;margin-bottom:.65rem;border:1px solid var(--glass-border);border-radius:13px;background:rgba(255,255,255,.018)}.pk-camera-side h3{font-family:var(--mono);font-size:.55rem;letter-spacing:.13em;text-transform:uppercase;color:var(--gold);margin-bottom:.5rem}.pk-camera-insight{display:grid;grid-template-columns:1fr auto;gap:.38rem .55rem;font-family:var(--mono);font-size:.56rem}.pk-camera-insight span{color:var(--muted)}.pk-camera-insight b{color:var(--ink);text-align:right;font-weight:600}.pk-camera-insight b.ok{color:var(--ok)}.pk-camera-insight b.bad{color:var(--bad)}
182
+ .pk-camera-pipeline{display:grid;gap:.34rem}.pk-camera-pipeline div{display:grid;grid-template-columns:8px 1fr auto;gap:.42rem;align-items:center;font-family:var(--mono);font-size:.53rem;color:var(--muted)}.pk-camera-pipeline i{width:7px;height:7px;border-radius:50%;background:var(--muted)}.pk-camera-pipeline i.ok{background:var(--ok);box-shadow:0 0 8px rgba(111,224,166,.45)}.pk-camera-pipeline i.warn{background:var(--warn)}
183
+ body.camera-ops-open{overflow:hidden}
184
+ @media(max-width:850px){.pk-camera-ops.open{grid-template-rows:auto 1fr}.pk-camera-ops-head{align-items:flex-start;flex-wrap:wrap}.pk-camera-ops-actions{width:100%;margin-left:0;justify-content:flex-start}.pk-camera-ops-grid{display:block;overflow:auto}.pk-camera-stage{height:58vh;min-height:340px}.pk-camera-side{border-left:0;border-top:1px solid var(--glass-border)}.pk-camera-overlay{grid-template-columns:repeat(2,1fr)}}
137
185
 
138
186
  /* hardware meters */
139
187
  .hw{border-radius:13px;padding:0.85rem;margin-top:1rem;
@@ -546,6 +594,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
546
594
  .rp-call-service .ready{font-family:var(--mono);font-size:.48rem;letter-spacing:.08em;text-transform:uppercase;color:var(--ok);}
547
595
  .rp-call-device{display:grid;gap:.22rem;margin-top:.45rem}.rp-call-device:first-child{margin-top:0}.rp-call-device label{font-family:var(--mono);font-size:.51rem;text-transform:uppercase;letter-spacing:.1em;color:var(--muted)}
548
596
  .rp-call-device select{width:100%;min-width:0;font-family:var(--mono);font-size:.62rem;background:#0c0a07;border:1px solid var(--glass-border);color:var(--ink);border-radius:9px;padding:.42rem .5rem;}
597
+ .rp-call-device-note{margin-top:.55rem;padding:.52rem .58rem;border:1px solid var(--glass-border);border-radius:10px;background:rgba(0,0,0,.22);font-family:var(--mono);font-size:.54rem;line-height:1.45;color:var(--muted)}
598
+ .rp-call-device-note.ok{color:var(--ok);border-color:rgba(87,224,156,.28);background:rgba(28,64,50,.28)}.rp-call-device-note.bad{color:var(--bad);border-color:rgba(255,126,110,.34);background:rgba(64,24,20,.34)}.rp-call-device-note.warn{color:var(--warn);border-color:rgba(242,193,78,.3);background:rgba(64,50,20,.3)}
599
+ .rp-call-device-actions{display:grid;grid-template-columns:1fr 1fr;gap:.4rem;margin-top:.55rem}.rp-call-device-actions .rp-btn{width:100%}
549
600
  .rp-call-stage{display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;overflow:hidden;}
550
601
  .rp-call-stage:before{content:"";position:absolute;width:420px;height:420px;border:1px solid rgba(246,215,123,.08);border-radius:50%;box-shadow:0 0 0 42px rgba(246,215,123,.018),0 0 0 92px rgba(246,215,123,.012);}
551
602
  .rp-call-visual{position:relative;width:min(330px,80%);aspect-ratio:1;border-radius:50%;display:grid;place-items:center;background:radial-gradient(circle,rgba(246,215,123,.14),rgba(10,8,5,.58) 52%,rgba(0,0,0,.82) 72%);border:1px solid rgba(246,215,123,.24);box-shadow:0 0 70px rgba(201,150,46,.13),inset 0 0 48px rgba(246,215,123,.06);z-index:1;transition:box-shadow .3s,border-color .3s;}
@@ -606,9 +657,40 @@ button.act:disabled{opacity:0.5;cursor:default;}
606
657
  .rp-vp-id{font-size:0.62rem;color:var(--muted);margin-top:0.1rem;word-break:break-all;}
607
658
  .rp-vp-desc{font-size:0.64rem;color:var(--muted);margin-top:0.2rem;line-height:1.4;}
608
659
  .rp-voice-picker-foot{font-size:0.6rem;color:var(--muted);text-align:center;padding-top:0.2rem;border-top:1px solid var(--glass-border);}
609
- .rp-voice-picker-foot .rp-muted{color:var(--muted);}
610
-
611
- .rp-fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:0.7rem;padding:0;}
660
+ .rp-voice-picker-foot .rp-muted{color:var(--muted);}
661
+
662
+ /* Character and voice configuration workspace */
663
+ .rp-config-workspace{grid-column:1/-1;display:grid;gap:1rem;}
664
+ .rp-config-hero{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;padding:1rem 1.1rem;border:1px solid var(--border-strong);border-radius:18px;background:radial-gradient(circle at 8% 0,rgba(246,215,123,.14),transparent 34%),linear-gradient(135deg,rgba(39,31,17,.82),rgba(8,7,5,.72));box-shadow:inset 0 1px 0 rgba(255,238,194,.1),0 18px 50px rgba(0,0,0,.28);}
665
+ .rp-config-hero h3{font-family:var(--display);font-size:1.25rem;color:var(--gold-pale);margin:0;}
666
+ .rp-config-hero p{max-width:72ch;margin:.25rem 0 0;color:var(--muted);font-size:.68rem;line-height:1.55;}
667
+ .rp-config-hero-actions,.rp-config-toolbar,.rp-config-actions{display:flex;align-items:center;gap:.45rem;flex-wrap:wrap;}
668
+ .rp-config-columns{display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start;}
669
+ .rp-config-library{min-width:0;padding:.9rem;border:1px solid var(--glass-border);border-radius:18px;background:linear-gradient(145deg,rgba(31,26,18,.62),rgba(9,8,6,.7));box-shadow:inset 0 1px 0 var(--glass-hi),0 14px 44px rgba(0,0,0,.24);}
670
+ .rp-config-library-head{display:flex;align-items:flex-start;gap:.7rem;margin-bottom:.7rem;}
671
+ .rp-config-library-head .icon{width:38px;height:38px;display:grid;place-items:center;border-radius:11px;background:rgba(246,215,123,.1);border:1px solid rgba(246,215,123,.22);color:var(--gold-bright);font-size:1rem;flex:none;}
672
+ .rp-config-library-head h4{font-family:var(--display);font-size:1rem;color:var(--gold-pale);margin:0;}
673
+ .rp-config-library-head p{font-size:.58rem;color:var(--muted);margin:.1rem 0 0;line-height:1.4;}
674
+ .rp-config-library-head .count{margin-left:auto;font-family:var(--mono);font-size:.56rem;color:var(--gold);padding:.25rem .5rem;border:1px solid var(--glass-border);border-radius:999px;}
675
+ .rp-config-toolbar{margin-bottom:.65rem;}.rp-config-toolbar input{flex:1;min-width:150px;}
676
+ .rp-config-cards{display:grid;gap:.55rem;max-height:560px;overflow:auto;padding-right:.2rem;scrollbar-color:var(--gold-deep) transparent;}
677
+ .rp-config-card{position:relative;padding:.72rem;border:1px solid var(--glass-border);border-radius:13px;background:rgba(255,255,255,.022);transition:border-color .15s,transform .15s,background .15s;cursor:pointer;overflow:hidden;}
678
+ .rp-config-card:before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(var(--gold-pale),var(--gold-deep));opacity:.65;}
679
+ .rp-config-card:hover{transform:translateY(-1px);border-color:rgba(246,215,123,.38);background:rgba(246,215,123,.045);}
680
+ .rp-config-card-head{display:flex;align-items:center;gap:.55rem;min-width:0;}.rp-config-card-head img{width:46px;height:46px;object-fit:contain;filter:drop-shadow(0 8px 12px rgba(0,0,0,.45));flex:none;}
681
+ .rp-config-card-title{min-width:0;flex:1}.rp-config-card-title b{display:block;font-family:var(--display);font-size:.9rem;color:var(--gold-pale);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rp-config-card-title small{display:block;font-family:var(--mono);font-size:.5rem;color:var(--muted);margin-top:.08rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
682
+ .rp-config-badges{display:flex;gap:.3rem;flex-wrap:wrap;margin-top:.55rem}.rp-config-badge{font-family:var(--mono);font-size:.48rem;letter-spacing:.04em;text-transform:uppercase;padding:.18rem .38rem;border-radius:999px;border:1px solid var(--glass-border);color:var(--muted);background:rgba(0,0,0,.18)}.rp-config-badge.ok{color:var(--ok);border-color:rgba(87,224,156,.28)}.rp-config-badge.gold{color:var(--gold-bright);border-color:rgba(246,215,123,.28)}
683
+ .rp-config-pipeline{display:grid;grid-template-columns:1fr auto 1fr auto 1fr;gap:.32rem;align-items:center;margin-top:.55rem;font-family:var(--mono);font-size:.53rem}.rp-config-pipeline span{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.28rem .4rem;border-radius:7px;background:rgba(0,0,0,.24);border:1px solid var(--glass-border);color:var(--ink)}.rp-config-pipeline i{font-style:normal;color:var(--gold)}
684
+ .rp-config-summary{margin-top:.5rem;color:var(--muted);font-size:.57rem;line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
685
+ .rp-config-card .rp-config-actions{margin-top:.6rem}.rp-config-card .rp-btn{font-size:.53rem;padding:.32rem .5rem;}
686
+ .rp-config-editor{display:grid;gap:.8rem}.rp-config-editor-intro{display:flex;gap:.75rem;padding:.72rem;border:1px solid rgba(246,215,123,.22);border-radius:13px;background:radial-gradient(circle at 0 0,rgba(246,215,123,.12),transparent 48%),rgba(0,0,0,.18)}.rp-config-editor-intro .mark{width:42px;height:42px;display:grid;place-items:center;border:1px solid rgba(246,215,123,.25);border-radius:12px;color:var(--gold-bright);background:rgba(246,215,123,.08);font-size:1rem;flex:none}.rp-config-editor-intro b{display:block;font-family:var(--display);font-size:.95rem;color:var(--gold-pale)}.rp-config-editor-intro span{display:block;color:var(--muted);font-size:.59rem;line-height:1.45;margin-top:.15rem}
687
+ .rp-config-section{padding:.75rem;border:1px solid var(--glass-border);border-radius:13px;background:rgba(255,255,255,.018)}.rp-config-section-head{display:flex;align-items:center;gap:.45rem;margin-bottom:.55rem;font-family:var(--mono);font-size:.58rem;text-transform:uppercase;letter-spacing:.09em;color:var(--gold-bright)}.rp-config-section-head span{margin-left:auto;color:var(--muted);font-size:.48rem;letter-spacing:.03em;text-transform:none}.rp-config-fields{display:grid;grid-template-columns:1fr 1fr;gap:.55rem}.rp-config-fields .wide{grid-column:1/-1}.rp-config-fields textarea{width:100%;resize:vertical;min-height:110px;font-family:var(--mono);font-size:.67rem;line-height:1.5}.rp-config-checks{display:flex;gap:.8rem;flex-wrap:wrap}.rp-config-checks label{display:flex;align-items:center;gap:.35rem;color:var(--ink);font-size:.62rem}.rp-config-checks input{width:auto;margin:0;accent-color:var(--gold-bright)}
688
+ .rp-config-editor-foot{position:sticky;bottom:-1rem;display:flex;justify-content:flex-end;gap:.45rem;padding:.7rem 0 .1rem;background:linear-gradient(transparent,var(--panel) 28%)}
689
+ .rp-modal.config-modal{width:min(980px,100%);max-height:90vh}.rp-modal.config-modal .rp-modal-body{padding:1rem 1.1rem 1.2rem;}
690
+ @media(max-width:900px){.rp-config-columns{grid-template-columns:1fr}.rp-config-hero{align-items:flex-start;flex-direction:column}.rp-config-cards{max-height:430px}}
691
+ @media(max-width:620px){.rp-config-fields{grid-template-columns:1fr}.rp-config-fields .wide{grid-column:auto}.rp-config-pipeline{grid-template-columns:1fr}.rp-config-pipeline i{display:none}.rp-config-hero-actions{width:100%}.rp-config-hero-actions .rp-btn{flex:1}}
692
+
693
+ .rp-fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:0.7rem;padding:0;}
612
694
  .rp-fleet-card{position:relative;}
613
695
  .rp-fleet-card .spark{margin-top:0.5rem;height:36px;}
614
696
  .rp-fleet-card .rp-actions{margin-top:0.6rem;}
@@ -648,12 +730,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
648
730
  <button class="tab" data-tab="robopark" id="tabbtn-robopark">Sessions &amp; Telemetry</button>
649
731
  </div>
650
732
 
651
- <div class="pane active" id="pane-fleet">
652
- <div class="wrap">
653
- <section>
654
- <h2>Fleet <span class="count" id="fleetcount"></span></h2>
655
- <div class="fleet" id="fleet"></div>
656
- <div class="hw" id="hw"></div>
733
+ <div class="pane active" id="pane-fleet">
734
+ <div class="wrap">
735
+ <section>
736
+ <div class="fleet-ops">
737
+ <div class="fleet-hero">
738
+ <div><div class="eyebrow">Production command center</div><h1>Fleet intelligence</h1><p>Live readiness, workload, release compliance, hardware pressure, and operator actions across every RoboPark device.</p></div>
739
+ <div class="fleet-hero-actions"><button class="rp-btn primary" id="fleet-open-park">Open Park map</button><button class="rp-btn" id="fleet-open-sessions">Live sessions</button><button class="rp-btn" id="fleet-open-incidents">Incidents</button><button class="rp-btn" id="fleet-refresh">Refresh</button></div>
740
+ </div>
741
+ <div class="fleet-kpis" id="fleet-kpis"></div>
742
+ <div class="fleet-insights">
743
+ <div class="fleet-panel"><div class="fleet-panel-head"><b>Operational readiness</b><span id="fleet-readiness-meta">live</span></div><div id="fleet-readiness"></div></div>
744
+ <div class="fleet-panel"><div class="fleet-panel-head"><b>Robot workload</b><span>sessions / triggers</span></div><div class="fleet-bars" id="fleet-workload"></div></div>
745
+ <div class="fleet-panel"><div class="fleet-panel-head"><b>Needs attention</b><span id="fleet-attention-meta">0 issues</span></div><div class="fleet-attention" id="fleet-attention"></div></div>
746
+ <div class="fleet-panel fleet-release" id="fleet-release"></div>
747
+ </div>
748
+ </div>
749
+ <h2>Fleet <span class="count" id="fleetcount"></span></h2>
750
+ <div class="fleet" id="fleet"></div>
751
+ <div class="hw" id="hw"></div>
657
752
  </section>
658
753
  <aside>
659
754
  <h2>Live activity</h2>
@@ -678,16 +773,39 @@ button.act:disabled{opacity:0.5;cursor:default;}
678
773
  </aside>
679
774
  <div class="pk-hint">drag to pan · scroll to zoom · <b>click a robot</b> for cam / mic / speaker</div>
680
775
  </div>
681
- <div class="pk-drawer" id="pk-drawer">
776
+ <div class="pk-drawer" id="pk-drawer">
682
777
  <div class="pk-dh">
683
778
  <div class="pk-av" id="pk-av">🤖</div>
684
779
  <div><div class="nm" id="pk-name">robot</div><div class="sub" id="pk-sub">satellite</div></div>
685
780
  <span class="pk-pill idle" id="pk-state" style="margin-left:0.4rem">idle</span>
686
781
  <button class="x" id="pk-close">✕</button>
687
782
  </div>
688
- <div class="pk-dbody" id="pk-body"></div>
689
- </div>
690
- </div>
783
+ <div class="pk-dbody" id="pk-body"></div>
784
+ </div>
785
+ </div>
786
+
787
+ <div class="pk-camera-ops" id="pk-camera-ops" role="dialog" aria-modal="true" aria-label="Fullscreen robot camera operations view">
788
+ <div class="pk-camera-ops-head">
789
+ <img class="avatar" id="pk-camera-ops-avatar" alt="">
790
+ <div><div class="title" id="pk-camera-ops-name">Robot camera</div><div class="sub" id="pk-camera-ops-sub">RoboVision mesh relay</div></div>
791
+ <div class="pk-camera-ops-actions"><button class="rp-btn primary" id="pk-camera-ops-trigger">Simulate motion</button><button class="rp-btn" id="pk-camera-ops-production">Production mode</button><button class="rp-btn danger" id="pk-camera-ops-end">End session</button><button class="rp-btn" id="pk-camera-ops-refresh">Refresh feed</button><button class="rp-btn" id="pk-camera-ops-native">Browser fullscreen</button><button class="rp-btn" id="pk-camera-ops-close">Close</button></div>
792
+ </div>
793
+ <div class="pk-camera-ops-grid">
794
+ <div class="pk-camera-stage" id="pk-camera-stage">
795
+ <img id="pk-camera-ops-img" alt="Live robot camera feed">
796
+ <div class="empty" id="pk-camera-ops-empty">opening RoboVision camera...</div>
797
+ <div class="pk-camera-rec"><i></i><span id="pk-camera-ops-rec">REC / CONNECTING</span></div>
798
+ <div class="pk-camera-clock" id="pk-camera-ops-clock">--:--:--</div>
799
+ <div class="pk-camera-overlay" id="pk-camera-ops-overlay"></div>
800
+ </div>
801
+ <aside class="pk-camera-side">
802
+ <div class="section"><h3>Live insights</h3><div class="pk-camera-insight" id="pk-camera-ops-insights"></div></div>
803
+ <div class="section"><h3>Conversation pipeline</h3><div class="pk-camera-pipeline" id="pk-camera-ops-pipeline"></div></div>
804
+ <div class="section"><h3>Hardware and media</h3><div class="pk-camera-insight" id="pk-camera-ops-hardware"></div></div>
805
+ <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>
806
+ </aside>
807
+ </div>
808
+ </div>
691
809
 
692
810
  <div class="pane" id="pane-robopark">
693
811
  <div class="rp-head">
@@ -709,9 +827,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
709
827
  <button class="rp-subtab" data-sub="live" id="rpsbtn-live" role="tab" aria-selected="false">Live Sessions <span class="badge" id="rps-live-badge">0</span></button>
710
828
  <button class="rp-subtab" data-sub="voice" id="rpsbtn-voice" role="tab" aria-selected="false">Voice Call</button>
711
829
  <button class="rp-subtab" data-sub="history" id="rpsbtn-history" role="tab" aria-selected="false">History</button>
712
- <button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
713
- <button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
714
- <button class="rp-subtab" data-sub="setup" id="rpsbtn-setup" role="tab" aria-selected="false">Setup</button>
830
+ <button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
831
+ <button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
832
+ <button class="rp-subtab" data-sub="config" id="rpsbtn-config" role="tab" aria-selected="false">Characters &amp; Voice</button>
833
+ <button class="rp-subtab" data-sub="setup" id="rpsbtn-setup" role="tab" aria-selected="false">Setup</button>
715
834
  </div>
716
835
 
717
836
  <!-- ═══ Overview ═══ -->
@@ -761,6 +880,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
761
880
  <div class="rp-call-card">
762
881
  <div class="rp-call-device"><label>Character preset</label><select id="rp-call-character-select"><option value="">select character...</option></select></div>
763
882
  <div class="rp-call-device"><label>Voice stack</label><select id="rp-call-stack-select"><option value="">character default</option></select></div>
883
+ <div class="rp-config-actions" style="margin-top:.6rem"><button class="rp-btn" id="rp-call-edit-character">Edit character</button><button class="rp-btn" id="rp-call-new-character">New</button><button class="rp-btn" id="rp-call-edit-stack">Edit stack</button><button class="rp-btn" id="rp-call-new-stack">New stack</button></div>
764
884
  </div>
765
885
  <div class="rp-call-card">
766
886
  <div class="rp-section-h" style="margin:0 0 .5rem">Voice pipeline <span class="meta">selected stack</span></div>
@@ -771,11 +891,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
771
891
  </div>
772
892
  </div>
773
893
  <div class="rp-call-card">
774
- <div class="rp-section-h" style="margin:0 0 .5rem">Browser devices <span class="meta" id="rp-call-device-status">permission on start</span></div>
894
+ <div class="rp-section-h" style="margin:0 0 .5rem">Browser devices <span class="meta" id="rp-call-device-status">permission required</span></div>
775
895
  <div class="rp-call-device"><label>Microphone</label><select id="rp-call-mic-device"><option value="">system default</option></select></div>
776
896
  <div class="rp-call-device"><label>Camera</label><select id="rp-call-cam-device"><option value="">system default</option></select></div>
777
897
  <div class="rp-call-device"><label>Speaker output</label><select id="rp-call-speaker-device"><option value="">system default</option></select></div>
778
- <button class="rp-btn" id="rp-call-refresh-devices" style="width:100%;margin-top:.55rem">Refresh browser devices</button>
898
+ <div class="rp-call-device-note" id="rp-call-device-note">Enable media access to reveal the actual microphone, camera, and speaker names available to this browser.</div>
899
+ <div class="rp-call-device-actions"><button class="rp-btn primary" id="rp-call-refresh-devices">Enable &amp; scan</button><button class="rp-btn" id="rp-call-test-mic">Test microphone</button></div>
779
900
  </div>
780
901
  </aside>
781
902
  <main class="rp-call-stage">
@@ -936,8 +1057,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
936
1057
  </div>
937
1058
 
938
1059
  <!-- ═══ Fleet ═══ -->
939
- <div class="rp-sub" id="rp-sub-fleet">
940
- <div class="rp-section-h">Robots <span class="meta" id="rp-fleet-meta">live + idle + offline</span></div>
1060
+ <div class="rp-sub" id="rp-sub-fleet">
1061
+ <div class="fleet-ops" style="margin-bottom:1rem">
1062
+ <div class="fleet-kpis" id="rp-fleet-kpis"></div>
1063
+ <div class="fleet-insights">
1064
+ <div class="fleet-panel"><div class="fleet-panel-head"><b>Operational readiness</b><span>scheduler</span></div><div id="rp-fleet-readiness"></div></div>
1065
+ <div class="fleet-panel"><div class="fleet-panel-head"><b>Robot workload</b><span>sessions / triggers</span></div><div class="fleet-bars" id="rp-fleet-workload"></div></div>
1066
+ <div class="fleet-panel"><div class="fleet-panel-head"><b>Needs attention</b><span id="rp-fleet-attention-meta">0 issues</span></div><div class="fleet-attention" id="rp-fleet-attention"></div></div>
1067
+ </div>
1068
+ </div>
1069
+ <div class="rp-section-h">Robots <span class="meta" id="rp-fleet-meta">live + idle + offline</span></div>
941
1070
  <div class="rp-fleet-grid" id="rp-grid"></div>
942
1071
  </div>
943
1072
 
@@ -1000,79 +1129,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
1000
1129
  <div class="rp-autonote" id="rp-runtime-note">Docker controls are protected by the hub token. Logs are limited to the latest 100 lines.</div>
1001
1130
  </div>
1002
1131
 
1003
- <!-- Config templates -->
1004
- <div class="rp-form" id="rp-templates" style="grid-template-columns:1fr 1fr; margin-top:1.2rem;">
1005
- <h3 style="grid-column:1/-1">Config templates</h3>
1006
- <div>
1007
- <h4 style="font-family:var(--mono);font-size:0.75rem;color:var(--gold-bright);margin:0.3rem 0 0.5rem;">Voice stacks</h4>
1008
- <div id="rp-voice-list" style="display:flex;flex-direction:column;gap:0.4rem;max-height:260px;overflow:auto;"></div>
1009
- <div style="display:flex;gap:0.4rem;margin-top:0.55rem;">
1010
- <input id="rp-newvoice-name" placeholder="new stack name" style="flex:1;min-width:0;">
1011
- <button class="rp-btn" id="rp-addvoice">Add</button>
1012
- </div>
1013
- <div class="rp-form" id="rp-voice-edit" style="display:none;grid-template-columns:1fr 1fr;gap:0.4rem;margin-top:0.6rem;padding:0.6rem;border-color:var(--border-strong);">
1014
- <h4 style="grid-column:1/-1;margin:0 0 0.2rem;">Edit voice stack: <span id="rpve-name-label"></span></h4>
1015
- <div class="rp-field"><label>STT provider</label><select id="rpve-stt-provider"><option value="speaches">speaches</option><option value="groq">groq</option><option value="vibevoice">vibevoice</option></select></div>
1016
- <div class="rp-field"><label>STT model</label><input id="rpve-stt-model"></div>
1017
- <div class="rp-field"><label>STT language</label><input id="rpve-stt-language" placeholder="en / auto"></div>
1018
- <div class="rp-field"><label>LLM provider</label><select id="rpve-llm-provider"><option value="ollama">ollama</option><option value="groq">groq</option><option value="gemini">gemini</option><option value="moltbot">moltbot</option></select></div>
1019
- <div class="rp-field"><label>LLM model</label><input id="rpve-llm-model"></div>
1020
- <div class="rp-field"><label>LLM API key</label><input type="password" id="rpve-llm-key" placeholder="(uses agent's own if blank)"></div>
1021
- <div class="rp-field"><label>Temperature</label><input id="rpve-temperature" type="number" step="0.1" value="0.7"></div>
1022
- <div class="rp-field"><label>Context size (num_ctx)</label><input id="rpve-num-ctx" type="number" value="8192"></div>
1023
- <div class="rp-field"><label>TTS provider</label><select id="rpve-tts-provider"><option value="elevenlabs">elevenlabs</option><option value="kokoro">kokoro</option><option value="piper">piper</option></select></div>
1024
- <div class="rp-field">
1025
- <label>TTS voice</label>
1026
- <div style="display:flex;gap:0.35rem;align-items:stretch;">
1027
- <select id="rpve-tts-voice" style="flex:1;min-width:0;"></select>
1028
- <button type="button" class="rp-btn" id="rpve-tts-voice-browse" title="Browse the full voice catalog for the selected provider" style="white-space:nowrap;">Browse…</button>
1029
- </div>
1030
- <div class="rp-autonote" id="rpve-tts-voice-note" style="margin-top:0.35rem;"></div>
1031
- </div>
1032
- <div class="rp-field"><label>TTS language</label><input id="rpve-tts-language" placeholder="en"></div>
1033
- <div class="rp-field"><label>Max turns</label><input id="rpve-max-turns" type="number" value="20"></div>
1034
- <div class="rp-field"><label>Min endpointing delay (s)</label><input id="rpve-endpointing" type="number" step="0.1" value="0.7"></div>
1035
- <div class="rp-field"><label><input type="checkbox" id="rpve-interruptions" checked style="width:auto;margin-right:0.35rem;">Allow interruptions</label></div>
1036
- <div class="rp-field"><label><input type="checkbox" id="rpve-wake-enabled" style="width:auto;margin-right:0.35rem;">Wake word enabled</label></div>
1037
- <div class="rp-field"><label>Wake word model</label><input id="rpve-wake-model" placeholder="models/hey_jarvis.onnx"></div>
1038
- <div class="rp-field"><label>Wake threshold</label><input id="rpve-wake-threshold" type="number" step="0.05" value="0.5"></div>
1039
- <div class="rp-field"><label>Wake timeout (s)</label><input id="rpve-wake-timeout" type="number" step="0.5" value="3.0"></div>
1040
- <div style="grid-column:1/-1;display:flex;gap:0.4rem;">
1041
- <button class="rp-btn" id="rpve-save">Save</button>
1042
- <button class="rp-btn" id="rpve-cancel">Cancel</button>
1043
- </div>
1044
- </div>
1045
- </div>
1046
- <div>
1047
- <h4 style="font-family:var(--mono);font-size:0.75rem;color:var(--gold-bright);margin:0.3rem 0 0.5rem;">Character presets</h4>
1048
- <div id="rp-char-list" style="display:flex;flex-direction:column;gap:0.4rem;max-height:260px;overflow:auto;"></div>
1049
- <div style="display:flex;gap:0.4rem;margin-top:0.55rem;">
1050
- <input id="rp-newchar-name" placeholder="new preset name" style="flex:1;min-width:0;">
1051
- <input id="rp-newchar-stack" list="rp-newchar-stack-list" placeholder="voice stack id" style="flex:1;min-width:0;">
1052
- <datalist id="rp-newchar-stack-list"></datalist>
1053
- <button class="rp-btn" id="rp-addchar">Add</button>
1054
- </div>
1055
- <div class="rp-form" id="rp-char-edit" style="display:none;grid-template-columns:1fr;gap:0.4rem;margin-top:0.6rem;padding:0.6rem;border-color:var(--border-strong);">
1056
- <h4 style="margin:0 0 0.2rem;">Edit character: <span id="rpce-name-label"></span></h4>
1057
- <div class="rp-field"><label>Name</label><input id="rpce-name"></div>
1058
- <div class="rp-field"><label>Description</label><input id="rpce-description"></div>
1059
- <div class="rp-field"><label>System prompt</label><textarea id="rpce-prompt" rows="4" style="font-family:var(--mono);font-size:0.72rem;width:100%;resize:vertical;"></textarea></div>
1060
- <div class="rp-field"><label>Voice stack</label><select id="rpce-stack"></select></div>
1061
- <div class="rp-field"><label>Motors (comma-separated)</label><input id="rpce-motors" placeholder="drive, wave"></div>
1062
- <div style="display:flex;gap:0.4rem;">
1063
- <div class="rp-field" style="flex:1"><label>Pad X (0-1)</label><input id="rpce-nx" type="number" step="0.01" min="0" max="1"></div>
1064
- <div class="rp-field" style="flex:1"><label>Pad Y (0-1)</label><input id="rpce-ny" type="number" step="0.01" min="0" max="1"></div>
1065
- </div>
1066
- <div class="rp-field"><label>Avatar image path</label><input id="rpce-img" placeholder="/static/robopark/jaguar.png"></div>
1067
- <div style="display:flex;gap:0.4rem;">
1068
- <button class="rp-btn" id="rpce-save">Save</button>
1069
- <button class="rp-btn" id="rpce-cancel">Cancel</button>
1070
- </div>
1071
- </div>
1072
- </div>
1073
- </div>
1074
- </div>
1075
- </div>
1132
+ </div>
1133
+
1134
+ <!-- Character + voice management -->
1135
+ <div class="rp-sub" id="rp-sub-config">
1136
+ <div class="rp-config-workspace" id="rp-templates">
1137
+ <div class="rp-config-hero">
1138
+ <div><h3>Character &amp; Voice Studio</h3><p>Build reusable personas and production voice pipelines in one place. Changes are applied centrally and become available immediately in Voice Studio, Fleet, and every Park robot panel.</p></div>
1139
+ <div class="rp-config-hero-actions"><button class="rp-btn primary" id="rp-new-character">New character</button><button class="rp-btn" id="rp-new-voice-stack">New voice stack</button></div>
1140
+ </div>
1141
+ <div class="rp-config-columns">
1142
+ <section class="rp-config-library">
1143
+ <div class="rp-config-library-head"><span class="icon">VOX</span><div><h4>Voice stacks</h4><p>Speech recognition, language model, synthesis, turn-taking, and wake behavior.</p></div><span class="count" id="rp-voice-count">0 stacks</span></div>
1144
+ <div class="rp-config-toolbar"><input id="rp-voice-search" type="search" placeholder="Search provider, model, voice, or stack…"><button class="rp-btn primary" id="rp-addvoice">Create</button></div>
1145
+ <div class="rp-config-cards" id="rp-voice-list"></div>
1146
+ <input id="rp-newvoice-name" type="hidden"><div id="rp-voice-edit" style="display:none"></div>
1147
+ </section>
1148
+ <section class="rp-config-library">
1149
+ <div class="rp-config-library-head"><span class="icon">AI</span><div><h4>Character presets</h4><p>Persona, prompt, robot avatar, park placement, motors, and default voice stack.</p></div><span class="count" id="rp-char-count">0 presets</span></div>
1150
+ <div class="rp-config-toolbar"><input id="rp-char-search" type="search" placeholder="Search character, stack, motor, or prompt…"><button class="rp-btn primary" id="rp-addchar">Create</button></div>
1151
+ <div class="rp-config-cards" id="rp-char-list"></div>
1152
+ <input id="rp-newchar-name" type="hidden"><input id="rp-newchar-stack" type="hidden"><datalist id="rp-newchar-stack-list"></datalist><div id="rp-char-edit" style="display:none"></div>
1153
+ </section>
1154
+ </div>
1155
+ </div>
1156
+ </div>
1157
+ </div>
1076
1158
 
1077
1159
  <!-- Generic modal: title + scrollable body, used for session + audit detail views -->
1078
1160
  <div class="rp-modal-back" id="rp-modal-back" role="dialog" aria-modal="true">
@@ -1282,9 +1364,70 @@ button.act:disabled{opacity:0.5;cursor:default;}
1282
1364
  }
1283
1365
  return null;
1284
1366
  }
1367
+ function renderFleetOperations(nodes, prefix, telemetryRows){
1368
+ prefix=prefix||'fleet'; nodes=Array.isArray(nodes)?nodes:[]; telemetryRows=Array.isArray(telemetryRows)?telemetryRows:null;
1369
+ var rows=[];
1370
+ function readinessOf(t,connected){ return String((t&&(t.readiness_code||t.status))||(!connected?'offline':'mesh_online')).toLowerCase(); }
1371
+ if(telemetryRows){
1372
+ for(var ti=0;ti<telemetryRows.length;ti++){
1373
+ var t=telemetryRows[ti], rn=robotName(t), rc=readinessOf(t,true), offline=rc==='offline'||rc==='robot_offline'||rc==='unreachable';
1374
+ rows.push({name:rn,connected:!offline,tele:t,hw:null,version:t.version||'',node:null,readiness:rc});
1375
+ }
1376
+ } else {
1377
+ for(var ni=0;ni<nodes.length;ni++){
1378
+ var n=nodes[ni], tele=rpTelemetryForNode(n), role=String(n.role||'').toLowerCase();
1379
+ if(role!=='satellite'&&role!=='robot'&&!tele) continue;
1380
+ rows.push({name:n.displayName||n.name||n.nodeId,connected:!!n.connected,tele:tele,hw:hwByNode[n.nodeId]||null,version:n.version||'',node:n,readiness:readinessOf(tele,!!n.connected)});
1381
+ }
1382
+ }
1383
+ var connected=0,ready=0,active=0,totalSessions=0,totalTriggers=0,latencies=[],drops=[],attention=[];
1384
+ for(var i=0;i<rows.length;i++){
1385
+ var r=rows[i], t=r.tele||{}, code=r.readiness;
1386
+ if(r.connected)connected++;
1387
+ if(code==='ready'||code==='in_session')ready++;
1388
+ if(code==='in_session'||t.active_session||t.in_session||t.session_active||t.current_session_id||t.status==='running'||t.status==='connecting')active++;
1389
+ totalSessions+=Number(t.sessions_total||0);totalTriggers+=Number(t.trigger_count||0);
1390
+ if(t.avg_latency_ms!=null)latencies.push(Number(t.avg_latency_ms));if(t.drop_rate!=null)drops.push(Number(t.drop_rate));
1391
+ var issues=[];
1392
+ if(!r.connected)issues.push('mesh or heartbeat offline');
1393
+ if(code!=='ready'&&code!=='in_session'&&code!=='mesh_online')issues.push(String(t.readiness_message||code).replace(/_/g,' '));
1394
+ if(t.drop_rate!=null&&Number(t.drop_rate)>=.05)issues.push(Math.round(Number(t.drop_rate)*100)+'% session drop rate');
1395
+ if(t.avg_latency_ms!=null&&Number(t.avg_latency_ms)>=3000)issues.push(Math.round(Number(t.avg_latency_ms))+'ms average response');
1396
+ if(r.hw&&r.hw.temperatureC!=null&&r.hw.temperatureC>=70)issues.push(Math.round(r.hw.temperatureC)+'C device temperature');
1397
+ if(r.hw&&r.hw.cpu&&r.hw.cpu.percent>=90)issues.push(Math.round(r.hw.cpu.percent)+'% CPU utilization');
1398
+ if(issues.length)attention.push({name:r.name,message:issues.join(' / '),bad:!r.connected||code==='stuck_session'||code==='join_timeout'||code==='robot_offline'});
1399
+ }
1400
+ var avgLatency=latencies.length?Math.round(latencies.reduce(function(a,b){return a+b;},0)/latencies.length):null;
1401
+ var avgDrop=drops.length?drops.reduce(function(a,b){return a+b;},0)/drops.length:null;
1402
+ var kpis=$(prefix+'-kpis');
1403
+ if(kpis){
1404
+ function k(label,value,detail,cls){return '<div class="fleet-kpi '+(cls||'')+'"><div class="label">'+esc(label)+'</div><div class="value">'+esc(value)+'</div><div class="detail">'+esc(detail)+'</div></div>';}
1405
+ kpis.innerHTML=k('Robots online',connected+'/'+rows.length,rows.length-connected+' unavailable',connected===rows.length?'ok':connected?'warn':'bad')+k('Ready',ready,rows.length?Math.round(ready/rows.length*100)+'% fleet readiness':'no robots',ready===rows.length?'ok':ready?'warn':'bad')+k('Live sessions',active,active?'operator oversight active':'fleet standing by',active?'ok':'')+k('Sessions',totalSessions,totalTriggers+' motion triggers','')+k('Response',avgLatency!=null?avgLatency+'ms':'--','fleet average',avgLatency==null?'':avgLatency>=3000?'bad':avgLatency>=1800?'warn':'ok')+k('Drop rate',avgDrop!=null?Math.round(avgDrop*100)+'%':'--','fleet average',avgDrop==null?'':avgDrop>=.15?'bad':avgDrop>=.05?'warn':'ok');
1406
+ }
1407
+ var read=$(prefix+'-readiness');
1408
+ if(read){
1409
+ var warn=Math.max(0,connected-ready),bad=Math.max(0,rows.length-connected),den=Math.max(1,rows.length),a1=ready/den*360,a2=(ready+warn)/den*360;
1410
+ read.innerHTML='<div class="fleet-readiness"><div class="fleet-donut" style="--ready-angle:'+a1+'deg;--warn-angle:'+a2+'deg"><strong>'+Math.round(ready/den*100)+'%</strong></div><div class="fleet-legend"><div><span><i style="background:var(--ok)"></i>ready / in session</span><b>'+ready+'</b></div><div><span><i style="background:var(--warn)"></i>degraded</span><b>'+warn+'</b></div><div><span><i style="background:var(--bad)"></i>offline</span><b>'+bad+'</b></div><div><span>active conversations</span><b>'+active+'</b></div></div></div>';
1411
+ }
1412
+ var workload=$(prefix+'-workload');
1413
+ if(workload){
1414
+ var maxWork=1;for(var wi=0;wi<rows.length;wi++){var wt=rows[wi].tele||{},wv=Number(wt.sessions_total||0)+Number(wt.trigger_count||0);if(wv>maxWork)maxWork=wv;}
1415
+ workload.innerHTML=rows.length?rows.slice().sort(function(a,b){var at=a.tele||{},bt=b.tele||{};return (Number(bt.sessions_total||0)+Number(bt.trigger_count||0))-(Number(at.sessions_total||0)+Number(at.trigger_count||0));}).map(function(x){var xt=x.tele||{},sessions=Number(xt.sessions_total||0),triggers=Number(xt.trigger_count||0),work=sessions+triggers;return '<div class="fleet-bar-row"><span class="nm">'+esc(x.name)+'</span><span class="track"><i class="fill" style="display:block;width:'+Math.max(3,work/maxWork*100)+'%"></i></span><span class="val">'+sessions+' / '+triggers+'</span></div>';}).join(''):'<div class="pk-note">No robot workload reported yet.</div>';
1416
+ }
1417
+ var attentionEl=$(prefix+'-attention'), attentionMeta=$(prefix+'-attention-meta');
1418
+ if(attentionMeta)attentionMeta.textContent=attention.length+' issue'+(attention.length===1?'':'s');
1419
+ if(attentionEl)attentionEl.innerHTML=attention.length?attention.map(function(a){return '<div class="fleet-alert-row '+(a.bad?'bad':'')+'"><i></i><div><b>'+esc(a.name)+'</b><span>'+esc(a.message)+'</span></div><em>inspect</em></div>';}).join(''):'<div class="fleet-alert-row ok"><i></i><div><b>All systems nominal</b><span>No active fleet readiness, latency, drop, temperature, or CPU alerts.</span></div><em>live</em></div>';
1420
+ var release=$(prefix+'-release');
1421
+ if(release){
1422
+ var versions={};for(var vi=0;vi<nodes.length;vi++){var version=nodes[vi].version||'unknown';versions[version]=(versions[version]||0)+1;}
1423
+ var versionKeys=Object.keys(versions).sort().reverse(),current=versionKeys[0]||'unknown';
1424
+ release.innerHTML='<div class="fleet-panel-head" style="border:0;margin:0;padding:0"><b>Release compliance</b><span>'+nodes.length+' mesh nodes</span></div>'+versionKeys.map(function(v){return '<span class="version-pill '+(v===current?'current':'')+'">v'+esc(v)+' / '+versions[v]+' node'+(versions[v]===1?'':'s')+'</span>';}).join('')+'<span class="pk-note" style="margin-left:auto">updated '+clock(Date.now())+'</span>';
1425
+ }
1426
+ }
1285
1427
  function renderFleet(nodes){
1286
- var el = $('fleet');
1287
- if(!nodes || !nodes.length){ el.innerHTML = '<div class="empty">No nodes reporting yet. Start peers with <span class="mono">infinicode serve</span> or <span class="mono">robopark setup</span>.</div>'; return; }
1428
+ var el = $('fleet');
1429
+ renderFleetOperations(nodes||[],'fleet');
1430
+ if(!nodes || !nodes.length){ el.innerHTML = '<div class="empty">No nodes reporting yet. Start peers with <span class="mono">infinicode serve</span> or <span class="mono">robopark setup</span>.</div>'; return; }
1288
1431
  // connected first, then robots/satellites above hubs so the fleet reads robot-first
1289
1432
  nodes.sort(function(a,b){ return (b.connected?1:0)-(a.connected?1:0); });
1290
1433
  var accent = '#6FE0A6';
@@ -1332,18 +1475,21 @@ button.act:disabled{opacity:0.5;cursor:default;}
1332
1475
  } else {
1333
1476
  html += '<div class="stats"><span style="color:var(--muted)">offline'+(n.lastSeenAt?(' · last seen '+clock(n.lastSeenAt)):'')+'</span></div>';
1334
1477
  }
1335
- html += (capHtml?('<div class="caps">'+capHtml+'</div>'):'') + '</div>';
1478
+ html += (capHtml?('<div class="caps">'+capHtml+'</div>'):'')
1479
+ + ((tele||String(n.role||'').toLowerCase()==='satellite')?('<div class="fleet-card-actions"><button class="rp-btn primary" data-fleet-action="camera" data-robot="'+esc(n.displayName)+'">Live camera</button><button class="rp-btn" data-fleet-action="sessions" data-robot="'+esc(n.displayName)+'">Sessions</button><button class="rp-btn" data-fleet-action="incidents" data-robot="'+esc(n.displayName)+'">Health</button>'+(tele&&(tele.character_id||tele.character_preset_id)?'<button class="rp-btn" data-config-edit-character="'+esc(tele.character_id||tele.character_preset_id)+'">Edit character</button>':'')+(tele&&tele.voice_stack_id?'<button class="rp-btn" data-config-edit-stack="'+esc(tele.voice_stack_id)+'">Edit voice</button>':'')+'</div>'):'')+'</div>';
1336
1480
  }
1337
1481
  el.innerHTML = html;
1338
1482
  var cards = el.querySelectorAll('.node');
1339
1483
  for(var j=0;j<cards.length;j++){
1340
1484
  (function(card){
1341
- function pick(){ selectNode(card.getAttribute('data-id'), card.getAttribute('data-name')); }
1485
+ function pick(ev){ if(ev&&ev.target&&ev.target.closest&&ev.target.closest('[data-fleet-action],[data-config-edit-stack],[data-config-edit-character]'))return; selectNode(card.getAttribute('data-id'), card.getAttribute('data-name')); }
1342
1486
  card.addEventListener('click', pick);
1343
1487
  card.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); pick(); } });
1344
- })(cards[j]);
1345
- }
1346
- }
1488
+ })(cards[j]);
1489
+ }
1490
+ var fleetButtons=el.querySelectorAll('[data-fleet-action]');
1491
+ for(var fb=0;fb<fleetButtons.length;fb++)(function(btn){btn.addEventListener('click',function(ev){ev.stopPropagation();var act=btn.getAttribute('data-fleet-action'),name=btn.getAttribute('data-robot');if(act==='camera'){selectTab('park');Park.openDrawerForRobot(name);}else{selectTab('robopark');setRpSubTab(act==='sessions'?'live':'incidents');}});})(fleetButtons[fb]);
1492
+ }
1347
1493
 
1348
1494
  function renderSummary(s){
1349
1495
  var nodes = s.nodes||[];
@@ -1623,7 +1769,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1623
1769
  btn.addEventListener('click', function(){ selectTab(btn.getAttribute('data-tab')); });
1624
1770
  })(tabs[ti]);
1625
1771
  }
1626
- function selectTab(name){
1772
+ function selectTab(name){
1627
1773
  activeTab = name;
1628
1774
  for (var i=0;i<tabs.length;i++) tabs[i].classList.toggle('active', tabs[i].getAttribute('data-tab')===name);
1629
1775
  $('pane-fleet').classList.toggle('active', name==='fleet');
@@ -1634,8 +1780,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
1634
1780
  pollRobopark();
1635
1781
  initRpSubTabs();
1636
1782
  }
1637
- if (name==='park') Park.activate(); else Park.deactivate();
1638
- }
1783
+ if (name==='park') Park.activate(); else Park.deactivate();
1784
+ }
1785
+ var fleetParkBtn=$('fleet-open-park'); if(fleetParkBtn) fleetParkBtn.addEventListener('click',function(){selectTab('park');});
1786
+ var fleetSessionsBtn=$('fleet-open-sessions'); if(fleetSessionsBtn) fleetSessionsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('live');});
1787
+ var fleetIncidentsBtn=$('fleet-open-incidents'); if(fleetIncidentsBtn) fleetIncidentsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('incidents');});
1788
+ var fleetRefreshBtn=$('fleet-refresh'); if(fleetRefreshBtn) fleetRefreshBtn.addEventListener('click',function(){poll();pollRobopark();});
1639
1789
  var initHash = location.hash.replace('#','');
1640
1790
  // Defer tab init until Park module is defined (same outer IIFE, later in source).
1641
1791
  function initTab(){
@@ -2038,8 +2188,66 @@ button.act:disabled{opacity:0.5;cursor:default;}
2038
2188
  }
2039
2189
  $('srv-add').addEventListener('click', addServer);
2040
2190
  $('rb-add').addEventListener('click', addRobot);
2041
- $('rp-addvoice').addEventListener('click', function(){ addVoiceStackFromTemplate(); });
2042
- $('rp-addchar').addEventListener('click', function(){ addCharacterFromTemplate(); });
2191
+ $('rp-addvoice').addEventListener('click', function(){ openVoiceStackManager(); });
2192
+ $('rp-addchar').addEventListener('click', function(){ openCharacterManager(); });
2193
+ var newVoiceStackBtn=$('rp-new-voice-stack'); if(newVoiceStackBtn) newVoiceStackBtn.addEventListener('click',function(){ openVoiceStackManager(); });
2194
+ var newCharacterBtn=$('rp-new-character'); if(newCharacterBtn) newCharacterBtn.addEventListener('click',function(){ openCharacterManager(); });
2195
+
2196
+ function configOption(value,label,current){ return '<option value="'+esc(value)+'"'+(String(value)===String(current)?' selected':'')+'>'+esc(label||value)+'</option>'; }
2197
+ function configNumber(id,fallback){ var el=$(id), n=el?Number(el.value):NaN; return Number.isFinite(n)?n:fallback; }
2198
+ function voiceStackDraft(){
2199
+ return {
2200
+ id:$('cfg-vs-id').value.trim(),name:$('cfg-vs-name').value.trim(),
2201
+ stt_provider:$('cfg-vs-stt-provider').value,stt_model:$('cfg-vs-stt-model').value.trim(),stt_language:$('cfg-vs-stt-language').value.trim()||'en',
2202
+ llm_provider:$('cfg-vs-llm-provider').value,llm_model:$('cfg-vs-llm-model').value.trim(),llm_api_key:$('cfg-vs-llm-key').value.trim()||null,
2203
+ temperature:configNumber('cfg-vs-temperature',0.7),num_ctx:configNumber('cfg-vs-num-ctx',8192),
2204
+ tts_provider:$('cfg-vs-tts-provider').value,tts_voice:$('cfg-vs-tts-voice').value.trim(),tts_language:$('cfg-vs-tts-language').value.trim()||'en',
2205
+ max_turns:configNumber('cfg-vs-max-turns',20),min_endpointing_delay:configNumber('cfg-vs-endpointing',0.7),
2206
+ allow_interruptions:$('cfg-vs-interruptions').checked,wake_word_enabled:$('cfg-vs-wake-enabled').checked,
2207
+ wake_word_model:$('cfg-vs-wake-model').value.trim()||null,wake_word_threshold:configNumber('cfg-vs-wake-threshold',0.5),wake_word_timeout:configNumber('cfg-vs-wake-timeout',3)
2208
+ };
2209
+ }
2210
+ function openVoiceStackManager(id,draft){
2211
+ var existing=null;
2212
+ for(var i=0;i<voiceStacks.length;i++) if(String(voiceStacks[i].id)===String(id)){ existing=voiceStacks[i]; break; }
2213
+ var s=draft||existing||{id:'',name:'',stt_provider:'groq',stt_model:'whisper-large-v3-turbo',stt_language:'en',llm_provider:'ollama',llm_model:'gemma3:27b',temperature:0.7,num_ctx:8192,tts_provider:'elevenlabs',tts_voice:'',tts_language:'en',max_turns:20,min_endpointing_delay:0.7,allow_interruptions:true,wake_word_enabled:false,wake_word_threshold:0.5,wake_word_timeout:3};
2214
+ var editing=!!existing;
2215
+ var html='<div class="rp-config-editor">'
2216
+ +'<div class="rp-config-editor-intro"><span class="mark">VOX</span><div><b>'+(editing?'Edit production voice stack':'Create production voice stack')+'</b><span>A stack is the complete conversational audio pipeline. Robot assignments keep referencing this ID when settings change.</span></div></div>'
2217
+ +'<div class="rp-config-section"><div class="rp-config-section-head">Identity <span>stable assignment key</span></div><div class="rp-config-fields"><div class="rp-field"><label>Stack ID</label><input id="cfg-vs-id" value="'+esc(s.id||'')+'" placeholder="english-fast"'+(editing?' disabled':'')+'></div><div class="rp-field"><label>Display name</label><input id="cfg-vs-name" value="'+esc(s.name||'')+'" placeholder="English fast"></div></div></div>'
2218
+ +'<div class="rp-config-section"><div class="rp-config-section-head">Speech recognition <span>audio to text</span></div><div class="rp-config-fields"><div class="rp-field"><label>STT provider</label><select id="cfg-vs-stt-provider">'+configOption('groq','Groq',s.stt_provider)+configOption('speaches','Speaches / local',s.stt_provider)+configOption('vibevoice','VibeVoice',s.stt_provider)+'</select></div><div class="rp-field"><label>STT model</label><input id="cfg-vs-stt-model" value="'+esc(s.stt_model||'')+'"></div><div class="rp-field"><label>Language</label><input id="cfg-vs-stt-language" value="'+esc(s.stt_language||'en')+'" placeholder="en or auto"></div></div></div>'
2219
+ +'<div class="rp-config-section"><div class="rp-config-section-head">Intelligence <span>prompt execution and response generation</span></div><div class="rp-config-fields"><div class="rp-field"><label>LLM provider</label><select id="cfg-vs-llm-provider">'+configOption('ollama','Ollama / Ollama Cloud',s.llm_provider)+configOption('groq','Groq',s.llm_provider)+configOption('gemini','Gemini',s.llm_provider)+configOption('moltbot','Moltbot',s.llm_provider)+'</select></div><div class="rp-field"><label>LLM model</label><input id="cfg-vs-llm-model" value="'+esc(s.llm_model||'')+'"></div><div class="rp-field"><label>Temperature</label><input id="cfg-vs-temperature" type="number" min="0" max="2" step="0.1" value="'+esc(s.temperature!=null?s.temperature:0.7)+'"></div><div class="rp-field"><label>Context size</label><input id="cfg-vs-num-ctx" type="number" min="512" step="512" value="'+esc(s.num_ctx!=null?s.num_ctx:8192)+'"></div><div class="rp-field wide"><label>Provider API key override</label><input id="cfg-vs-llm-key" type="password" value="'+esc(s.llm_api_key||'')+'" placeholder="Leave blank to use the ROBOVOICE agent secret"></div></div></div>'
2220
+ +'<div class="rp-config-section"><div class="rp-config-section-head">Voice synthesis <span>text to robot speech</span></div><div class="rp-config-fields"><div class="rp-field"><label>TTS provider</label><select id="cfg-vs-tts-provider">'+configOption('elevenlabs','ElevenLabs',s.tts_provider)+configOption('kokoro','Kokoro',s.tts_provider)+configOption('piper','Piper',s.tts_provider)+'</select></div><div class="rp-field"><label>Language</label><input id="cfg-vs-tts-language" value="'+esc(s.tts_language||'en')+'"></div><div class="rp-field wide"><label>Voice ID</label><div class="rp-config-actions"><input id="cfg-vs-tts-voice" style="flex:1" value="'+esc(s.tts_voice||'')+'" placeholder="Choose or paste provider voice ID"><button type="button" class="rp-btn" id="cfg-vs-browse">Browse catalog</button></div></div></div></div>'
2221
+ +'<div class="rp-config-section"><div class="rp-config-section-head">Conversation behavior <span>latency and turn-taking</span></div><div class="rp-config-fields"><div class="rp-field"><label>Max turns</label><input id="cfg-vs-max-turns" type="number" min="1" value="'+esc(s.max_turns!=null?s.max_turns:20)+'"></div><div class="rp-field"><label>Endpointing delay (seconds)</label><input id="cfg-vs-endpointing" type="number" min="0.1" step="0.1" value="'+esc(s.min_endpointing_delay!=null?s.min_endpointing_delay:0.7)+'"></div><div class="rp-config-checks wide"><label><input id="cfg-vs-interruptions" type="checkbox"'+(s.allow_interruptions!==false?' checked':'')+'>Allow natural interruptions</label><label><input id="cfg-vs-wake-enabled" type="checkbox"'+(s.wake_word_enabled?' checked':'')+'>Require wake word</label></div></div></div>'
2222
+ +'<div class="rp-config-section"><div class="rp-config-section-head">Wake word <span>optional local activation gate</span></div><div class="rp-config-fields"><div class="rp-field wide"><label>Model path</label><input id="cfg-vs-wake-model" value="'+esc(s.wake_word_model||'')+'" placeholder="models/hey_jarvis.onnx"></div><div class="rp-field"><label>Threshold</label><input id="cfg-vs-wake-threshold" type="number" min="0" max="1" step="0.05" value="'+esc(s.wake_word_threshold!=null?s.wake_word_threshold:0.5)+'"></div><div class="rp-field"><label>Timeout (seconds)</label><input id="cfg-vs-wake-timeout" type="number" min="0" step="0.5" value="'+esc(s.wake_word_timeout!=null?s.wake_word_timeout:3)+'"></div></div></div>'
2223
+ +'<div class="rp-config-editor-foot"><button class="rp-btn" id="cfg-vs-cancel">Cancel</button><button class="rp-btn primary" id="cfg-vs-save">'+(editing?'Save changes':'Create stack')+'</button></div></div>';
2224
+ showModal(editing?'Edit voice stack · '+(s.name||s.id):'New voice stack',html); $('rp-modal').classList.add('config-modal');
2225
+ $('cfg-vs-cancel').addEventListener('click',closeModal);
2226
+ $('cfg-vs-browse').addEventListener('click',function(){ var next=voiceStackDraft(); openTtsVoicePicker(next.tts_provider,function(voiceId){ next.tts_voice=voiceId; openVoiceStackManager(editing?id:null,next); }); });
2227
+ $('cfg-vs-save').addEventListener('click',function(){
2228
+ var body=voiceStackDraft(); if(!body.name){ showOut('Voice stack name is required'); return; }
2229
+ if(!editing && !body.id) body.id=body.name.toLowerCase().replace(/[^a-z0-9_-]+/g,'-').replace(/^-|-$/g,'').slice(0,48);
2230
+ var btn=$('cfg-vs-save'); btn.disabled=true; btn.textContent='Saving…';
2231
+ rpAction(editing?'PUT':'POST',editing?'/robopark/api/voice-stacks/'+encodeURIComponent(id):'/robopark/api/voice-stacks',body).then(function(){ closeModal(); showOut((editing?'Updated ':'Created ')+body.name); return loadVoiceStacks(); }).then(function(){ pollRobopark(); if(Park&&Park.refreshDrawer) Park.refreshDrawer(); }).catch(function(e){ btn.disabled=false; btn.textContent=editing?'Save changes':'Create stack'; showOut('voice stack: '+e.message); });
2232
+ });
2233
+ }
2234
+
2235
+ function characterDraft(){
2236
+ return {id:$('cfg-cp-id').value.trim(),name:$('cfg-cp-name').value.trim(),description:$('cfg-cp-description').value.trim()||null,system_prompt:$('cfg-cp-prompt').value.trim()||null,voice_stack_id:$('cfg-cp-stack').value||null,motors:$('cfg-cp-motors').value.split(',').map(function(x){return x.trim();}).filter(Boolean),nx:configNumber('cfg-cp-nx',0.5),ny:configNumber('cfg-cp-ny',0.5),img:$('cfg-cp-img').value.trim()||null};
2237
+ }
2238
+ function openCharacterManager(id,draft){
2239
+ var existing=null; for(var i=0;i<characterPresets.length;i++) if(String(characterPresets[i].id)===String(id)){ existing=characterPresets[i]; break; }
2240
+ var p=draft||existing||{id:'',name:'',description:'',system_prompt:'',voice_stack_id:'',motors:['drive'],nx:0.5,ny:0.5,img:''}, editing=!!existing, stackOptions='<option value="">No default stack</option>';
2241
+ for(var j=0;j<voiceStacks.length;j++) stackOptions+=configOption(voiceStacks[j].id,voiceStacks[j].name||voiceStacks[j].id,p.voice_stack_id);
2242
+ var html='<div class="rp-config-editor">'
2243
+ +'<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>'
2244
+ +'<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>'
2245
+ +'<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>'
2246
+ +'<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>'
2247
+ +'<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>';
2248
+ showModal(editing?'Edit character · '+(p.name||p.id):'New character preset',html); $('rp-modal').classList.add('config-modal'); $('cfg-cp-cancel').addEventListener('click',closeModal);
2249
+ $('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);}); });
2250
+ }
2043
2251
 
2044
2252
  function addVoiceStackFromTemplate(){
2045
2253
  var name=$('rp-newvoice-name').value.trim(); if(!name){ showOut('Voice stack name required'); return; }
@@ -2051,25 +2259,28 @@ button.act:disabled{opacity:0.5;cursor:default;}
2051
2259
  var id=name.toLowerCase().replace(/[^a-z0-9_-]+/g,'-').replace(/^-|-$/g,'').slice(0,32);
2052
2260
  rpAction('POST','/robopark/api/character-presets',{id:id,name:name,voice_stack_id:voice_stack_id,motors:['drive']}).then(function(j){ showOut('character preset added: '+esc(j.id||id)); $('rp-newchar-name').value=''; $('rp-newchar-stack').value=''; loadCharacterPresets(); loadParkSlots(); }).catch(function(e){ showOut('add character preset: '+e.message); });
2053
2261
  }
2054
- function fillTemplateVoiceList(){
2055
- var el=$('rp-voice-list'); if(!el) return;
2056
- var html='';
2057
- for(var i=0;i<voiceStacks.length;i++){ var s=voiceStacks[i]; html += '<div class="rp-card" data-vsid="'+esc(s.id)+'" style="padding:0.5rem 0.6rem;border-left-color:var(--info);cursor:pointer;"><div style="font-family:var(--mono);font-size:0.74rem;font-weight:600;">'+esc(s.name||s.id)+'</div><div style="font-size:0.6rem;color:var(--muted);margin-top:0.15rem;">'+esc(s.stt_provider)+' '+esc(s.llm_provider)+'/'+esc(s.llm_model||'')+' '+esc(s.tts_provider)+'</div></div>'; }
2058
- if(!voiceStacks.length) html='<div class="pk-note">no voice stacks loaded</div>';
2059
- el.innerHTML=html;
2060
- var dl=$('rp-newchar-stack-list'); if(dl){ dl.innerHTML=''; for(var j=0;j<voiceStacks.length;j++){ var o=document.createElement('option'); o.value=voiceStacks[j].id; dl.appendChild(o); } }
2061
- }
2062
- function fillTemplateCharacterList(){
2063
- var el=$('rp-char-list'); if(!el) return;
2064
- var html='';
2065
- for(var i=0;i<characterPresets.length;i++){ var p=characterPresets[i]; html += '<div class="rp-card" data-cpid="'+esc(p.id)+'" style="padding:0.5rem 0.6rem;cursor:pointer;"><div style="font-family:var(--mono);font-size:0.74rem;font-weight:600;">'+esc(p.name||p.id)+'</div><div style="font-size:0.6rem;color:var(--muted);margin-top:0.15rem;">stack: '+esc(p.voice_stack_id||'')+' · motors: '+esc((p.motors||[]).join(', '))+(p.system_prompt?' · has prompt':' · no prompt set')+'</div></div>'; }
2066
- if(!characterPresets.length) html='<div class="pk-note">no character presets loaded</div>';
2067
- el.innerHTML=html;
2068
- }
2262
+ function fillTemplateVoiceList(){
2263
+ var el=$('rp-voice-list'); if(!el) return;
2264
+ var query=String(($('rp-voice-search')&&$('rp-voice-search').value)||'').trim().toLowerCase(), html='',shown=0;
2265
+ for(var i=0;i<voiceStacks.length;i++){ var s=voiceStacks[i], hay=[s.id,s.name,s.stt_provider,s.stt_model,s.llm_provider,s.llm_model,s.tts_provider,s.tts_voice].join(' ').toLowerCase(); if(query&&hay.indexOf(query)<0) continue; shown++; html += '<article class="rp-config-card" data-vsid="'+esc(s.id)+'"><div class="rp-config-card-head"><div class="rp-config-card-title"><b>'+esc(s.name||s.id)+'</b><small>'+esc(s.id)+' · '+esc(s.tts_voice||'voice not selected')+'</small></div><span class="rp-config-badge '+(s.allow_interruptions?'ok':'')+'">'+(s.allow_interruptions?'interruptible':'fixed turn')+'</span></div><div class="rp-config-pipeline"><span>'+esc(s.stt_provider||'STT')+' / '+esc(s.stt_model||'default')+'</span><i>→</i><span>'+esc(s.llm_provider||'LLM')+' / '+esc(s.llm_model||'default')+'</span><i>→</i><span>'+esc(s.tts_provider||'TTS')+'</span></div><div class="rp-config-badges"><span class="rp-config-badge gold">'+esc(s.stt_language||'auto')+'</span><span class="rp-config-badge">'+esc(String(s.min_endpointing_delay!=null?s.min_endpointing_delay:0.7))+'s endpoint</span><span class="rp-config-badge">'+esc(String(s.max_turns||20))+' turns</span>'+(s.wake_word_enabled?'<span class="rp-config-badge ok">wake word</span>':'')+'</div><div class="rp-config-actions"><button class="rp-btn primary" data-config-edit-stack="'+esc(s.id)+'">Edit stack</button><button class="rp-btn" data-config-use-stack="'+esc(s.id)+'">Open in Voice Studio</button></div></article>'; }
2266
+ if(!shown) html='<div class="rp-voice-picker-empty">'+(query?'No voice stacks match this search.':'No voice stacks yet. Create the first production pipeline.')+'</div>';
2267
+ el.innerHTML=html;
2268
+ var count=$('rp-voice-count'); if(count) count.textContent=voiceStacks.length+' stack'+(voiceStacks.length===1?'':'s');
2269
+ var dl=$('rp-newchar-stack-list'); if(dl){ dl.innerHTML=''; for(var j=0;j<voiceStacks.length;j++){ var o=document.createElement('option'); o.value=voiceStacks[j].id; dl.appendChild(o); } }
2270
+ }
2271
+ function fillTemplateCharacterList(){
2272
+ var el=$('rp-char-list'); if(!el) return;
2273
+ var query=String(($('rp-char-search')&&$('rp-char-search').value)||'').trim().toLowerCase(), html='',shown=0;
2274
+ for(var i=0;i<characterPresets.length;i++){ var p=characterPresets[i], hay=[p.id,p.name,p.description,p.system_prompt,p.voice_stack_id,(p.motors||[]).join(' ')].join(' ').toLowerCase(); if(query&&hay.indexOf(query)<0) continue; shown++; var avatar=p.img||rpAvatarFor(p.id||p.name); html += '<article class="rp-config-card" data-cpid="'+esc(p.id)+'"><div class="rp-config-card-head">'+(avatar?'<img src="'+esc(avatar+QS)+'" alt="'+esc(p.name||p.id)+'">':'')+'<div class="rp-config-card-title"><b>'+esc(p.name||p.id)+'</b><small>'+esc(p.id)+' · '+esc(p.description||'No operator description')+'</small></div><span class="rp-config-badge '+(p.system_prompt?'ok':'')+'">'+(p.system_prompt?'prompt ready':'prompt missing')+'</span></div><div class="rp-config-summary">'+esc(p.system_prompt||'Add a system prompt to define this character’s speaking style, constraints, and behavior.')+'</div><div class="rp-config-badges"><span class="rp-config-badge gold">'+esc(p.voice_stack_id||'default stack')+'</span>'+((p.motors||[]).map(function(m){return '<span class="rp-config-badge">'+esc(m)+'</span>';}).join(''))+'<span class="rp-config-badge">pad '+Math.round((p.nx||0)*100)+' / '+Math.round((p.ny||0)*100)+'</span></div><div class="rp-config-actions"><button class="rp-btn primary" data-config-edit-character="'+esc(p.id)+'">Edit character</button><button class="rp-btn" data-config-use-character="'+esc(p.id)+'">Open in Voice Studio</button>'+(p.voice_stack_id?'<button class="rp-btn" data-config-edit-stack="'+esc(p.voice_stack_id)+'">Edit voice</button>':'')+'</div></article>'; }
2275
+ if(!shown) html='<div class="rp-voice-picker-empty">'+(query?'No character presets match this search.':'No character presets yet. Create the first park persona.')+'</div>';
2276
+ el.innerHTML=html;
2277
+ var count=$('rp-char-count'); if(count) count.textContent=characterPresets.length+' preset'+(characterPresets.length===1?'':'s');
2278
+ }
2069
2279
 
2070
2280
  // ── voice-stack edit panel ──
2071
- var editingVoiceStackId=null;
2072
- function openVoiceStackEdit(id){
2281
+ var editingVoiceStackId=null;
2282
+ function openVoiceStackEdit(id){
2283
+ openVoiceStackManager(id); return;
2073
2284
  var s=null; for(var i=0;i<voiceStacks.length;i++){ if(voiceStacks[i].id===id){ s=voiceStacks[i]; break; } }
2074
2285
  if(!s) return;
2075
2286
  editingVoiceStackId=id;
@@ -2113,7 +2324,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2113
2324
  };
2114
2325
  rpAction('PUT','/robopark/api/voice-stacks/'+encodeURIComponent(editingVoiceStackId),body).then(function(){ showOut('voice stack saved'); $('rp-voice-edit').style.display='none'; editingVoiceStackId=null; loadVoiceStacks(); }).catch(function(e){ showOut('save voice stack: '+e.message); });
2115
2326
  }
2116
- var vl=$('rp-voice-list'); if(vl) vl.addEventListener('click', function(e){ var c=e.target.closest?e.target.closest('.rp-card'):null; if(!c) return; var id=c.getAttribute('data-vsid'); if(id) openVoiceStackEdit(id); });
2327
+ var vl=$('rp-voice-list'); if(vl) vl.addEventListener('click', function(e){ var c=e.target.closest?e.target.closest('.rp-config-card'):null; if(!c) return; var id=c.getAttribute('data-vsid'); if(id&&!e.target.closest('[data-config-use-stack]')) openVoiceStackManager(id); });
2117
2328
  var vsSave=$('rpve-save'); if(vsSave) vsSave.addEventListener('click', saveVoiceStackEdit);
2118
2329
  var vsCancel=$('rpve-cancel'); if(vsCancel) vsCancel.addEventListener('click', function(){ $('rp-voice-edit').style.display='none'; editingVoiceStackId=null; });
2119
2330
  // TTS provider change: re-populate the voice <select> for the new provider.
@@ -2150,13 +2361,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
2150
2361
  });
2151
2362
 
2152
2363
  // ── character-preset edit panel ──
2153
- var editingCharPresetId=null;
2364
+ var editingCharPresetId=null;
2154
2365
  function fillCharEditStackOptions(cur){
2155
2366
  var sel=$('rpce-stack'); if(!sel) return;
2156
2367
  sel.innerHTML='<option value="">— default —</option>';
2157
2368
  for(var i=0;i<voiceStacks.length;i++){ var s=voiceStacks[i]; var o=document.createElement('option'); o.value=s.id; o.textContent=s.name||s.id; if(s.id===cur) o.selected=true; sel.appendChild(o); }
2158
2369
  }
2159
- function openCharPresetEdit(id){
2370
+ function openCharPresetEdit(id){
2371
+ openCharacterManager(id); return;
2160
2372
  var p=null; for(var i=0;i<characterPresets.length;i++){ if(characterPresets[i].id===id){ p=characterPresets[i]; break; } }
2161
2373
  if(!p) return;
2162
2374
  editingCharPresetId=id;
@@ -2181,9 +2393,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
2181
2393
  };
2182
2394
  rpAction('PUT','/robopark/api/character-presets/'+encodeURIComponent(editingCharPresetId),body).then(function(){ showOut('character preset saved'); $('rp-char-edit').style.display='none'; editingCharPresetId=null; loadCharacterPresets(); loadParkSlots(); }).catch(function(e){ showOut('save character preset: '+e.message); });
2183
2395
  }
2184
- var cl=$('rp-char-list'); if(cl) cl.addEventListener('click', function(e){ var c=e.target.closest?e.target.closest('.rp-card'):null; if(!c) return; var id=c.getAttribute('data-cpid'); if(id) openCharPresetEdit(id); });
2396
+ var cl=$('rp-char-list'); if(cl) cl.addEventListener('click', function(e){ var c=e.target.closest?e.target.closest('.rp-config-card'):null; if(!c) return; var id=c.getAttribute('data-cpid'); if(id&&!e.target.closest('[data-config-use-character],[data-config-edit-stack]')) openCharacterManager(id); });
2185
2397
  var cpSave=$('rpce-save'); if(cpSave) cpSave.addEventListener('click', saveCharPresetEdit);
2186
- var cpCancel=$('rpce-cancel'); if(cpCancel) cpCancel.addEventListener('click', function(){ $('rp-char-edit').style.display='none'; editingCharPresetId=null; });
2398
+ var cpCancel=$('rpce-cancel'); if(cpCancel) cpCancel.addEventListener('click', function(){ $('rp-char-edit').style.display='none'; editingCharPresetId=null; });
2399
+ var voiceSearch=$('rp-voice-search'); if(voiceSearch) voiceSearch.addEventListener('input',fillTemplateVoiceList);
2400
+ var charSearch=$('rp-char-search'); if(charSearch) charSearch.addEventListener('input',fillTemplateCharacterList);
2401
+ document.addEventListener('click',function(e){
2402
+ var stackEdit=e.target.closest&&e.target.closest('[data-config-edit-stack]'); if(stackEdit){ e.preventDefault();e.stopPropagation();openVoiceStackManager(stackEdit.getAttribute('data-config-edit-stack'));return; }
2403
+ var charEdit=e.target.closest&&e.target.closest('[data-config-edit-character]'); if(charEdit){ e.preventDefault();e.stopPropagation();openCharacterManager(charEdit.getAttribute('data-config-edit-character'));return; }
2404
+ var stackUse=e.target.closest&&e.target.closest('[data-config-use-stack]'); if(stackUse){ e.preventDefault();e.stopPropagation();setRpSubTab('voice');setTimeout(function(){var s=$('rp-call-stack-select');if(s){s.value=stackUse.getAttribute('data-config-use-stack');renderVoiceServices();}},0);return; }
2405
+ var charUse=e.target.closest&&e.target.closest('[data-config-use-character]'); if(charUse){ e.preventDefault();e.stopPropagation();setRpSubTab('voice');setTimeout(function(){var s=$('rp-call-character-select');if(s){s.value=charUse.getAttribute('data-config-use-character');renderVoiceCharacter();renderVoiceServices();}},0); }
2406
+ });
2187
2407
 
2188
2408
  function renderRobotCardSelect(name, current){
2189
2409
  var sel=$('rp-char-'+name); if(!sel) return;
@@ -2213,8 +2433,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
2213
2433
  grid.innerHTML = '<div class="empty" style="grid-column:1/-1">'+esc(d.error||'no scheduler linked')+' — start the hub with <span class="mono">--scheduler-url http://&lt;scheduler&gt;:8080</span></div>';
2214
2434
  return;
2215
2435
  }
2216
- var robots = robotsFrom(d);
2217
- var tot = (d && d.totals) || {};
2436
+ var robots = robotsFrom(d);
2437
+ renderFleetOperations([], 'rp-fleet', robots);
2438
+ var tot = (d && d.totals) || {};
2218
2439
  var totalSessions = tot.sessions_total!=null?tot.sessions_total:robots.reduce(function(a,x){return a+(x.sessions_total||0);},0);
2219
2440
  var totalTrig = tot.trigger_count!=null?tot.trigger_count:robots.reduce(function(a,x){return a+(x.trigger_count||0);},0);
2220
2441
  totals.innerHTML =
@@ -2242,8 +2463,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
2242
2463
  + '<div class="rp-m"><span class="v '+dcls+'">'+drop+'</span><span class="l">drop rate</span></div>'
2243
2464
  + '</div><div class="rp-actions">'
2244
2465
  + '<button class="rp-btn" data-act="trigger" data-robot="'+idAttr+'"'+(rpReadOnly?' disabled':'')+'>Trigger motion</button>'
2245
- + '<button class="rp-btn" data-act="preview" data-robot="'+idAttr+'">Preview</button>'
2246
- + '<button class="rp-btn danger" data-act="end" data-robot="'+idAttr+'"'+((rpReadOnly||!active)?' disabled':'')+'>End session</button>'
2466
+ + '<button class="rp-btn" data-act="preview" data-robot="'+idAttr+'">Preview</button>'
2467
+ + '<button class="rp-btn danger" data-act="end" data-robot="'+idAttr+'"'+((rpReadOnly||!active)?' disabled':'')+'>End session</button>'
2468
+ + ((x.character_id||x.character_preset_id)?'<button class="rp-btn" data-config-edit-character="'+esc(x.character_id||x.character_preset_id)+'">Edit character</button>':'')
2469
+ + (x.voice_stack_id?'<button class="rp-btn" data-config-edit-stack="'+esc(x.voice_stack_id)+'">Edit voice</button>':'')
2247
2470
  + '</div>'
2248
2471
  + '<div class="pk-note '+readinessClass+'" style="margin-top:0.5rem;color:'+(readinessBad?'var(--bad)':'var(--ok)')+'">'+esc(readiness.replace(/_/g,' '))+' · '+esc(x.readiness_message||'no readiness detail')+'</div>'
2249
2472
  + (readiness==='stuck_session'||readiness==='stale_session'||readiness==='join_timeout'?'<div class="rp-actions"><button class="rp-btn danger" data-act="recover" data-robot="'+idAttr+'"'+(rpReadOnly?' disabled':'')+'>Recover robot</button></div>':'')
@@ -2338,7 +2561,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2338
2561
  s.classList.toggle('active', s.id==='rp-sub-'+name);
2339
2562
  }
2340
2563
  // stop every sub-tab interval, then start only the active one
2341
- var names = ['overview','incidents','live','voice','history','audit','fleet','setup'];
2564
+ var names = ['overview','incidents','live','voice','history','audit','fleet','config','setup'];
2342
2565
  for(var k=0;k<names.length;k++){
2343
2566
  var n=names[k];
2344
2567
  if(rpIntervals[n]){ clearInterval(rpIntervals[n]); rpIntervals[n]=null; }
@@ -2365,7 +2588,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2365
2588
  var h=String(location.hash||'').replace('#','');
2366
2589
  var m = h.match(/^robopark\\/(.+)$/);
2367
2590
  var initial = (m && m[1]) ? m[1] : 'overview';
2368
- if(['overview','incidents','live','voice','history','audit','fleet','setup'].indexOf(initial)<0) initial='overview';
2591
+ if(['overview','incidents','live','voice','history','audit','fleet','config','setup'].indexOf(initial)<0) initial='overview';
2369
2592
  setRpSubTab(initial);
2370
2593
  }
2371
2594
 
@@ -2377,12 +2600,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
2377
2600
  $('rp-modal-body').innerHTML = bodyHtml || '';
2378
2601
  back.classList.add('open');
2379
2602
  }
2380
- function closeModal(){
2381
- var back = $('rp-modal-back');
2382
- if(!back) return;
2383
- back.classList.remove('open');
2384
- $('rp-modal-body').innerHTML = '';
2385
- }
2603
+ function closeModal(){
2604
+ var back = $('rp-modal-back');
2605
+ if(!back) return;
2606
+ back.classList.remove('open');
2607
+ var modal=$('rp-modal'); if(modal) modal.classList.remove('config-modal');
2608
+ $('rp-modal-body').innerHTML = '';
2609
+ }
2386
2610
  // wire the close handlers (once)
2387
2611
  (function(){
2388
2612
  var closeBtn = $('rp-modal-close');
@@ -2542,13 +2766,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
2542
2766
  // input. Bound once on the document so re-opening the modal doesn't
2543
2767
  // stack listeners.
2544
2768
  document.addEventListener('click', function(e){
2545
- var btn = e.target.closest && e.target.closest('.rp-vp-item');
2546
- if(btn){
2547
- var vid = btn.getAttribute('data-voice-id');
2548
- if(vid && ttsPickerState.onPick){ ttsPickerState.onPick(vid); }
2549
- closeModal();
2550
- return;
2551
- }
2769
+ var btn = e.target.closest && e.target.closest('.rp-vp-item');
2770
+ if(btn){
2771
+ var vid = btn.getAttribute('data-voice-id');
2772
+ closeModal();
2773
+ if(vid && ttsPickerState.onPick){ ttsPickerState.onPick(vid); }
2774
+ return;
2775
+ }
2552
2776
  var r = e.target.closest && e.target.closest('#rp-voice-picker-refresh');
2553
2777
  if(r){
2554
2778
  // Re-fetch the catalog, ignore cache, then re-render.
@@ -2714,18 +2938,46 @@ button.act:disabled{opacity:0.5;cursor:default;}
2714
2938
  wrap.appendChild(div); wrap.scrollTop=wrap.scrollHeight;
2715
2939
  rpVoiceCall.messages++; if($('rp-call-message-count'))$('rp-call-message-count').textContent=rpVoiceCall.messages+' message'+(rpVoiceCall.messages===1?'':'s');
2716
2940
  }
2941
+ var VOICE_DEVICE_PREFS_KEY='rp-voice-device-prefs-v1';
2942
+ function voiceDevicePrefs(){try{return JSON.parse(localStorage.getItem(VOICE_DEVICE_PREFS_KEY)||'{}')||{};}catch(_){return {};}}
2943
+ function saveVoiceDevicePrefs(){try{localStorage.setItem(VOICE_DEVICE_PREFS_KEY,JSON.stringify({audioinput:$('rp-call-mic-device').value||'',videoinput:$('rp-call-cam-device').value||'',audiooutput:$('rp-call-speaker-device').value||''}));}catch(_){}}
2944
+ function voiceDeviceMessage(text,state){var note=$('rp-call-device-note');if(note){note.textContent=text;note.className='rp-call-device-note '+(state||'');}}
2945
+ function voiceMediaSupportError(){
2946
+ if(!window.isSecureContext && location.hostname!=='localhost' && location.hostname!=='127.0.0.1') return 'Browser microphone access is blocked on insecure HTTP. Open this control center over HTTPS (or localhost), then scan again.';
2947
+ if(!navigator.mediaDevices) return 'This browser does not expose media devices. Use current Edge or Chrome over HTTPS.';
2948
+ if(!navigator.mediaDevices.getUserMedia) return 'Microphone capture is unavailable in this browser context.';
2949
+ return '';
2950
+ }
2717
2951
  function fillDeviceSelect(id,devices,kind){
2718
- var sel=$(id);if(!sel)return;var current=sel.value,rows=devices.filter(function(d){return d.kind===kind;});
2719
- sel.innerHTML='<option value="">system default</option>'+rows.map(function(d,i){return '<option value="'+esc(d.deviceId)+'">'+esc(d.label||kind+' '+(i+1))+'</option>';}).join('');if(current)sel.value=current;
2952
+ var sel=$(id);if(!sel)return 0;var prefs=voiceDevicePrefs(),current=sel.value||prefs[kind]||'',rows=devices.filter(function(d){return d.kind===kind;});
2953
+ var fallback=kind==='audioinput'?'System default microphone':kind==='videoinput'?'System default camera':'System default speaker';
2954
+ sel.innerHTML='<option value="">'+fallback+'</option>'+rows.filter(function(d){return d.deviceId&&d.deviceId!=='default';}).map(function(d,i){var label=d.label||((kind==='audioinput'?'Microphone ':kind==='videoinput'?'Camera ':'Speaker ')+(i+1));return '<option value="'+esc(d.deviceId)+'">'+esc(label)+'</option>';}).join('');
2955
+ if(current){sel.value=current;if(sel.value!==current)sel.value='';} return rows.length;
2720
2956
  }
2721
- function refreshBrowserDevices(requestPermission){
2722
- if(!navigator.mediaDevices||!navigator.mediaDevices.enumerateDevices){$('rp-call-device-status').textContent='unsupported';return Promise.resolve([]);}
2957
+ function refreshBrowserDevices(requestPermission,includeCamera){
2958
+ var unsupported=voiceMediaSupportError();
2959
+ if(unsupported){$('rp-call-device-status').textContent='blocked';voiceDeviceMessage(unsupported,'bad');return requestPermission?Promise.reject(new Error(unsupported)):Promise.resolve([]);}
2960
+ $('rp-call-device-status').textContent=requestPermission?'requesting access…':'scanning…';
2723
2961
  var permission=Promise.resolve();
2724
- if(requestPermission)permission=navigator.mediaDevices.getUserMedia({audio:true}).then(function(stream){stream.getTracks().forEach(function(t){t.stop();});}).catch(function(e){setVoiceEvent('Microphone',e.message||'permission denied');});
2725
- return permission.then(function(){return navigator.mediaDevices.enumerateDevices();}).then(function(devices){fillDeviceSelect('rp-call-mic-device',devices,'audioinput');fillDeviceSelect('rp-call-cam-device',devices,'videoinput');fillDeviceSelect('rp-call-speaker-device',devices,'audiooutput');$('rp-call-device-status').textContent=devices.length+' detected';return devices;});
2962
+ if(requestPermission){
2963
+ var selected=$('rp-call-mic-device').value, audio=selected?{deviceId:{exact:selected}}:true;
2964
+ permission=navigator.mediaDevices.getUserMedia({audio:audio}).then(function(stream){stream.getTracks().forEach(function(t){t.stop();});if(includeCamera)return navigator.mediaDevices.getUserMedia({video:true}).then(function(cam){cam.getTracks().forEach(function(t){t.stop();});}).catch(function(){return null;});});
2965
+ }
2966
+ return permission.then(function(){return navigator.mediaDevices.enumerateDevices();}).then(function(devices){
2967
+ var mic=fillDeviceSelect('rp-call-mic-device',devices,'audioinput'),cam=fillDeviceSelect('rp-call-cam-device',devices,'videoinput'),speaker=fillDeviceSelect('rp-call-speaker-device',devices,'audiooutput');
2968
+ saveVoiceDevicePrefs();$('rp-call-device-status').textContent=mic+' mic · '+cam+' cam · '+speaker+' out';
2969
+ voiceDeviceMessage('Ready: '+mic+' microphone'+(mic===1?'':'s')+', '+cam+' camera'+(cam===1?'':'s')+', and '+speaker+' speaker output'+(speaker===1?'':'s')+' detected. Device choices are saved in this browser.','ok');return devices;
2970
+ }).catch(function(e){var message=e&&e.name==='NotAllowedError'?'Media permission was denied. Allow microphone access in the browser address bar and scan again.':(e&&e.message||'Media device scan failed');$('rp-call-device-status').textContent='permission failed';voiceDeviceMessage(message,'bad');setVoiceEvent('Browser media',message);throw new Error(message);});
2971
+ }
2972
+ function switchVoiceDevice(kind,id){saveVoiceDevicePrefs();if(!rpVoiceCall.room||!id)return Promise.resolve();return rpVoiceCall.room.switchActiveDevice(kind,id).catch(function(e){setVoiceEvent('Device switch failed',e.message);voiceDeviceMessage('Could not switch device: '+e.message,'bad');});}
2973
+ function applyVoiceOutputDevice(){saveVoiceDevicePrefs();var id=$('rp-call-speaker-device').value;if(!id)return;var els=$('rp-call-audio').querySelectorAll('audio'),supported=false;for(var i=0;i<els.length;i++)if(typeof els[i].setSinkId==='function'){supported=true;els[i].setSinkId(id).catch(function(e){voiceDeviceMessage('Speaker selection failed: '+e.message,'bad');});}if(!supported&&els.length)voiceDeviceMessage('This browser cannot route call audio to a selected speaker. System default output is active.','warn');}
2974
+ function testVoiceMicrophone(){
2975
+ var btn=$('rp-call-test-mic');btn.disabled=true;btn.textContent='Listening…';
2976
+ refreshBrowserDevices(true,false).then(function(){var id=$('rp-call-mic-device').value,constraints=id?{deviceId:{exact:id}}:true;return navigator.mediaDevices.getUserMedia({audio:constraints});}).then(function(stream){
2977
+ var C=window.AudioContext||window.webkitAudioContext,ctx=new C(),src=ctx.createMediaStreamSource(stream),an=ctx.createAnalyser(),data=new Uint8Array(128),peak=0;an.fftSize=256;src.connect(an);var started=Date.now();
2978
+ return new Promise(function(resolve){function sample(){an.getByteTimeDomainData(data);var sum=0;for(var i=0;i<data.length;i++){var v=(data[i]-128)/128;sum+=v*v;}peak=Math.max(peak,Math.sqrt(sum/data.length));voiceDeviceMessage('Microphone test live · level '+Math.round(peak*300)+'%','ok');if(Date.now()-started<2500)requestAnimationFrame(sample);else{stream.getTracks().forEach(function(t){t.stop();});ctx.close().catch(function(){});resolve(peak);}}sample();});
2979
+ }).then(function(peak){voiceDeviceMessage(peak>.01?'Microphone test passed · signal detected.':'Microphone opened but no signal was detected. Check the selected input and gain.',peak>.01?'ok':'warn');}).catch(function(e){voiceDeviceMessage(e.message||'Microphone test failed','bad');}).finally(function(){btn.disabled=false;btn.textContent='Test microphone';});
2726
2980
  }
2727
- function switchVoiceDevice(kind,id){if(!rpVoiceCall.room||!id)return Promise.resolve();return rpVoiceCall.room.switchActiveDevice(kind,id).catch(function(e){setVoiceEvent('Device switch failed',e.message);});}
2728
- function applyVoiceOutputDevice(){var id=$('rp-call-speaker-device').value;if(!id)return;var els=$('rp-call-audio').querySelectorAll('audio');for(var i=0;i<els.length;i++)if(typeof els[i].setSinkId==='function')els[i].setSinkId(id).catch(function(){});}
2729
2981
  function startVoiceVisualizer(track){
2730
2982
  stopVoiceVisualizer();try{var C=window.AudioContext||window.webkitAudioContext;if(!C||!track||!track.mediaStreamTrack)return;var ctx=new C(),an=ctx.createAnalyser(),source=ctx.createMediaStreamSource(new MediaStream([track.mediaStreamTrack]));an.fftSize=128;source.connect(an);rpVoiceCall.audioContext=ctx;rpVoiceCall.analyser=an;var data=new Uint8Array(an.frequencyBinCount),bars=$('rp-call-bars').querySelectorAll('i');
2731
2983
  function draw(){an.getByteFrequencyData(data);var sum=0;for(var x=0;x<data.length;x++)sum+=data[x];var avg=sum/data.length;for(var i=0;i<bars.length;i++){var idx=Math.floor(i*data.length/bars.length);bars[i].style.height=(12+Math.min(70,data[idx]*.28))+'px';}$('rp-call-visual').classList.toggle('speaking',avg>10);rpVoiceCall.audioRaf=requestAnimationFrame(draw);}draw();
@@ -2759,7 +3011,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2759
3011
  if(!cid){setVoiceCallStatus('select a character first','');return;}
2760
3012
  if(rpVoiceCall.room)return;
2761
3013
  setVoiceCallStatus('requesting microphone...','busy');setVoiceAgentState('Opening channel','requesting browser media permission'); $('rp-call-start').disabled=true;
2762
- refreshBrowserDevices(true).then(function(){return rpAction('POST','/robopark/api/sessions/voice-call',{character_preset_id:cid,voice_stack_id:sid||null,client_name:getOperatorName()||'RoboPark Operator'});})
3014
+ refreshBrowserDevices(true,false).then(function(devices){if(!devices.some(function(d){return d.kind==='audioinput';}))throw new Error('No browser microphone was detected');return rpAction('POST','/robopark/api/sessions/voice-call',{character_preset_id:cid,voice_stack_id:sid||null,client_name:getOperatorName()||'RoboPark Operator'});})
2763
3015
  .then(function(call){
2764
3016
  if(!call)throw new Error('scheduler returned no call');
2765
3017
  rpVoiceCall.session=call.session_id; rpVoiceCall.eventToken=call.event_token; rpVoiceCall.seen={};rpVoiceCall.messages=0;rpVoiceCall.ending=false;
@@ -2779,16 +3031,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
2779
3031
  if(LK.RoomEvent.ParticipantAttributesChanged)room.on(LK.RoomEvent.ParticipantAttributesChanged,function(changed,p){updateAgentFromParticipant(p);});
2780
3032
  room.on(LK.RoomEvent.ParticipantConnected,function(p){updateAgentFromParticipant(p);setVoiceEvent('Agent connected',p.name||p.identity||'ROBOVOICE worker');});
2781
3033
  room.on(LK.RoomEvent.Disconnected,function(){ if(!rpVoiceCall.ending) finishVoiceCall('connection_lost'); });
2782
- return room.connect(call.server_url,call.token).then(function(){var mic=$('rp-call-mic-device').value,opts=mic?{deviceId:mic}:undefined;return room.localParticipant.setMicrophoneEnabled(true,opts);});
3034
+ return room.connect(call.server_url,call.token).then(function(){var mic=$('rp-call-mic-device').value,opts=mic?{deviceId:mic}:undefined;setVoiceEvent('LiveKit connected','publishing '+(mic?'selected microphone':'system default microphone'));return room.localParticipant.setMicrophoneEnabled(true,opts);});
2783
3035
  }).then(function(){
2784
3036
  setVoiceCallStatus('secure channel connected','live');setVoiceAgentState('Listening','speak naturally');setVoiceEvent('Connected','waiting for ROBOVOICE agent');rpVoiceCall.startedAt=Date.now();rpVoiceCall.timer=setInterval(updateVoiceTimer,1000);updateVoiceTimer();
2785
3037
  $('rp-call-mute').disabled=false;$('rp-call-camera').disabled=false;$('rp-call-screen').disabled=false;$('rp-call-end').disabled=false;$('rp-call-message').disabled=false;$('rp-call-send').disabled=false;
2786
3038
  return fetch('/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/joined'+QS,{method:'POST',headers:{'X-RoboPark-Session-Token':rpVoiceCall.eventToken}}).catch(function(){});
2787
3039
  });
2788
3040
  }).catch(function(e){
3041
+ if(window.console)console.error('RoboPark voice call failed',e);
2789
3042
  setVoiceCallStatus('call failed','');setVoiceAgentState('Unable to connect',e&&e.message||String(e));setVoiceEvent('Call failed',e&&e.message||String(e)); $('rp-call-start').disabled=false;
2790
- if(rpVoiceCall.session)rpAction('POST','/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/end?reason=connection_failed');
2791
- if(rpVoiceCall.room){try{rpVoiceCall.room.disconnect();}catch(_){} rpVoiceCall.room=null;}
3043
+ var failedSession=rpVoiceCall.session,failedRoom=rpVoiceCall.room;rpVoiceCall.ending=true;rpVoiceCall.room=null;rpVoiceCall.session=null;rpVoiceCall.eventToken=null;
3044
+ if(failedSession)rpAction('POST','/robopark/api/sessions/'+encodeURIComponent(failedSession)+'/end?reason=connection_failed').catch(function(){});
3045
+ if(failedRoom){try{failedRoom.disconnect();}catch(_){}}
3046
+ setTimeout(function(){rpVoiceCall.ending=false;},300);
2792
3047
  });
2793
3048
  }
2794
3049
  function toggleVoiceMute(){
@@ -2806,12 +3061,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
2806
3061
  var cs=$('rp-call-character-select'),ss=$('rp-call-stack-select'),start=$('rp-call-start'),mute=$('rp-call-mute'),end=$('rp-call-end');
2807
3062
  if(cs)cs.addEventListener('change',paintVoiceCallCharacter); if(start)start.addEventListener('click',startVoiceCall);
2808
3063
  if(ss)ss.addEventListener('change',paintVoiceCallStack);if(mute)mute.addEventListener('click',toggleVoiceMute);if(end)end.addEventListener('click',function(){finishVoiceCall('operator');});
2809
- $('rp-call-camera').addEventListener('click',toggleVoiceCamera);$('rp-call-screen').addEventListener('click',toggleVoiceScreen);$('rp-call-refresh-devices').addEventListener('click',function(){refreshBrowserDevices(true);});
3064
+ $('rp-call-camera').addEventListener('click',toggleVoiceCamera);$('rp-call-screen').addEventListener('click',toggleVoiceScreen);$('rp-call-refresh-devices').addEventListener('click',function(){refreshBrowserDevices(true,true).catch(function(){});});$('rp-call-test-mic').addEventListener('click',testVoiceMicrophone);
3065
+ $('rp-call-edit-character').addEventListener('click',function(){var id=$('rp-call-character-select').value;if(id)openCharacterManager(id);else showOut('Choose a character to edit');});
3066
+ $('rp-call-new-character').addEventListener('click',function(){openCharacterManager();});
3067
+ $('rp-call-edit-stack').addEventListener('click',function(){var s=selectedVoiceStack();if(s)openVoiceStackManager(s.id);else showOut('Choose a voice stack to edit');});
3068
+ $('rp-call-new-stack').addEventListener('click',function(){openVoiceStackManager();});
2810
3069
  $('rp-call-mic-device').addEventListener('change',function(){switchVoiceDevice('audioinput',this.value);});$('rp-call-cam-device').addEventListener('change',function(){switchVoiceDevice('videoinput',this.value);});$('rp-call-speaker-device').addEventListener('change',applyVoiceOutputDevice);
2811
3070
  function toggleTranscript(){rpVoiceCall.transcriptCollapsed=!rpVoiceCall.transcriptCollapsed;$('rp-call-transcript').classList.toggle('collapsed',rpVoiceCall.transcriptCollapsed);$('rp-call-collapse').textContent=rpVoiceCall.transcriptCollapsed?'expand transcript':'collapse transcript';}
2812
3071
  $('rp-call-chat-toggle').addEventListener('click',toggleTranscript);$('rp-call-collapse').addEventListener('click',toggleTranscript);
2813
3072
  $('rp-call-compose').addEventListener('submit',function(e){e.preventDefault();var input=$('rp-call-message'),text=input.value.trim();if(!text)return;$('rp-call-send').disabled=true;sendVoiceMessage(text).then(function(){input.value='';}).catch(function(err){setVoiceEvent('Message failed',err.message);}).finally(function(){$('rp-call-send').disabled=!rpVoiceCall.room;});});
2814
3073
  refreshBrowserDevices(false);
3074
+ if(navigator.mediaDevices&&navigator.mediaDevices.addEventListener)navigator.mediaDevices.addEventListener('devicechange',function(){refreshBrowserDevices(false).catch(function(){});});
2815
3075
  })();
2816
3076
 
2817
3077
  var rpHistoryFilter = { robot: '', endReason: '', range: '7d', search: '' };
@@ -6245,7 +6505,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6245
6505
  +'<img id="pk-vision-img" style="width:100%;height:100%;object-fit:cover;position:absolute;inset:0;display:none;border-radius:inherit" alt="vision feed"/>'
6246
6506
  +'<div class="hud"><div class="rec"><i></i>REC · '+esc(n.name)+'</div><div class="tr" id="pk-camres">— · —</div><div class="br" id="pk-camts">--:--:--</div></div></div>'
6247
6507
  +'<div class="pk-note" id="pk-camnote">seeking live feed…</div>'
6248
- +'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-golive" data-robot="'+esc(n.name)+'">▶ Go live</button><span class="pk-note" id="pk-golivenote" style="margin:0"></span></div>'
6508
+ +'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-golive" data-robot="'+esc(n.name)+'">▶ Go live</button><button class="rp-btn primary" id="pk-camera-fullscreen">Fullscreen operations view</button><span class="pk-note" id="pk-golivenote" style="margin:0"></span></div>'
6249
6509
  +'<label class="pk-prodtoggle"><input type="checkbox" id="pk-prodmode"'+(devForPm.production_mode?' checked':'')+'><span>Production mode — continuous motion-triggered loop, camera always live</span></label>')
6250
6510
  + sect('Remote production test','<div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
6251
6511
  + '<div class="pk-ctl" style="margin-top:0.55rem"><button class="rp-btn primary" id="pk-pipeline-start">Start full test</button><button class="rp-btn danger" id="pk-pipeline-stop">Stop conversation</button></div>'
@@ -6268,7 +6528,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6268
6528
  + sect('⚙ controls','<div class="pk-ctl"><button class="rp-btn danger" id="pk-end" data-robot="'+esc(n.name)+'">End session</button><button class="rp-btn" id="pk-follow">Follow run</button></div><div class="pk-note">end-session is live via the scheduler; cam/mic/speaker fill in as the robot plugin publishes them</div>')
6269
6529
  + sect('🩺 robot services','<div id="pk-svc-list" class="pk-svc-list"><div class="pk-note">waiting for supervisor report…</div></div><div class="pk-note" id="pk-svc-age" style="margin-top:0.35rem;"></div><div class="rp-field" style="margin-top:0.55rem;"><label>Quick run (on the robot, from packages/robopark/scheduler)</label><input id="pk-svc-clicmd" readonly value="python robot_supervisor.py"></div><button class="rp-btn" id="pk-svc-clicmd-copy" style="margin-top:0.35rem;">📋 Copy command</button>')
6270
6530
  + buildShellSection(schedId(n))
6271
- + 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><button class="rp-btn" id="pk-eff-save" data-robot="'+esc(schedId(n))+'" style="margin-top:0.35rem;">Set up on this robot</button>');
6531
+ + 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>');
6272
6532
  b.innerHTML += sect('Camera + audio devices','<div class="rp-field"><label>Search devices</label><input id="pk-device-search" placeholder="camera, USB, microphone, HDMI..." autocomplete="off"></div><div class="rp-field"><label>Camera input</label><select id="pk-video-device"></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input"></select></div><div class="rp-field"><label>Speaker / output inventory</label><select id="pk-audio-output"></select></div><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 device selection + greetings</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
6273
6533
  // Put live hardware selection beside the camera and audio tests,
6274
6534
  // ahead of persona and service administration in the Park drawer.
@@ -6290,6 +6550,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
6290
6550
  vl.addEventListener('click', function(){ tryVisionOverlay(n, true); if(!visionOn) showOut('RoboVision feed unavailable: robot has no reachable LAN/Tailscale address'); });
6291
6551
  gl.after(vl);
6292
6552
  }
6553
+ var fullCam=$('pk-camera-fullscreen'); if(fullCam) fullCam.addEventListener('click',function(){ openCameraOperations(n); });
6554
+ var editCharacter=$('pk-edit-character'); if(editCharacter) editCharacter.addEventListener('click',function(){var id=$('pk-eff-char').value;if(id)openCharacterManager(id);else showOut('Choose a character to edit');});
6555
+ var editStack=$('pk-edit-stack'); if(editStack) editStack.addEventListener('click',function(){var id=$('pk-eff-voice').value;if(id)openVoiceStackManager(id);else showOut('Choose a voice stack to edit');});
6556
+ var newStack=$('pk-new-stack'); if(newStack) newStack.addEventListener('click',function(){openVoiceStackManager();});
6293
6557
  var pm=$('pk-prodmode'); if(pm) pm.addEventListener('change', function(){ if(rpReadOnly){ pm.checked=!pm.checked; showOut('read-only node'); return; } setProductionMode(n, pm.checked); });
6294
6558
  var svcCopy=$('pk-svc-clicmd-copy'); if(svcCopy) svcCopy.addEventListener('click', function(){ var i=$('pk-svc-clicmd'); i.select(); try{document.execCommand('copy');}catch(e){} showOut('command copied — run it on the robot itself'); });
6295
6559
  var svcList=$('pk-svc-list'); if(svcList) svcList.addEventListener('click', function(ev){
@@ -6406,7 +6670,38 @@ button.act:disabled{opacity:0.5;cursor:default;}
6406
6670
  function set(id,v){ var e=$(id); if(e) e.textContent=v; }
6407
6671
  function setFill(id,pct){ var e=$(id); if(!e||pct==null) return; e.style.width=Math.max(2,Math.min(100,pct))+'%'; e.style.background=pct>=75?'var(--bad)':pct>=45?'var(--warn)':'var(--gold-bright)'; }
6408
6672
 
6409
- // ── cam: target a LiveKit track; fall back to a styled preview ──
6673
+ var cameraOpsNode=null,cameraOpsTimer=null;
6674
+ function cameraOpsFeedUrl(n){
6675
+ var ref=n&&(n.displayName||n.name||n.nodeId||n.id); if(!ref)return '';
6676
+ return '/fed/media/robots/'+encodeURIComponent(ref)+'/video_feed'+QS+(QS?'&':'?')+'_='+Date.now();
6677
+ }
6678
+ function refreshCameraOperations(){
6679
+ var n=cameraOpsNode;if(!n)return;var row=schedRow(n)||{},device=schedulerDevice(n)||{},hw=hwByNode[n.nodeId]||{};
6680
+ var readiness=String(row.readiness_code||(!n.connected?'offline':'mesh_online')),active=!!(row.active_session||row.in_session||row.session_active||row.current_session_id||row.status==='running'||row.status==='connecting');
6681
+ var img=$('pk-camera-ops-img'),cameraLive=!!(img&&img.dataset.live==='true'),clockEl=$('pk-camera-ops-clock');if(clockEl)clockEl.textContent=clock(Date.now());
6682
+ var rec=$('pk-camera-ops-rec');if(rec)rec.textContent=(cameraLive?'REC / LIVE':'REC / CONNECTING')+' / '+String(n.displayName||n.name||'robot').toUpperCase();
6683
+ var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>readiness</span></div><div class="metric"><b>'+esc(active?'in session':'standing by')+'</b><span>conversation</span></div><div class="metric"><b>'+(row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'--')+'</b><span>average latency</span></div><div class="metric"><b>'+(row.drop_rate!=null?Math.round(row.drop_rate*100)+'%':'--')+'</b><span>session drops</span></div>';
6684
+ var insights=$('pk-camera-ops-insights');if(insights)insights.innerHTML='<span>Camera relay</span><b class="'+(cameraLive?'ok':'bad')+'">'+(cameraLive?'LIVE':'CONNECTING')+'</b><span>Mesh node</span><b class="'+(n.connected?'ok':'bad')+'">'+(n.connected?'connected':'offline')+'</b><span>Production mode</span><b class="'+(device.production_mode?'ok':'')+'">'+(device.production_mode?'armed':'disabled')+'</b><span>Readiness</span><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>Heartbeat</span><b>'+(row.heartbeat_age_seconds!=null?Math.round(row.heartbeat_age_seconds)+'s ago':'--')+'</b><span>Current room</span><b>'+esc(String(row.current_session_id||row.active_session_id||'none').slice(0,24))+'</b><span>Triggers</span><b>'+esc(row.trigger_count!=null?row.trigger_count:0)+'</b><span>Sessions</span><b>'+esc(row.sessions_total!=null?row.sessions_total:0)+'</b>';
6685
+ var stages=[['Robot online',!!n.connected],['Camera streaming',cameraLive],['Motion detector armed',!!device.production_mode],['Scheduler session',active],['Voice pipeline',readiness==='in_session'||active]],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s){return '<div><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?'ready':'waiting')+'</b></div>';}).join('');
6686
+ var inv=device.device_inventory||{},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>Selected camera</span><b>'+esc(device.video_device||'auto')+'</b><span>Selected microphone</span><b>'+esc(device.audio_device||'default')+'</b>';
6687
+ var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
6688
+ }
6689
+ function reloadCameraOperations(){
6690
+ 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...';}
6691
+ img.onload=function(){img.dataset.live='true';if(empty)empty.style.display='none';refreshCameraOperations();};img.onerror=function(){img.dataset.live='false';if(empty){empty.style.display='block';empty.textContent='camera relay unavailable / retrying';}refreshCameraOperations();};img.src=cameraOpsFeedUrl(cameraOpsNode);refreshCameraOperations();
6692
+ }
6693
+ function openCameraOperations(n){
6694
+ 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);
6695
+ }
6696
+ function closeCameraOperations(){
6697
+ var host=$('pk-camera-ops'),img=$('pk-camera-ops-img');if(host)host.classList.remove('open');document.body.classList.remove('camera-ops-open');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(){});
6698
+ }
6699
+ (function bindCameraOperations(){
6700
+ 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');
6701
+ 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();});
6702
+ })();
6703
+
6704
+ // ── cam: target a LiveKit track; fall back to a styled preview ──
6410
6705
  function startCam(n){
6411
6706
  idleCam('connecting');
6412
6707
  tryVisionOverlay(n, true);