infinicode 2.8.68 → 2.8.70

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.
@@ -29,46 +29,39 @@ export const DASHBOARD_HTML = `<!doctype html>
29
29
  <title>RoboPark Control</title>
30
30
  <style>
31
31
  /* ── premium tech glass theme ── dark is the committed default; light is a frosted toggle ── */
32
- :root{
33
- --bg:#08070F;
34
- --grad: radial-gradient(1100px 680px at 12% -12%, rgba(255,184,64,0.16), transparent 60%),
35
- radial-gradient(900px 620px at 102% -6%, rgba(96,132,255,0.15), transparent 58%),
36
- radial-gradient(700px 700px at 88% 108%, rgba(150,90,255,0.10), transparent 60%),
37
- linear-gradient(162deg,#0A0814 0%,#0B0A17 42%,#070610 100%);
38
- --panel: rgba(28,25,42,0.55); --panel-2: rgba(42,37,60,0.60); --panel-solid:#141020;
39
- --glass-border: rgba(255,255,255,0.10); --glass-hi: rgba(255,255,255,0.06);
40
- --border: rgba(255,255,255,0.10); --border-strong: rgba(255,214,112,0.42);
41
- --ink:#F2ECDC; --muted:#9E93B4;
42
- --gold:#E6B23C; --gold-bright:#FFD86B;
32
+ :root{
33
+ --bg:#050504;
34
+ --grad: radial-gradient(900px 520px at 7% -8%, rgba(255,198,79,0.18), transparent 64%),
35
+ radial-gradient(760px 520px at 98% 4%, rgba(161,105,16,0.16), transparent 62%),
36
+ radial-gradient(650px 520px at 62% 112%, rgba(255,211,123,0.08), transparent 68%),
37
+ linear-gradient(145deg,#030303 0%,#0b0905 46%,#050504 100%);
38
+ --panel: rgba(16,14,10,0.64); --panel-2: rgba(27,23,15,0.68); --panel-solid:#0d0b08;
39
+ --glass-border: rgba(255,224,151,0.14); --glass-hi: rgba(255,241,204,0.09);
40
+ --border: rgba(255,224,151,0.12); --border-strong: rgba(255,205,93,0.52);
41
+ --ink:#F6F0E3; --muted:#A99D87;
42
+ --gold:#C9962E; --gold-bright:#F6D77B; --gold-pale:#FFF0BE; --gold-deep:#78520F;
43
43
  --ok:#6FE0A6; --ok-bg:rgba(28,64,50,0.55); --warn:#F2C14E; --warn-bg:rgba(64,50,20,0.5);
44
44
  --bad:#FF7E6E; --bad-bg:rgba(64,24,20,0.55); --info:#78BBFF; --info-bg:rgba(20,44,66,0.55);
45
- --shadow: 0 10px 34px rgba(0,0,0,0.42);
46
- --mono:ui-monospace,"Cascadia Code",Consolas,monospace;
47
- --sans:system-ui,"Segoe UI",Roboto,sans-serif;
45
+ --shadow: 0 18px 55px rgba(0,0,0,0.56),0 2px 12px rgba(0,0,0,0.36);
46
+ --mono:"Cascadia Code","SFMono-Regular",Consolas,monospace;
47
+ --sans:"Trebuchet MS","Segoe UI",sans-serif;
48
+ --display:"Palatino Linotype","Book Antiqua",Palatino,serif;
48
49
  }
49
50
  @media (prefers-color-scheme:dark){:root{}}
50
51
  :root[data-theme="dark"]{}
51
- :root[data-theme="light"]{
52
- --bg:#EDEAF6;
53
- --grad: radial-gradient(1000px 640px at 10% -10%, rgba(255,200,90,0.28), transparent 60%),
54
- radial-gradient(900px 600px at 104% -6%, rgba(120,150,255,0.22), transparent 58%),
55
- linear-gradient(160deg,#F3EFFB 0%,#ECEAF7 50%,#F5F0E4 100%);
56
- --panel: rgba(255,255,255,0.62); --panel-2: rgba(255,255,255,0.78); --panel-solid:#FBF7EE;
57
- --glass-border: rgba(90,70,20,0.16); --glass-hi: rgba(255,255,255,0.7);
58
- --border: rgba(90,70,20,0.16); --border-strong: rgba(185,120,22,0.55);
59
- --ink:#241A08; --muted:#6E5A3E;
60
- --gold:#8A5A0C; --gold-bright:#B9781A;
61
- --ok:#1F7A4C; --ok-bg:rgba(180,220,190,0.6); --warn:#8A6410; --warn-bg:rgba(240,225,180,0.7);
62
- --bad:#B5382E; --bad-bg:rgba(240,205,200,0.7); --info:#1F6F9E; --info-bg:rgba(200,225,240,0.7);
63
- --shadow: 0 10px 30px rgba(60,40,10,0.14);
52
+ :root[data-theme="light"]{
53
+ --bg:#090806;--panel:rgba(24,20,13,.72);--panel-2:rgba(38,31,18,.76);--panel-solid:#100e09;
54
+ --ink:#FFF8E8;--muted:#B5A88F;--glass-border:rgba(255,224,151,.17);--glass-hi:rgba(255,244,214,.1);
55
+ --border:rgba(255,224,151,.14);--border-strong:rgba(255,205,93,.58);--gold:#D3A43D;--gold-bright:#FFE094;
64
56
  }
65
57
  *{box-sizing:border-box;margin:0;padding:0}
66
58
  html,body{height:100%}
67
- body{background:var(--bg);background-image:var(--grad);background-attachment:fixed;background-size:cover;
68
- color:var(--ink);font-family:var(--sans);line-height:1.45;-webkit-font-smoothing:antialiased;}
59
+ body{background:var(--bg);background-image:var(--grad);background-attachment:fixed;background-size:cover;
60
+ color:var(--ink);font-family:var(--sans);line-height:1.45;-webkit-font-smoothing:antialiased;}
61
+ body:before{content:"";position:fixed;inset:0;pointer-events:none;z-index:-1;opacity:.22;background-image:linear-gradient(rgba(255,221,140,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,221,140,.025) 1px,transparent 1px);background-size:42px 42px;mask-image:linear-gradient(to bottom,black,transparent 82%);}
69
62
  .mono{font-family:var(--mono);font-variant-numeric:tabular-nums;}
70
- .glass{background:var(--panel);-webkit-backdrop-filter:blur(16px) saturate(1.3);backdrop-filter:blur(16px) saturate(1.3);
71
- border:1px solid var(--glass-border);box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi);}
63
+ .glass{background:var(--panel);-webkit-backdrop-filter:blur(16px) saturate(1.3);backdrop-filter:blur(16px) saturate(1.3);
64
+ border:1px solid var(--glass-border);box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi),inset 0 -1px 0 rgba(0,0,0,.5);}
72
65
 
73
66
  /* top bar */
74
67
  header.bar{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;
@@ -80,7 +73,7 @@ header.bar{display:flex;align-items:center;justify-content:space-between;gap:1re
80
73
  .livedot.stale{background:var(--muted);box-shadow:none;animation:none;}
81
74
  @keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 55%,transparent),0 0 12px var(--ok)}70%{box-shadow:0 0 0 7px transparent,0 0 12px var(--ok)}100%{box-shadow:0 0 0 0 transparent,0 0 12px var(--ok)}}
82
75
  @media (prefers-reduced-motion:reduce){.livedot{animation:none}}
83
- .brand b{font-size:0.98rem;letter-spacing:-0.01em;}
76
+ .brand b{font-family:var(--display);font-size:1.08rem;letter-spacing:.015em;color:var(--gold-pale);text-shadow:0 0 24px rgba(246,215,123,.2);}
84
77
  .brand .who{font-family:var(--mono);font-size:0.72rem;color:var(--muted);}
85
78
  .chips{display:flex;gap:0.45rem;flex-wrap:wrap;align-items:center;}
86
79
  .chip{font-family:var(--mono);font-size:0.68rem;padding:0.28em 0.62em;border-radius:100px;border:1px solid var(--glass-border);
@@ -139,8 +132,56 @@ section h2 .count{color:var(--muted);}
139
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);}
140
133
  .tbadge.ok{color:var(--ok);} .tbadge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 40%,transparent);}
141
134
  .tbadge.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent);background:var(--bad-bg);}
142
- .chip.alert{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent);background:var(--bad-bg);}
143
- .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)}}
144
185
 
145
186
  /* hardware meters */
146
187
  .hw{border-radius:13px;padding:0.85rem;margin-top:1rem;
@@ -532,21 +573,49 @@ button.act:disabled{opacity:0.5;cursor:default;}
532
573
  .rp-modal-body pre{background:var(--panel-solid);border:1px solid var(--glass-border);border-radius:8px;padding:0.5rem 0.7rem;
533
574
  font-size:0.66rem;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-word;color:var(--ink);}
534
575
 
535
- .rp-call-shell{display:grid;grid-template-columns:minmax(260px,0.8fr) minmax(320px,1.2fr);gap:0.8rem;min-height:500px;}
536
- .rp-call-panel{border:1px solid var(--glass-border);border-radius:14px;background:var(--panel);box-shadow:var(--shadow),inset 0 1px 0 var(--glass-hi);padding:0.85rem;}
537
- .rp-call-character{display:flex;align-items:center;gap:0.8rem;padding:0.7rem;border:1px solid var(--glass-border);border-radius:12px;background:var(--panel-2);margin-bottom:0.75rem;}
538
- .rp-call-character img{width:78px;height:78px;object-fit:contain;filter:drop-shadow(0 8px 16px rgba(0,0,0,.35));}
539
- .rp-call-character .name{font-size:1.05rem;color:var(--gold-bright);font-weight:700;}
540
- .rp-call-state{display:flex;align-items:center;gap:0.55rem;font-family:var(--mono);font-size:0.72rem;padding:0.65rem 0;}
541
- .rp-call-orb{width:10px;height:10px;border-radius:50%;background:var(--muted);box-shadow:0 0 0 transparent;}
542
- .rp-call-orb.live{background:var(--ok);box-shadow:0 0 12px var(--ok);animation:pulse 1.6s infinite;}
543
- .rp-call-orb.busy{background:var(--gold-bright);box-shadow:0 0 12px var(--gold-bright);animation:pulse 1s infinite;}
544
- .rp-call-actions{display:flex;gap:0.45rem;flex-wrap:wrap;margin-top:0.7rem;}
545
- .rp-call-transcript{height:430px;overflow-y:auto;display:flex;flex-direction:column;gap:0.55rem;padding:0.3rem;}
546
- .rp-turn{max-width:86%;border:1px solid var(--glass-border);border-radius:12px;padding:0.55rem 0.7rem;font-family:var(--mono);font-size:0.72rem;line-height:1.45;background:var(--panel-2);}
547
- .rp-turn.user{align-self:flex-end;border-color:color-mix(in srgb,var(--info) 45%,transparent);background:color-mix(in srgb,var(--info) 9%,var(--panel-2));}
548
- .rp-turn.assistant{align-self:flex-start;border-color:color-mix(in srgb,var(--gold-bright) 42%,transparent);}
549
- .rp-turn .who{font-size:0.57rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:.18rem;}
576
+ .rp-call-shell{position:relative;min-height:720px;overflow:hidden;border:1px solid var(--glass-border);border-radius:24px;background:linear-gradient(145deg,rgba(18,15,9,.84),rgba(5,5,4,.9));box-shadow:0 30px 90px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,238,194,.11);}
577
+ .rp-call-shell:before{content:"";position:absolute;width:520px;height:520px;border-radius:50%;left:18%;top:-340px;background:rgba(246,215,123,.15);filter:blur(75px);pointer-events:none;}
578
+ .rp-call-top{height:58px;padding:0 1.1rem;display:flex;align-items:center;gap:.75rem;border-bottom:1px solid var(--glass-border);background:rgba(8,7,5,.56);backdrop-filter:blur(24px);position:relative;z-index:3;}
579
+ .rp-call-top .eyebrow{font-family:var(--mono);font-size:.57rem;letter-spacing:.16em;color:var(--gold);text-transform:uppercase;}
580
+ .rp-call-top .title{font-family:var(--display);font-size:1.02rem;color:var(--gold-pale);letter-spacing:.03em;}
581
+ .rp-call-top .session{margin-left:auto;font-family:var(--mono);font-size:.58rem;color:var(--muted);max-width:32ch;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
582
+ .rp-call-grid{display:grid;grid-template-columns:280px minmax(380px,1fr) 330px;gap:1px;background:var(--glass-border);min-height:662px;position:relative;z-index:2;}
583
+ .rp-call-rail,.rp-call-stage,.rp-call-chat{background:rgba(8,7,5,.82);padding:1rem;min-width:0;}
584
+ .rp-call-rail{display:flex;flex-direction:column;gap:.8rem;}
585
+ .rp-call-card{border:1px solid var(--glass-border);border-radius:16px;background:linear-gradient(145deg,rgba(38,31,18,.58),rgba(13,11,8,.66));box-shadow:inset 0 1px 0 var(--glass-hi),0 12px 35px rgba(0,0,0,.25);padding:.8rem;}
586
+ .rp-call-character{position:relative;display:flex;align-items:center;gap:.75rem;padding:.75rem;border:1px solid rgba(246,215,123,.2);border-radius:15px;background:radial-gradient(circle at 12% 10%,rgba(246,215,123,.15),transparent 52%),rgba(16,14,10,.65);overflow:hidden;}
587
+ .rp-call-character img{width:76px;height:76px;object-fit:contain;filter:drop-shadow(0 10px 20px rgba(0,0,0,.48));}
588
+ .rp-call-character .name{font-family:var(--display);font-size:1.18rem;color:var(--gold-pale);font-weight:700;}
589
+ .rp-call-character .meta{font-size:.64rem;line-height:1.4;}
590
+ .rp-call-service-grid{display:grid;gap:.4rem;}
591
+ .rp-call-service{display:grid;grid-template-columns:29px 1fr auto;align-items:center;gap:.45rem;padding:.48rem;border:1px solid var(--glass-border);border-radius:10px;background:rgba(255,255,255,.018);}
592
+ .rp-call-service .ico{width:29px;height:29px;display:grid;place-items:center;border-radius:8px;background:rgba(246,215,123,.09);color:var(--gold-bright);font-size:.75rem;}
593
+ .rp-call-service b{display:block;font-size:.64rem;color:var(--ink);}.rp-call-service small{display:block;font-family:var(--mono);font-size:.52rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:130px;}
594
+ .rp-call-service .ready{font-family:var(--mono);font-size:.48rem;letter-spacing:.08em;text-transform:uppercase;color:var(--ok);}
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)}
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-stage{display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;overflow:hidden;}
598
+ .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);}
599
+ .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;}
600
+ .rp-call-visual.speaking{box-shadow:0 0 95px rgba(246,215,123,.28),inset 0 0 55px rgba(246,215,123,.11);border-color:rgba(255,229,157,.55)}
601
+ .rp-call-avatar{position:absolute;width:58%;height:58%;object-fit:contain;filter:drop-shadow(0 18px 30px rgba(0,0,0,.65));opacity:.96;}
602
+ .rp-call-avatar:not([src]){display:none}
603
+ .rp-call-avatar-fallback{position:absolute;width:128px;height:128px;border-radius:50%;display:grid;place-items:center;font-family:var(--display);font-size:2.8rem;letter-spacing:.08em;color:var(--gold-pale);background:radial-gradient(circle at 32% 24%,rgba(255,234,174,.18),rgba(201,150,46,.07) 48%,rgba(0,0,0,.2));border:1px solid rgba(246,215,123,.2);box-shadow:inset 0 0 34px rgba(246,215,123,.06),0 18px 45px rgba(0,0,0,.42);}
604
+ .rp-call-avatar[src]{z-index:1}.rp-call-avatar[src]+.rp-call-avatar-fallback{display:none}
605
+ .rp-call-bars{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:7px;opacity:.82}.rp-call-bars i{width:5px;height:16px;border-radius:8px;background:linear-gradient(var(--gold-pale),var(--gold));box-shadow:0 0 12px rgba(246,215,123,.35);transition:height .09s ease;}
606
+ .rp-call-ring{position:absolute;inset:-10px;border:1px solid rgba(246,215,123,.13);border-radius:50%;animation:rp-call-spin 18s linear infinite}.rp-call-ring:before,.rp-call-ring:after{content:"";position:absolute;width:7px;height:7px;border-radius:50%;background:var(--gold-bright);box-shadow:0 0 15px var(--gold-bright)}.rp-call-ring:before{left:15%;top:12%}.rp-call-ring:after{right:10%;bottom:18%}
607
+ @keyframes rp-call-spin{to{transform:rotate(360deg)}}
608
+ .rp-call-agent-state{position:relative;z-index:2;margin-top:1.15rem;text-align:center}.rp-call-agent-state strong{display:block;font-family:var(--display);font-size:1.35rem;color:var(--gold-pale);font-weight:500}.rp-call-agent-state span{font-family:var(--mono);font-size:.55rem;letter-spacing:.13em;color:var(--muted);text-transform:uppercase}
609
+ .rp-call-state{display:flex;align-items:center;justify-content:center;gap:.5rem;font-family:var(--mono);font-size:.62rem;padding:.55rem 0;position:relative;z-index:2;}
610
+ .rp-call-orb{width:8px;height:8px;border-radius:50%;background:var(--muted)}.rp-call-orb.live{background:var(--ok);box-shadow:0 0 12px var(--ok);animation:pulse 1.6s infinite}.rp-call-orb.busy{background:var(--gold-bright);box-shadow:0 0 12px var(--gold-bright);animation:pulse 1s infinite}
611
+ .rp-call-latency{position:relative;z-index:2;display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem;width:min(420px,92%);margin-top:.65rem}.rp-call-latency div{padding:.48rem;border:1px solid var(--glass-border);border-radius:10px;background:rgba(0,0,0,.28);text-align:center}.rp-call-latency b{display:block;font-family:var(--mono);font-size:.72rem;color:var(--gold-bright)}.rp-call-latency span{font-family:var(--mono);font-size:.46rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
612
+ .rp-call-media{position:absolute;right:1rem;top:1rem;width:128px;aspect-ratio:4/3;border:1px solid var(--glass-border);border-radius:12px;background:#050504;overflow:hidden;display:none;z-index:4;box-shadow:var(--shadow)}.rp-call-media.on{display:block}.rp-call-media video{width:100%;height:100%;object-fit:cover}.rp-call-media .tag{position:absolute;left:.35rem;bottom:.3rem;font-family:var(--mono);font-size:.46rem;color:var(--gold-pale);background:rgba(0,0,0,.7);padding:.12rem .3rem;border-radius:4px}
613
+ .rp-call-controls{position:relative;z-index:3;display:flex;align-items:center;justify-content:center;gap:.52rem;margin-top:1rem;flex-wrap:wrap}.rp-call-ctl{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;border:1px solid var(--glass-border);background:rgba(255,255,255,.035);color:var(--ink);cursor:pointer;font-size:1rem;transition:.18s}.rp-call-ctl:hover:not(:disabled){transform:translateY(-2px);border-color:var(--gold-bright);color:var(--gold-bright);box-shadow:0 7px 24px rgba(0,0,0,.38)}.rp-call-ctl.on{background:rgba(246,215,123,.12);border-color:rgba(246,215,123,.46);color:var(--gold-bright)}.rp-call-ctl.danger{background:rgba(255,78,63,.13);border-color:rgba(255,126,110,.35);color:var(--bad)}.rp-call-ctl:disabled{opacity:.32;cursor:not-allowed}.rp-call-start{width:auto;border-radius:100px;padding:0 1.15rem;font-family:var(--mono);font-size:.61rem;text-transform:uppercase;letter-spacing:.08em;background:linear-gradient(135deg,#ffe59a,#b77b16);color:#201401;border-color:#ffdc7d;font-weight:700}
614
+ .rp-call-chat{display:flex;flex-direction:column;min-height:0}.rp-call-chat-head{display:flex;align-items:center;gap:.45rem;padding-bottom:.65rem;border-bottom:1px solid var(--glass-border)}.rp-call-chat-head b{font-family:var(--display);font-size:.94rem;color:var(--gold-pale)}.rp-call-chat-head button{margin-left:auto;background:none;border:0;color:var(--muted);font-family:var(--mono);font-size:.55rem;cursor:pointer}.rp-call-chat-head button:hover{color:var(--gold-bright)}
615
+ .rp-call-transcript{height:500px;overflow-y:auto;display:flex;flex-direction:column;gap:.55rem;padding:.7rem .15rem;scrollbar-color:var(--gold-deep) transparent}.rp-call-transcript.collapsed{display:none}
616
+ .rp-turn{max-width:90%;border:1px solid var(--glass-border);border-radius:14px;padding:.6rem .7rem;font-size:.71rem;line-height:1.5;background:rgba(255,255,255,.025);box-shadow:0 8px 22px rgba(0,0,0,.2)}.rp-turn.user{align-self:flex-end;border-color:rgba(246,215,123,.22);background:linear-gradient(145deg,rgba(201,150,46,.13),rgba(255,255,255,.025));border-bottom-right-radius:4px}.rp-turn.assistant{align-self:flex-start;border-color:rgba(246,215,123,.38);border-bottom-left-radius:4px}.rp-turn .who{font-family:var(--mono);font-size:.5rem;text-transform:uppercase;letter-spacing:.1em;color:var(--gold);margin-bottom:.2rem}
617
+ .rp-call-compose{display:flex;gap:.4rem;margin-top:auto;padding-top:.65rem;border-top:1px solid var(--glass-border)}.rp-call-compose input{flex:1;min-width:0;background:#0c0a07;border:1px solid var(--glass-border);color:var(--ink);border-radius:11px;padding:.58rem .65rem;font-size:.7rem}.rp-call-compose input:focus{outline:none;border-color:var(--gold-bright);box-shadow:0 0 0 3px rgba(246,215,123,.08)}.rp-call-compose button{width:38px;border-radius:11px;border:1px solid rgba(246,215,123,.38);background:rgba(246,215,123,.1);color:var(--gold-bright);cursor:pointer}
618
+ .rp-call-event{min-height:42px;margin-top:.65rem;padding:.5rem .6rem;border:1px solid var(--glass-border);border-radius:10px;background:rgba(0,0,0,.22);font-family:var(--mono);font-size:.54rem;color:var(--muted);line-height:1.45}.rp-call-event b{color:var(--gold-bright)}
550
619
  .rp-detail-hero{display:flex;align-items:center;gap:.8rem;padding:.6rem;border:1px solid var(--glass-border);border-radius:12px;background:var(--panel-2);margin-bottom:.8rem;}
551
620
  .rp-detail-hero img{width:72px;height:72px;object-fit:contain;}
552
621
  .rp-pipeline{display:flex;flex-direction:column;gap:.3rem;margin-top:.5rem;}
@@ -556,7 +625,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
556
625
  .node .node-avatar{width:68px;height:68px;object-fit:contain;flex:none;filter:drop-shadow(0 7px 12px rgba(0,0,0,.3));}
557
626
  .node .node-health{font-family:var(--mono);font-size:.61rem;line-height:1.45;color:var(--muted);}
558
627
  .node .node-health b{color:var(--ink);}
559
- @media(max-width:820px){.rp-call-shell{grid-template-columns:1fr}.rp-call-transcript{height:320px}.rp-pipeline-row{grid-template-columns:62px 105px 58px 1fr}}
628
+ @media(max-width:1150px){.rp-call-grid{grid-template-columns:250px minmax(360px,1fr)}.rp-call-chat{grid-column:1/-1;min-height:390px}.rp-call-transcript{height:280px}}
629
+ @media(max-width:760px){.rp-call-shell{border-radius:15px}.rp-call-grid{display:block}.rp-call-rail,.rp-call-stage,.rp-call-chat{border-bottom:1px solid var(--glass-border)}.rp-call-stage{min-height:590px}.rp-call-top .session{display:none}.rp-call-visual{width:min(285px,78vw)}.rp-call-transcript{height:320px}.rp-pipeline-row{grid-template-columns:62px 105px 58px 1fr}}
560
630
 
561
631
  /* ── TTS voice picker (used inside the generic modal) ── */
562
632
  .rp-voice-picker{display:flex;flex-direction:column;gap:0.6rem;min-height:320px;}
@@ -584,9 +654,40 @@ button.act:disabled{opacity:0.5;cursor:default;}
584
654
  .rp-vp-id{font-size:0.62rem;color:var(--muted);margin-top:0.1rem;word-break:break-all;}
585
655
  .rp-vp-desc{font-size:0.64rem;color:var(--muted);margin-top:0.2rem;line-height:1.4;}
586
656
  .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);}
587
- .rp-voice-picker-foot .rp-muted{color:var(--muted);}
588
-
589
- .rp-fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:0.7rem;padding:0;}
657
+ .rp-voice-picker-foot .rp-muted{color:var(--muted);}
658
+
659
+ /* Character and voice configuration workspace */
660
+ .rp-config-workspace{grid-column:1/-1;display:grid;gap:1rem;}
661
+ .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);}
662
+ .rp-config-hero h3{font-family:var(--display);font-size:1.25rem;color:var(--gold-pale);margin:0;}
663
+ .rp-config-hero p{max-width:72ch;margin:.25rem 0 0;color:var(--muted);font-size:.68rem;line-height:1.55;}
664
+ .rp-config-hero-actions,.rp-config-toolbar,.rp-config-actions{display:flex;align-items:center;gap:.45rem;flex-wrap:wrap;}
665
+ .rp-config-columns{display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start;}
666
+ .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);}
667
+ .rp-config-library-head{display:flex;align-items:flex-start;gap:.7rem;margin-bottom:.7rem;}
668
+ .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;}
669
+ .rp-config-library-head h4{font-family:var(--display);font-size:1rem;color:var(--gold-pale);margin:0;}
670
+ .rp-config-library-head p{font-size:.58rem;color:var(--muted);margin:.1rem 0 0;line-height:1.4;}
671
+ .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;}
672
+ .rp-config-toolbar{margin-bottom:.65rem;}.rp-config-toolbar input{flex:1;min-width:150px;}
673
+ .rp-config-cards{display:grid;gap:.55rem;max-height:560px;overflow:auto;padding-right:.2rem;scrollbar-color:var(--gold-deep) transparent;}
674
+ .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;}
675
+ .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;}
676
+ .rp-config-card:hover{transform:translateY(-1px);border-color:rgba(246,215,123,.38);background:rgba(246,215,123,.045);}
677
+ .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;}
678
+ .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;}
679
+ .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)}
680
+ .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)}
681
+ .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;}
682
+ .rp-config-card .rp-config-actions{margin-top:.6rem}.rp-config-card .rp-btn{font-size:.53rem;padding:.32rem .5rem;}
683
+ .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}
684
+ .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)}
685
+ .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%)}
686
+ .rp-modal.config-modal{width:min(980px,100%);max-height:90vh}.rp-modal.config-modal .rp-modal-body{padding:1rem 1.1rem 1.2rem;}
687
+ @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}}
688
+ @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}}
689
+
690
+ .rp-fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:0.7rem;padding:0;}
590
691
  .rp-fleet-card{position:relative;}
591
692
  .rp-fleet-card .spark{margin-top:0.5rem;height:36px;}
592
693
  .rp-fleet-card .rp-actions{margin-top:0.6rem;}
@@ -626,12 +727,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
626
727
  <button class="tab" data-tab="robopark" id="tabbtn-robopark">Sessions &amp; Telemetry</button>
627
728
  </div>
628
729
 
629
- <div class="pane active" id="pane-fleet">
630
- <div class="wrap">
631
- <section>
632
- <h2>Fleet <span class="count" id="fleetcount"></span></h2>
633
- <div class="fleet" id="fleet"></div>
634
- <div class="hw" id="hw"></div>
730
+ <div class="pane active" id="pane-fleet">
731
+ <div class="wrap">
732
+ <section>
733
+ <div class="fleet-ops">
734
+ <div class="fleet-hero">
735
+ <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>
736
+ <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>
737
+ </div>
738
+ <div class="fleet-kpis" id="fleet-kpis"></div>
739
+ <div class="fleet-insights">
740
+ <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>
741
+ <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>
742
+ <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>
743
+ <div class="fleet-panel fleet-release" id="fleet-release"></div>
744
+ </div>
745
+ </div>
746
+ <h2>Fleet <span class="count" id="fleetcount"></span></h2>
747
+ <div class="fleet" id="fleet"></div>
748
+ <div class="hw" id="hw"></div>
635
749
  </section>
636
750
  <aside>
637
751
  <h2>Live activity</h2>
@@ -656,16 +770,39 @@ button.act:disabled{opacity:0.5;cursor:default;}
656
770
  </aside>
657
771
  <div class="pk-hint">drag to pan · scroll to zoom · <b>click a robot</b> for cam / mic / speaker</div>
658
772
  </div>
659
- <div class="pk-drawer" id="pk-drawer">
773
+ <div class="pk-drawer" id="pk-drawer">
660
774
  <div class="pk-dh">
661
775
  <div class="pk-av" id="pk-av">🤖</div>
662
776
  <div><div class="nm" id="pk-name">robot</div><div class="sub" id="pk-sub">satellite</div></div>
663
777
  <span class="pk-pill idle" id="pk-state" style="margin-left:0.4rem">idle</span>
664
778
  <button class="x" id="pk-close">✕</button>
665
779
  </div>
666
- <div class="pk-dbody" id="pk-body"></div>
667
- </div>
668
- </div>
780
+ <div class="pk-dbody" id="pk-body"></div>
781
+ </div>
782
+ </div>
783
+
784
+ <div class="pk-camera-ops" id="pk-camera-ops" role="dialog" aria-modal="true" aria-label="Fullscreen robot camera operations view">
785
+ <div class="pk-camera-ops-head">
786
+ <img class="avatar" id="pk-camera-ops-avatar" alt="">
787
+ <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>
788
+ <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>
789
+ </div>
790
+ <div class="pk-camera-ops-grid">
791
+ <div class="pk-camera-stage" id="pk-camera-stage">
792
+ <img id="pk-camera-ops-img" alt="Live robot camera feed">
793
+ <div class="empty" id="pk-camera-ops-empty">opening RoboVision camera...</div>
794
+ <div class="pk-camera-rec"><i></i><span id="pk-camera-ops-rec">REC / CONNECTING</span></div>
795
+ <div class="pk-camera-clock" id="pk-camera-ops-clock">--:--:--</div>
796
+ <div class="pk-camera-overlay" id="pk-camera-ops-overlay"></div>
797
+ </div>
798
+ <aside class="pk-camera-side">
799
+ <div class="section"><h3>Live insights</h3><div class="pk-camera-insight" id="pk-camera-ops-insights"></div></div>
800
+ <div class="section"><h3>Conversation pipeline</h3><div class="pk-camera-pipeline" id="pk-camera-ops-pipeline"></div></div>
801
+ <div class="section"><h3>Hardware and media</h3><div class="pk-camera-insight" id="pk-camera-ops-hardware"></div></div>
802
+ <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>
803
+ </aside>
804
+ </div>
805
+ </div>
669
806
 
670
807
  <div class="pane" id="pane-robopark">
671
808
  <div class="rp-head">
@@ -687,9 +824,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
687
824
  <button class="rp-subtab" data-sub="live" id="rpsbtn-live" role="tab" aria-selected="false">Live Sessions <span class="badge" id="rps-live-badge">0</span></button>
688
825
  <button class="rp-subtab" data-sub="voice" id="rpsbtn-voice" role="tab" aria-selected="false">Voice Call</button>
689
826
  <button class="rp-subtab" data-sub="history" id="rpsbtn-history" role="tab" aria-selected="false">History</button>
690
- <button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
691
- <button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
692
- <button class="rp-subtab" data-sub="setup" id="rpsbtn-setup" role="tab" aria-selected="false">Setup</button>
827
+ <button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
828
+ <button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
829
+ <button class="rp-subtab" data-sub="config" id="rpsbtn-config" role="tab" aria-selected="false">Characters &amp; Voice</button>
830
+ <button class="rp-subtab" data-sub="setup" id="rpsbtn-setup" role="tab" aria-selected="false">Setup</button>
693
831
  </div>
694
832
 
695
833
  <!-- ═══ Overview ═══ -->
@@ -729,23 +867,65 @@ button.act:disabled{opacity:0.5;cursor:default;}
729
867
  <!-- ═══ History ═══ -->
730
868
  <div class="rp-sub" id="rp-sub-voice">
731
869
  <div class="rp-call-shell">
732
- <div class="rp-call-panel">
733
- <div class="rp-section-h">Operator voice call <span class="meta">RoboVoice pipeline</span></div>
734
- <div class="rp-call-character" id="rp-call-character"><div><div class="name">Choose a character</div><div class="meta">The selected preset and stack are frozen into session history.</div></div></div>
735
- <div class="rp-field"><label>Character preset</label><select id="rp-call-character-select"><option value="">select character...</option></select></div>
736
- <div class="rp-field" style="margin-top:.55rem"><label>Voice stack</label><select id="rp-call-stack-select"><option value="">character default</option></select></div>
737
- <div class="rp-call-state"><span class="rp-call-orb" id="rp-call-orb"></span><span id="rp-call-status">ready to start</span></div>
738
- <div class="rp-call-actions">
739
- <button class="rp-btn primary" id="rp-call-start">Start voice call</button>
740
- <button class="rp-btn" id="rp-call-mute" disabled>Mute microphone</button>
741
- <button class="rp-btn danger" id="rp-call-end" disabled>End call</button>
742
- </div>
743
- <div class="pk-note" style="margin-top:.7rem">Uses your browser microphone. Agent audio plays here and every final transcript turn is saved to session history.</div>
870
+ <div class="rp-call-top">
871
+ <div><div class="eyebrow">RoboVoice operator channel</div><div class="title">Royal Voice Studio</div></div>
872
+ <div class="session" id="rp-call-session">No active session</div>
744
873
  </div>
745
- <div class="rp-call-panel">
746
- <div class="rp-section-h">Live transcript <span class="meta" id="rp-call-session">not connected</span></div>
747
- <div class="rp-call-transcript" id="rp-call-transcript"><div class="rp-tbl-empty">Transcript appears when the call starts.</div></div>
748
- <div id="rp-call-audio" hidden></div>
874
+ <div class="rp-call-grid">
875
+ <aside class="rp-call-rail">
876
+ <div class="rp-call-character" id="rp-call-character"><div><div class="name">Choose a character</div><div class="meta">Preset and voice stack are frozen into session history.</div></div></div>
877
+ <div class="rp-call-card">
878
+ <div class="rp-call-device"><label>Character preset</label><select id="rp-call-character-select"><option value="">select character...</option></select></div>
879
+ <div class="rp-call-device"><label>Voice stack</label><select id="rp-call-stack-select"><option value="">character default</option></select></div>
880
+ <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>
881
+ </div>
882
+ <div class="rp-call-card">
883
+ <div class="rp-section-h" style="margin:0 0 .5rem">Voice pipeline <span class="meta">selected stack</span></div>
884
+ <div class="rp-call-service-grid" id="rp-call-services">
885
+ <div class="rp-call-service"><span class="ico">STT</span><div><b>Speech recognition</b><small>choose a stack</small></div><span class="ready">standby</span></div>
886
+ <div class="rp-call-service"><span class="ico">LLM</span><div><b>Language model</b><small>choose a stack</small></div><span class="ready">standby</span></div>
887
+ <div class="rp-call-service"><span class="ico">TTS</span><div><b>Voice synthesis</b><small>choose a stack</small></div><span class="ready">standby</span></div>
888
+ </div>
889
+ </div>
890
+ <div class="rp-call-card">
891
+ <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>
892
+ <div class="rp-call-device"><label>Microphone</label><select id="rp-call-mic-device"><option value="">system default</option></select></div>
893
+ <div class="rp-call-device"><label>Camera</label><select id="rp-call-cam-device"><option value="">system default</option></select></div>
894
+ <div class="rp-call-device"><label>Speaker output</label><select id="rp-call-speaker-device"><option value="">system default</option></select></div>
895
+ <button class="rp-btn" id="rp-call-refresh-devices" style="width:100%;margin-top:.55rem">Refresh browser devices</button>
896
+ </div>
897
+ </aside>
898
+ <main class="rp-call-stage">
899
+ <div class="rp-call-media" id="rp-call-media"><video id="rp-call-video" autoplay muted playsinline></video><span class="tag" id="rp-call-media-tag">CAMERA</span></div>
900
+ <div class="rp-call-visual" id="rp-call-visual">
901
+ <div class="rp-call-ring"></div>
902
+ <img class="rp-call-avatar" id="rp-call-avatar" alt="" onerror="this.removeAttribute('src')">
903
+ <div class="rp-call-avatar-fallback" id="rp-call-avatar-fallback" aria-hidden="true">RP</div>
904
+ <div class="rp-call-bars" id="rp-call-bars"><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
905
+ </div>
906
+ <div class="rp-call-agent-state"><strong id="rp-call-agent-state">Ready when you are</strong><span id="rp-call-agent-detail">select character and begin</span></div>
907
+ <div class="rp-call-state"><span class="rp-call-orb" id="rp-call-orb"></span><span id="rp-call-status">ready to start</span><span class="mono" id="rp-call-timer">00:00</span></div>
908
+ <div class="rp-call-latency">
909
+ <div><b id="rp-call-lat-total">--</b><span>Total response</span></div>
910
+ <div><b id="rp-call-lat-stt">--</b><span>STT to LLM</span></div>
911
+ <div><b id="rp-call-lat-tts">--</b><span>LLM to TTS</span></div>
912
+ </div>
913
+ <div class="rp-call-controls">
914
+ <button class="rp-call-ctl rp-call-start" id="rp-call-start">Start call</button>
915
+ <button class="rp-call-ctl" id="rp-call-mute" title="Microphone" disabled>MIC</button>
916
+ <button class="rp-call-ctl" id="rp-call-camera" title="Camera" disabled>CAM</button>
917
+ <button class="rp-call-ctl" id="rp-call-screen" title="Share screen" disabled>SCR</button>
918
+ <button class="rp-call-ctl" id="rp-call-chat-toggle" title="Toggle transcript">TXT</button>
919
+ <button class="rp-call-ctl danger" id="rp-call-end" title="End call" disabled>END</button>
920
+ </div>
921
+ <div class="rp-call-event" id="rp-call-event"><b>System</b> Voice pipeline is standing by.</div>
922
+ </main>
923
+ <section class="rp-call-chat">
924
+ <div class="rp-call-chat-head"><b>Conversation</b><span class="chip" id="rp-call-message-count">0 messages</span><button id="rp-call-collapse">collapse transcript</button></div>
925
+ <div class="rp-call-transcript" id="rp-call-transcript"><div class="rp-tbl-empty">Your live transcript and typed messages appear here.</div></div>
926
+ <form class="rp-call-compose" id="rp-call-compose"><input id="rp-call-message" autocomplete="off" placeholder="Type a message to the character..." disabled><button type="submit" id="rp-call-send" disabled>SEND</button></form>
927
+ <div id="rp-call-audio" hidden></div>
928
+ </section>
749
929
  </div>
750
930
  </div>
751
931
  </div>
@@ -873,8 +1053,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
873
1053
  </div>
874
1054
 
875
1055
  <!-- ═══ Fleet ═══ -->
876
- <div class="rp-sub" id="rp-sub-fleet">
877
- <div class="rp-section-h">Robots <span class="meta" id="rp-fleet-meta">live + idle + offline</span></div>
1056
+ <div class="rp-sub" id="rp-sub-fleet">
1057
+ <div class="fleet-ops" style="margin-bottom:1rem">
1058
+ <div class="fleet-kpis" id="rp-fleet-kpis"></div>
1059
+ <div class="fleet-insights">
1060
+ <div class="fleet-panel"><div class="fleet-panel-head"><b>Operational readiness</b><span>scheduler</span></div><div id="rp-fleet-readiness"></div></div>
1061
+ <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>
1062
+ <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>
1063
+ </div>
1064
+ </div>
1065
+ <div class="rp-section-h">Robots <span class="meta" id="rp-fleet-meta">live + idle + offline</span></div>
878
1066
  <div class="rp-fleet-grid" id="rp-grid"></div>
879
1067
  </div>
880
1068
 
@@ -937,79 +1125,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
937
1125
  <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>
938
1126
  </div>
939
1127
 
940
- <!-- Config templates -->
941
- <div class="rp-form" id="rp-templates" style="grid-template-columns:1fr 1fr; margin-top:1.2rem;">
942
- <h3 style="grid-column:1/-1">Config templates</h3>
943
- <div>
944
- <h4 style="font-family:var(--mono);font-size:0.75rem;color:var(--gold-bright);margin:0.3rem 0 0.5rem;">Voice stacks</h4>
945
- <div id="rp-voice-list" style="display:flex;flex-direction:column;gap:0.4rem;max-height:260px;overflow:auto;"></div>
946
- <div style="display:flex;gap:0.4rem;margin-top:0.55rem;">
947
- <input id="rp-newvoice-name" placeholder="new stack name" style="flex:1;min-width:0;">
948
- <button class="rp-btn" id="rp-addvoice">Add</button>
949
- </div>
950
- <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);">
951
- <h4 style="grid-column:1/-1;margin:0 0 0.2rem;">Edit voice stack: <span id="rpve-name-label"></span></h4>
952
- <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>
953
- <div class="rp-field"><label>STT model</label><input id="rpve-stt-model"></div>
954
- <div class="rp-field"><label>STT language</label><input id="rpve-stt-language" placeholder="en / auto"></div>
955
- <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>
956
- <div class="rp-field"><label>LLM model</label><input id="rpve-llm-model"></div>
957
- <div class="rp-field"><label>LLM API key</label><input type="password" id="rpve-llm-key" placeholder="(uses agent's own if blank)"></div>
958
- <div class="rp-field"><label>Temperature</label><input id="rpve-temperature" type="number" step="0.1" value="0.7"></div>
959
- <div class="rp-field"><label>Context size (num_ctx)</label><input id="rpve-num-ctx" type="number" value="8192"></div>
960
- <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>
961
- <div class="rp-field">
962
- <label>TTS voice</label>
963
- <div style="display:flex;gap:0.35rem;align-items:stretch;">
964
- <select id="rpve-tts-voice" style="flex:1;min-width:0;"></select>
965
- <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>
966
- </div>
967
- <div class="rp-autonote" id="rpve-tts-voice-note" style="margin-top:0.35rem;"></div>
968
- </div>
969
- <div class="rp-field"><label>TTS language</label><input id="rpve-tts-language" placeholder="en"></div>
970
- <div class="rp-field"><label>Max turns</label><input id="rpve-max-turns" type="number" value="20"></div>
971
- <div class="rp-field"><label>Min endpointing delay (s)</label><input id="rpve-endpointing" type="number" step="0.1" value="0.7"></div>
972
- <div class="rp-field"><label><input type="checkbox" id="rpve-interruptions" checked style="width:auto;margin-right:0.35rem;">Allow interruptions</label></div>
973
- <div class="rp-field"><label><input type="checkbox" id="rpve-wake-enabled" style="width:auto;margin-right:0.35rem;">Wake word enabled</label></div>
974
- <div class="rp-field"><label>Wake word model</label><input id="rpve-wake-model" placeholder="models/hey_jarvis.onnx"></div>
975
- <div class="rp-field"><label>Wake threshold</label><input id="rpve-wake-threshold" type="number" step="0.05" value="0.5"></div>
976
- <div class="rp-field"><label>Wake timeout (s)</label><input id="rpve-wake-timeout" type="number" step="0.5" value="3.0"></div>
977
- <div style="grid-column:1/-1;display:flex;gap:0.4rem;">
978
- <button class="rp-btn" id="rpve-save">Save</button>
979
- <button class="rp-btn" id="rpve-cancel">Cancel</button>
980
- </div>
981
- </div>
982
- </div>
983
- <div>
984
- <h4 style="font-family:var(--mono);font-size:0.75rem;color:var(--gold-bright);margin:0.3rem 0 0.5rem;">Character presets</h4>
985
- <div id="rp-char-list" style="display:flex;flex-direction:column;gap:0.4rem;max-height:260px;overflow:auto;"></div>
986
- <div style="display:flex;gap:0.4rem;margin-top:0.55rem;">
987
- <input id="rp-newchar-name" placeholder="new preset name" style="flex:1;min-width:0;">
988
- <input id="rp-newchar-stack" list="rp-newchar-stack-list" placeholder="voice stack id" style="flex:1;min-width:0;">
989
- <datalist id="rp-newchar-stack-list"></datalist>
990
- <button class="rp-btn" id="rp-addchar">Add</button>
991
- </div>
992
- <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);">
993
- <h4 style="margin:0 0 0.2rem;">Edit character: <span id="rpce-name-label"></span></h4>
994
- <div class="rp-field"><label>Name</label><input id="rpce-name"></div>
995
- <div class="rp-field"><label>Description</label><input id="rpce-description"></div>
996
- <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>
997
- <div class="rp-field"><label>Voice stack</label><select id="rpce-stack"></select></div>
998
- <div class="rp-field"><label>Motors (comma-separated)</label><input id="rpce-motors" placeholder="drive, wave"></div>
999
- <div style="display:flex;gap:0.4rem;">
1000
- <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>
1001
- <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>
1002
- </div>
1003
- <div class="rp-field"><label>Avatar image path</label><input id="rpce-img" placeholder="/static/robopark/jaguar.png"></div>
1004
- <div style="display:flex;gap:0.4rem;">
1005
- <button class="rp-btn" id="rpce-save">Save</button>
1006
- <button class="rp-btn" id="rpce-cancel">Cancel</button>
1007
- </div>
1008
- </div>
1009
- </div>
1010
- </div>
1011
- </div>
1012
- </div>
1128
+ </div>
1129
+
1130
+ <!-- Character + voice management -->
1131
+ <div class="rp-sub" id="rp-sub-config">
1132
+ <div class="rp-config-workspace" id="rp-templates">
1133
+ <div class="rp-config-hero">
1134
+ <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>
1135
+ <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>
1136
+ </div>
1137
+ <div class="rp-config-columns">
1138
+ <section class="rp-config-library">
1139
+ <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>
1140
+ <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>
1141
+ <div class="rp-config-cards" id="rp-voice-list"></div>
1142
+ <input id="rp-newvoice-name" type="hidden"><div id="rp-voice-edit" style="display:none"></div>
1143
+ </section>
1144
+ <section class="rp-config-library">
1145
+ <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>
1146
+ <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>
1147
+ <div class="rp-config-cards" id="rp-char-list"></div>
1148
+ <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>
1149
+ </section>
1150
+ </div>
1151
+ </div>
1152
+ </div>
1153
+ </div>
1013
1154
 
1014
1155
  <!-- Generic modal: title + scrollable body, used for session + audit detail views -->
1015
1156
  <div class="rp-modal-back" id="rp-modal-back" role="dialog" aria-modal="true">
@@ -1219,9 +1360,70 @@ button.act:disabled{opacity:0.5;cursor:default;}
1219
1360
  }
1220
1361
  return null;
1221
1362
  }
1363
+ function renderFleetOperations(nodes, prefix, telemetryRows){
1364
+ prefix=prefix||'fleet'; nodes=Array.isArray(nodes)?nodes:[]; telemetryRows=Array.isArray(telemetryRows)?telemetryRows:null;
1365
+ var rows=[];
1366
+ function readinessOf(t,connected){ return String((t&&(t.readiness_code||t.status))||(!connected?'offline':'mesh_online')).toLowerCase(); }
1367
+ if(telemetryRows){
1368
+ for(var ti=0;ti<telemetryRows.length;ti++){
1369
+ var t=telemetryRows[ti], rn=robotName(t), rc=readinessOf(t,true), offline=rc==='offline'||rc==='robot_offline'||rc==='unreachable';
1370
+ rows.push({name:rn,connected:!offline,tele:t,hw:null,version:t.version||'',node:null,readiness:rc});
1371
+ }
1372
+ } else {
1373
+ for(var ni=0;ni<nodes.length;ni++){
1374
+ var n=nodes[ni], tele=rpTelemetryForNode(n), role=String(n.role||'').toLowerCase();
1375
+ if(role!=='satellite'&&role!=='robot'&&!tele) continue;
1376
+ 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)});
1377
+ }
1378
+ }
1379
+ var connected=0,ready=0,active=0,totalSessions=0,totalTriggers=0,latencies=[],drops=[],attention=[];
1380
+ for(var i=0;i<rows.length;i++){
1381
+ var r=rows[i], t=r.tele||{}, code=r.readiness;
1382
+ if(r.connected)connected++;
1383
+ if(code==='ready'||code==='in_session')ready++;
1384
+ if(code==='in_session'||t.active_session||t.in_session||t.session_active||t.current_session_id||t.status==='running'||t.status==='connecting')active++;
1385
+ totalSessions+=Number(t.sessions_total||0);totalTriggers+=Number(t.trigger_count||0);
1386
+ if(t.avg_latency_ms!=null)latencies.push(Number(t.avg_latency_ms));if(t.drop_rate!=null)drops.push(Number(t.drop_rate));
1387
+ var issues=[];
1388
+ if(!r.connected)issues.push('mesh or heartbeat offline');
1389
+ if(code!=='ready'&&code!=='in_session'&&code!=='mesh_online')issues.push(String(t.readiness_message||code).replace(/_/g,' '));
1390
+ if(t.drop_rate!=null&&Number(t.drop_rate)>=.05)issues.push(Math.round(Number(t.drop_rate)*100)+'% session drop rate');
1391
+ if(t.avg_latency_ms!=null&&Number(t.avg_latency_ms)>=3000)issues.push(Math.round(Number(t.avg_latency_ms))+'ms average response');
1392
+ if(r.hw&&r.hw.temperatureC!=null&&r.hw.temperatureC>=70)issues.push(Math.round(r.hw.temperatureC)+'C device temperature');
1393
+ if(r.hw&&r.hw.cpu&&r.hw.cpu.percent>=90)issues.push(Math.round(r.hw.cpu.percent)+'% CPU utilization');
1394
+ if(issues.length)attention.push({name:r.name,message:issues.join(' / '),bad:!r.connected||code==='stuck_session'||code==='join_timeout'||code==='robot_offline'});
1395
+ }
1396
+ var avgLatency=latencies.length?Math.round(latencies.reduce(function(a,b){return a+b;},0)/latencies.length):null;
1397
+ var avgDrop=drops.length?drops.reduce(function(a,b){return a+b;},0)/drops.length:null;
1398
+ var kpis=$(prefix+'-kpis');
1399
+ if(kpis){
1400
+ 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>';}
1401
+ 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');
1402
+ }
1403
+ var read=$(prefix+'-readiness');
1404
+ if(read){
1405
+ 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;
1406
+ 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>';
1407
+ }
1408
+ var workload=$(prefix+'-workload');
1409
+ if(workload){
1410
+ 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;}
1411
+ 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>';
1412
+ }
1413
+ var attentionEl=$(prefix+'-attention'), attentionMeta=$(prefix+'-attention-meta');
1414
+ if(attentionMeta)attentionMeta.textContent=attention.length+' issue'+(attention.length===1?'':'s');
1415
+ 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>';
1416
+ var release=$(prefix+'-release');
1417
+ if(release){
1418
+ var versions={};for(var vi=0;vi<nodes.length;vi++){var version=nodes[vi].version||'unknown';versions[version]=(versions[version]||0)+1;}
1419
+ var versionKeys=Object.keys(versions).sort().reverse(),current=versionKeys[0]||'unknown';
1420
+ 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>';
1421
+ }
1422
+ }
1222
1423
  function renderFleet(nodes){
1223
- var el = $('fleet');
1224
- if(!nodes || !nodes.length){ el.innerHTML = '<div class="empty">No nodes reporting yet. Start peers with <span class="mono">infinicode serve</span> or <span class="mono">robopark setup</span>.</div>'; return; }
1424
+ var el = $('fleet');
1425
+ renderFleetOperations(nodes||[],'fleet');
1426
+ 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; }
1225
1427
  // connected first, then robots/satellites above hubs so the fleet reads robot-first
1226
1428
  nodes.sort(function(a,b){ return (b.connected?1:0)-(a.connected?1:0); });
1227
1429
  var accent = '#6FE0A6';
@@ -1269,18 +1471,21 @@ button.act:disabled{opacity:0.5;cursor:default;}
1269
1471
  } else {
1270
1472
  html += '<div class="stats"><span style="color:var(--muted)">offline'+(n.lastSeenAt?(' · last seen '+clock(n.lastSeenAt)):'')+'</span></div>';
1271
1473
  }
1272
- html += (capHtml?('<div class="caps">'+capHtml+'</div>'):'') + '</div>';
1474
+ html += (capHtml?('<div class="caps">'+capHtml+'</div>'):'')
1475
+ + ((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>';
1273
1476
  }
1274
1477
  el.innerHTML = html;
1275
1478
  var cards = el.querySelectorAll('.node');
1276
1479
  for(var j=0;j<cards.length;j++){
1277
1480
  (function(card){
1278
- function pick(){ selectNode(card.getAttribute('data-id'), card.getAttribute('data-name')); }
1481
+ 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')); }
1279
1482
  card.addEventListener('click', pick);
1280
1483
  card.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); pick(); } });
1281
- })(cards[j]);
1282
- }
1283
- }
1484
+ })(cards[j]);
1485
+ }
1486
+ var fleetButtons=el.querySelectorAll('[data-fleet-action]');
1487
+ 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]);
1488
+ }
1284
1489
 
1285
1490
  function renderSummary(s){
1286
1491
  var nodes = s.nodes||[];
@@ -1560,7 +1765,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1560
1765
  btn.addEventListener('click', function(){ selectTab(btn.getAttribute('data-tab')); });
1561
1766
  })(tabs[ti]);
1562
1767
  }
1563
- function selectTab(name){
1768
+ function selectTab(name){
1564
1769
  activeTab = name;
1565
1770
  for (var i=0;i<tabs.length;i++) tabs[i].classList.toggle('active', tabs[i].getAttribute('data-tab')===name);
1566
1771
  $('pane-fleet').classList.toggle('active', name==='fleet');
@@ -1571,8 +1776,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
1571
1776
  pollRobopark();
1572
1777
  initRpSubTabs();
1573
1778
  }
1574
- if (name==='park') Park.activate(); else Park.deactivate();
1575
- }
1779
+ if (name==='park') Park.activate(); else Park.deactivate();
1780
+ }
1781
+ var fleetParkBtn=$('fleet-open-park'); if(fleetParkBtn) fleetParkBtn.addEventListener('click',function(){selectTab('park');});
1782
+ var fleetSessionsBtn=$('fleet-open-sessions'); if(fleetSessionsBtn) fleetSessionsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('live');});
1783
+ var fleetIncidentsBtn=$('fleet-open-incidents'); if(fleetIncidentsBtn) fleetIncidentsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('incidents');});
1784
+ var fleetRefreshBtn=$('fleet-refresh'); if(fleetRefreshBtn) fleetRefreshBtn.addEventListener('click',function(){poll();pollRobopark();});
1576
1785
  var initHash = location.hash.replace('#','');
1577
1786
  // Defer tab init until Park module is defined (same outer IIFE, later in source).
1578
1787
  function initTab(){
@@ -1975,8 +2184,66 @@ button.act:disabled{opacity:0.5;cursor:default;}
1975
2184
  }
1976
2185
  $('srv-add').addEventListener('click', addServer);
1977
2186
  $('rb-add').addEventListener('click', addRobot);
1978
- $('rp-addvoice').addEventListener('click', function(){ addVoiceStackFromTemplate(); });
1979
- $('rp-addchar').addEventListener('click', function(){ addCharacterFromTemplate(); });
2187
+ $('rp-addvoice').addEventListener('click', function(){ openVoiceStackManager(); });
2188
+ $('rp-addchar').addEventListener('click', function(){ openCharacterManager(); });
2189
+ var newVoiceStackBtn=$('rp-new-voice-stack'); if(newVoiceStackBtn) newVoiceStackBtn.addEventListener('click',function(){ openVoiceStackManager(); });
2190
+ var newCharacterBtn=$('rp-new-character'); if(newCharacterBtn) newCharacterBtn.addEventListener('click',function(){ openCharacterManager(); });
2191
+
2192
+ function configOption(value,label,current){ return '<option value="'+esc(value)+'"'+(String(value)===String(current)?' selected':'')+'>'+esc(label||value)+'</option>'; }
2193
+ function configNumber(id,fallback){ var el=$(id), n=el?Number(el.value):NaN; return Number.isFinite(n)?n:fallback; }
2194
+ function voiceStackDraft(){
2195
+ return {
2196
+ id:$('cfg-vs-id').value.trim(),name:$('cfg-vs-name').value.trim(),
2197
+ stt_provider:$('cfg-vs-stt-provider').value,stt_model:$('cfg-vs-stt-model').value.trim(),stt_language:$('cfg-vs-stt-language').value.trim()||'en',
2198
+ 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,
2199
+ temperature:configNumber('cfg-vs-temperature',0.7),num_ctx:configNumber('cfg-vs-num-ctx',8192),
2200
+ tts_provider:$('cfg-vs-tts-provider').value,tts_voice:$('cfg-vs-tts-voice').value.trim(),tts_language:$('cfg-vs-tts-language').value.trim()||'en',
2201
+ max_turns:configNumber('cfg-vs-max-turns',20),min_endpointing_delay:configNumber('cfg-vs-endpointing',0.7),
2202
+ allow_interruptions:$('cfg-vs-interruptions').checked,wake_word_enabled:$('cfg-vs-wake-enabled').checked,
2203
+ 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)
2204
+ };
2205
+ }
2206
+ function openVoiceStackManager(id,draft){
2207
+ var existing=null;
2208
+ for(var i=0;i<voiceStacks.length;i++) if(String(voiceStacks[i].id)===String(id)){ existing=voiceStacks[i]; break; }
2209
+ 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};
2210
+ var editing=!!existing;
2211
+ var html='<div class="rp-config-editor">'
2212
+ +'<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>'
2213
+ +'<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>'
2214
+ +'<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>'
2215
+ +'<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>'
2216
+ +'<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>'
2217
+ +'<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>'
2218
+ +'<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>'
2219
+ +'<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>';
2220
+ showModal(editing?'Edit voice stack · '+(s.name||s.id):'New voice stack',html); $('rp-modal').classList.add('config-modal');
2221
+ $('cfg-vs-cancel').addEventListener('click',closeModal);
2222
+ $('cfg-vs-browse').addEventListener('click',function(){ var next=voiceStackDraft(); openTtsVoicePicker(next.tts_provider,function(voiceId){ next.tts_voice=voiceId; openVoiceStackManager(editing?id:null,next); }); });
2223
+ $('cfg-vs-save').addEventListener('click',function(){
2224
+ var body=voiceStackDraft(); if(!body.name){ showOut('Voice stack name is required'); return; }
2225
+ if(!editing && !body.id) body.id=body.name.toLowerCase().replace(/[^a-z0-9_-]+/g,'-').replace(/^-|-$/g,'').slice(0,48);
2226
+ var btn=$('cfg-vs-save'); btn.disabled=true; btn.textContent='Saving…';
2227
+ 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); });
2228
+ });
2229
+ }
2230
+
2231
+ function characterDraft(){
2232
+ 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};
2233
+ }
2234
+ function openCharacterManager(id,draft){
2235
+ var existing=null; for(var i=0;i<characterPresets.length;i++) if(String(characterPresets[i].id)===String(id)){ existing=characterPresets[i]; break; }
2236
+ 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>';
2237
+ for(var j=0;j<voiceStacks.length;j++) stackOptions+=configOption(voiceStacks[j].id,voiceStacks[j].name||voiceStacks[j].id,p.voice_stack_id);
2238
+ var html='<div class="rp-config-editor">'
2239
+ +'<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>'
2240
+ +'<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>'
2241
+ +'<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>'
2242
+ +'<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>'
2243
+ +'<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>';
2244
+ showModal(editing?'Edit character · '+(p.name||p.id):'New character preset',html); $('rp-modal').classList.add('config-modal'); $('cfg-cp-cancel').addEventListener('click',closeModal);
2245
+ $('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);}); });
2246
+ }
1980
2247
 
1981
2248
  function addVoiceStackFromTemplate(){
1982
2249
  var name=$('rp-newvoice-name').value.trim(); if(!name){ showOut('Voice stack name required'); return; }
@@ -1988,25 +2255,28 @@ button.act:disabled{opacity:0.5;cursor:default;}
1988
2255
  var id=name.toLowerCase().replace(/[^a-z0-9_-]+/g,'-').replace(/^-|-$/g,'').slice(0,32);
1989
2256
  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); });
1990
2257
  }
1991
- function fillTemplateVoiceList(){
1992
- var el=$('rp-voice-list'); if(!el) return;
1993
- var html='';
1994
- 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>'; }
1995
- if(!voiceStacks.length) html='<div class="pk-note">no voice stacks loaded</div>';
1996
- el.innerHTML=html;
1997
- 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); } }
1998
- }
1999
- function fillTemplateCharacterList(){
2000
- var el=$('rp-char-list'); if(!el) return;
2001
- var html='';
2002
- 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>'; }
2003
- if(!characterPresets.length) html='<div class="pk-note">no character presets loaded</div>';
2004
- el.innerHTML=html;
2005
- }
2258
+ function fillTemplateVoiceList(){
2259
+ var el=$('rp-voice-list'); if(!el) return;
2260
+ var query=String(($('rp-voice-search')&&$('rp-voice-search').value)||'').trim().toLowerCase(), html='',shown=0;
2261
+ 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>'; }
2262
+ 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>';
2263
+ el.innerHTML=html;
2264
+ var count=$('rp-voice-count'); if(count) count.textContent=voiceStacks.length+' stack'+(voiceStacks.length===1?'':'s');
2265
+ 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); } }
2266
+ }
2267
+ function fillTemplateCharacterList(){
2268
+ var el=$('rp-char-list'); if(!el) return;
2269
+ var query=String(($('rp-char-search')&&$('rp-char-search').value)||'').trim().toLowerCase(), html='',shown=0;
2270
+ 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>'; }
2271
+ 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>';
2272
+ el.innerHTML=html;
2273
+ var count=$('rp-char-count'); if(count) count.textContent=characterPresets.length+' preset'+(characterPresets.length===1?'':'s');
2274
+ }
2006
2275
 
2007
2276
  // ── voice-stack edit panel ──
2008
- var editingVoiceStackId=null;
2009
- function openVoiceStackEdit(id){
2277
+ var editingVoiceStackId=null;
2278
+ function openVoiceStackEdit(id){
2279
+ openVoiceStackManager(id); return;
2010
2280
  var s=null; for(var i=0;i<voiceStacks.length;i++){ if(voiceStacks[i].id===id){ s=voiceStacks[i]; break; } }
2011
2281
  if(!s) return;
2012
2282
  editingVoiceStackId=id;
@@ -2050,7 +2320,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2050
2320
  };
2051
2321
  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); });
2052
2322
  }
2053
- 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); });
2323
+ 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); });
2054
2324
  var vsSave=$('rpve-save'); if(vsSave) vsSave.addEventListener('click', saveVoiceStackEdit);
2055
2325
  var vsCancel=$('rpve-cancel'); if(vsCancel) vsCancel.addEventListener('click', function(){ $('rp-voice-edit').style.display='none'; editingVoiceStackId=null; });
2056
2326
  // TTS provider change: re-populate the voice <select> for the new provider.
@@ -2087,13 +2357,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
2087
2357
  });
2088
2358
 
2089
2359
  // ── character-preset edit panel ──
2090
- var editingCharPresetId=null;
2360
+ var editingCharPresetId=null;
2091
2361
  function fillCharEditStackOptions(cur){
2092
2362
  var sel=$('rpce-stack'); if(!sel) return;
2093
2363
  sel.innerHTML='<option value="">— default —</option>';
2094
2364
  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); }
2095
2365
  }
2096
- function openCharPresetEdit(id){
2366
+ function openCharPresetEdit(id){
2367
+ openCharacterManager(id); return;
2097
2368
  var p=null; for(var i=0;i<characterPresets.length;i++){ if(characterPresets[i].id===id){ p=characterPresets[i]; break; } }
2098
2369
  if(!p) return;
2099
2370
  editingCharPresetId=id;
@@ -2118,9 +2389,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
2118
2389
  };
2119
2390
  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); });
2120
2391
  }
2121
- 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); });
2392
+ 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); });
2122
2393
  var cpSave=$('rpce-save'); if(cpSave) cpSave.addEventListener('click', saveCharPresetEdit);
2123
- var cpCancel=$('rpce-cancel'); if(cpCancel) cpCancel.addEventListener('click', function(){ $('rp-char-edit').style.display='none'; editingCharPresetId=null; });
2394
+ var cpCancel=$('rpce-cancel'); if(cpCancel) cpCancel.addEventListener('click', function(){ $('rp-char-edit').style.display='none'; editingCharPresetId=null; });
2395
+ var voiceSearch=$('rp-voice-search'); if(voiceSearch) voiceSearch.addEventListener('input',fillTemplateVoiceList);
2396
+ var charSearch=$('rp-char-search'); if(charSearch) charSearch.addEventListener('input',fillTemplateCharacterList);
2397
+ document.addEventListener('click',function(e){
2398
+ 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; }
2399
+ 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; }
2400
+ 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; }
2401
+ 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); }
2402
+ });
2124
2403
 
2125
2404
  function renderRobotCardSelect(name, current){
2126
2405
  var sel=$('rp-char-'+name); if(!sel) return;
@@ -2150,8 +2429,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
2150
2429
  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>';
2151
2430
  return;
2152
2431
  }
2153
- var robots = robotsFrom(d);
2154
- var tot = (d && d.totals) || {};
2432
+ var robots = robotsFrom(d);
2433
+ renderFleetOperations([], 'rp-fleet', robots);
2434
+ var tot = (d && d.totals) || {};
2155
2435
  var totalSessions = tot.sessions_total!=null?tot.sessions_total:robots.reduce(function(a,x){return a+(x.sessions_total||0);},0);
2156
2436
  var totalTrig = tot.trigger_count!=null?tot.trigger_count:robots.reduce(function(a,x){return a+(x.trigger_count||0);},0);
2157
2437
  totals.innerHTML =
@@ -2179,8 +2459,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
2179
2459
  + '<div class="rp-m"><span class="v '+dcls+'">'+drop+'</span><span class="l">drop rate</span></div>'
2180
2460
  + '</div><div class="rp-actions">'
2181
2461
  + '<button class="rp-btn" data-act="trigger" data-robot="'+idAttr+'"'+(rpReadOnly?' disabled':'')+'>Trigger motion</button>'
2182
- + '<button class="rp-btn" data-act="preview" data-robot="'+idAttr+'">Preview</button>'
2183
- + '<button class="rp-btn danger" data-act="end" data-robot="'+idAttr+'"'+((rpReadOnly||!active)?' disabled':'')+'>End session</button>'
2462
+ + '<button class="rp-btn" data-act="preview" data-robot="'+idAttr+'">Preview</button>'
2463
+ + '<button class="rp-btn danger" data-act="end" data-robot="'+idAttr+'"'+((rpReadOnly||!active)?' disabled':'')+'>End session</button>'
2464
+ + ((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>':'')
2465
+ + (x.voice_stack_id?'<button class="rp-btn" data-config-edit-stack="'+esc(x.voice_stack_id)+'">Edit voice</button>':'')
2184
2466
  + '</div>'
2185
2467
  + '<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>'
2186
2468
  + (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>':'')
@@ -2275,7 +2557,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2275
2557
  s.classList.toggle('active', s.id==='rp-sub-'+name);
2276
2558
  }
2277
2559
  // stop every sub-tab interval, then start only the active one
2278
- var names = ['overview','incidents','live','voice','history','audit','fleet','setup'];
2560
+ var names = ['overview','incidents','live','voice','history','audit','fleet','config','setup'];
2279
2561
  for(var k=0;k<names.length;k++){
2280
2562
  var n=names[k];
2281
2563
  if(rpIntervals[n]){ clearInterval(rpIntervals[n]); rpIntervals[n]=null; }
@@ -2302,7 +2584,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2302
2584
  var h=String(location.hash||'').replace('#','');
2303
2585
  var m = h.match(/^robopark\\/(.+)$/);
2304
2586
  var initial = (m && m[1]) ? m[1] : 'overview';
2305
- if(['overview','incidents','live','voice','history','audit','fleet','setup'].indexOf(initial)<0) initial='overview';
2587
+ if(['overview','incidents','live','voice','history','audit','fleet','config','setup'].indexOf(initial)<0) initial='overview';
2306
2588
  setRpSubTab(initial);
2307
2589
  }
2308
2590
 
@@ -2314,12 +2596,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
2314
2596
  $('rp-modal-body').innerHTML = bodyHtml || '';
2315
2597
  back.classList.add('open');
2316
2598
  }
2317
- function closeModal(){
2318
- var back = $('rp-modal-back');
2319
- if(!back) return;
2320
- back.classList.remove('open');
2321
- $('rp-modal-body').innerHTML = '';
2322
- }
2599
+ function closeModal(){
2600
+ var back = $('rp-modal-back');
2601
+ if(!back) return;
2602
+ back.classList.remove('open');
2603
+ var modal=$('rp-modal'); if(modal) modal.classList.remove('config-modal');
2604
+ $('rp-modal-body').innerHTML = '';
2605
+ }
2323
2606
  // wire the close handlers (once)
2324
2607
  (function(){
2325
2608
  var closeBtn = $('rp-modal-close');
@@ -2479,13 +2762,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
2479
2762
  // input. Bound once on the document so re-opening the modal doesn't
2480
2763
  // stack listeners.
2481
2764
  document.addEventListener('click', function(e){
2482
- var btn = e.target.closest && e.target.closest('.rp-vp-item');
2483
- if(btn){
2484
- var vid = btn.getAttribute('data-voice-id');
2485
- if(vid && ttsPickerState.onPick){ ttsPickerState.onPick(vid); }
2486
- closeModal();
2487
- return;
2488
- }
2765
+ var btn = e.target.closest && e.target.closest('.rp-vp-item');
2766
+ if(btn){
2767
+ var vid = btn.getAttribute('data-voice-id');
2768
+ closeModal();
2769
+ if(vid && ttsPickerState.onPick){ ttsPickerState.onPick(vid); }
2770
+ return;
2771
+ }
2489
2772
  var r = e.target.closest && e.target.closest('#rp-voice-picker-refresh');
2490
2773
  if(r){
2491
2774
  // Re-fetch the catalog, ignore cache, then re-render.
@@ -2597,7 +2880,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2597
2880
  })();
2598
2881
 
2599
2882
  // ── History sub-tab ──
2600
- var rpVoiceCall={room:null,session:null,eventToken:null,muted:false,ending:false,seen:{}};
2883
+ var rpVoiceCall={room:null,LK:null,session:null,eventToken:null,muted:false,camera:false,screen:false,ending:false,seen:{},messages:0,timer:null,startedAt:0,audioContext:null,analyser:null,audioRaf:0,transcriptCollapsed:false};
2601
2884
  function refreshVoiceCallOptions(){
2602
2885
  var cs=$('rp-call-character-select'), ss=$('rp-call-stack-select');
2603
2886
  if(!cs||!ss) return;
@@ -2605,19 +2888,35 @@ button.act:disabled{opacity:0.5;cursor:default;}
2605
2888
  cs.innerHTML='<option value="">select character...</option>'+(characterPresets||[]).map(function(c){return '<option value="'+esc(c.id)+'">'+esc(c.name||c.id)+'</option>';}).join('');
2606
2889
  ss.innerHTML='<option value="">character default</option>'+(voiceStacks||[]).map(function(s){return '<option value="'+esc(s.id)+'">'+esc(s.name||s.id)+' - '+esc(s.stt_provider)+' / '+esc(s.llm_model)+' / '+esc(s.tts_provider)+'</option>';}).join('');
2607
2890
  if(cv) cs.value=cv; if(sv) ss.value=sv;
2608
- paintVoiceCallCharacter();
2891
+ paintVoiceCallCharacter(); paintVoiceCallStack();
2609
2892
  }
2610
2893
  function paintVoiceCallCharacter(){
2611
2894
  var sel=$('rp-call-character-select'), box=$('rp-call-character'); if(!sel||!box) return;
2612
2895
  var chosen=null; for(var i=0;i<characterPresets.length;i++) if(characterPresets[i].id===sel.value){chosen=characterPresets[i];break;}
2613
- if(!chosen){ box.innerHTML='<div><div class="name">Choose a character</div><div class="meta">The selected preset and stack are frozen into session history.</div></div>'; return; }
2896
+ if(!chosen){ box.innerHTML='<div><div class="name">Choose a character</div><div class="meta">Preset and voice stack are frozen into session history.</div></div>'; $('rp-call-avatar').removeAttribute('src'); $('rp-call-avatar-fallback').textContent='RP'; return; }
2614
2897
  var img=chosen.img||rpAvatarFor(chosen.id||chosen.name);
2615
2898
  box.innerHTML=(img?'<img alt="'+esc(chosen.name)+'" src="'+esc(img+QS)+'">':'')+'<div><div class="name">'+esc(chosen.name||chosen.id)+'</div><div class="meta">'+esc(chosen.description||'Ready for an operator voice call')+'</div></div>';
2899
+ $('rp-call-avatar-fallback').textContent=String(chosen.name||chosen.id||'RP').slice(0,2).toUpperCase();
2900
+ if(img)$('rp-call-avatar').src=img+QS;else $('rp-call-avatar').removeAttribute('src');
2616
2901
  var stack=$('rp-call-stack-select'); if(stack && !stack.value && chosen.voice_stack_id) stack.value=chosen.voice_stack_id;
2902
+ paintVoiceCallStack();
2903
+ }
2904
+ function selectedVoiceStack(){
2905
+ var sid=$('rp-call-stack-select').value, chosen=null;
2906
+ if(!sid){var cid=$('rp-call-character-select').value;for(var i=0;i<characterPresets.length;i++)if(characterPresets[i].id===cid){sid=characterPresets[i].voice_stack_id||'';break;}}
2907
+ for(var j=0;j<voiceStacks.length;j++)if(voiceStacks[j].id===sid){chosen=voiceStacks[j];break;} return chosen;
2908
+ }
2909
+ function paintVoiceCallStack(){
2910
+ var wrap=$('rp-call-services'); if(!wrap)return; var s=selectedVoiceStack();
2911
+ if(!s){wrap.innerHTML='<div class="rp-call-service"><span class="ico">STT</span><div><b>Speech recognition</b><small>choose a stack</small></div><span class="ready">standby</span></div><div class="rp-call-service"><span class="ico">LLM</span><div><b>Language model</b><small>choose a stack</small></div><span class="ready">standby</span></div><div class="rp-call-service"><span class="ico">TTS</span><div><b>Voice synthesis</b><small>choose a stack</small></div><span class="ready">standby</span></div>';return;}
2912
+ function svc(code,name,provider,model){return '<div class="rp-call-service"><span class="ico">'+code+'</span><div><b>'+esc(name)+'</b><small>'+esc((provider||'default')+' / '+(model||'default'))+'</small></div><span class="ready">ready</span></div>';}
2913
+ wrap.innerHTML=svc('STT','Speech recognition',s.stt_provider,s.stt_model)+svc('LLM','Language model',s.llm_provider,s.llm_model)+svc('TTS','Voice synthesis',s.tts_provider,s.tts_voice);
2617
2914
  }
2618
2915
  function setVoiceCallStatus(text,state){
2619
2916
  var s=$('rp-call-status'), o=$('rp-call-orb'); if(s)s.textContent=text; if(o)o.className='rp-call-orb '+(state||'');
2620
2917
  }
2918
+ function setVoiceAgentState(title,detail){if($('rp-call-agent-state'))$('rp-call-agent-state').textContent=title;if($('rp-call-agent-detail'))$('rp-call-agent-detail').textContent=detail||'';}
2919
+ function setVoiceEvent(title,detail){var e=$('rp-call-event');if(e)e.innerHTML='<b>'+esc(title)+'</b> '+esc(detail||'');}
2621
2920
  function ensureVoiceLiveKit(){
2622
2921
  if(window.LivekitClient) return Promise.resolve(window.LivekitClient);
2623
2922
  return new Promise(function(resolve,reject){
@@ -2633,7 +2932,41 @@ button.act:disabled{opacity:0.5;cursor:default;}
2633
2932
  var div=document.createElement('div'); div.className='rp-turn '+role;
2634
2933
  div.innerHTML='<div class="who">'+esc(role==='user'?'operator':'character')+'</div><div>'+esc(text)+'</div>';
2635
2934
  wrap.appendChild(div); wrap.scrollTop=wrap.scrollHeight;
2935
+ rpVoiceCall.messages++; if($('rp-call-message-count'))$('rp-call-message-count').textContent=rpVoiceCall.messages+' message'+(rpVoiceCall.messages===1?'':'s');
2936
+ }
2937
+ function fillDeviceSelect(id,devices,kind){
2938
+ var sel=$(id);if(!sel)return;var current=sel.value,rows=devices.filter(function(d){return d.kind===kind;});
2939
+ 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;
2636
2940
  }
2941
+ function refreshBrowserDevices(requestPermission){
2942
+ if(!navigator.mediaDevices||!navigator.mediaDevices.enumerateDevices){$('rp-call-device-status').textContent='unsupported';return Promise.resolve([]);}
2943
+ var permission=Promise.resolve();
2944
+ 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');});
2945
+ 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;});
2946
+ }
2947
+ 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);});}
2948
+ 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(){});}
2949
+ function startVoiceVisualizer(track){
2950
+ 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');
2951
+ 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();
2952
+ }catch(_){}
2953
+ }
2954
+ function stopVoiceVisualizer(){if(rpVoiceCall.audioRaf)cancelAnimationFrame(rpVoiceCall.audioRaf);rpVoiceCall.audioRaf=0;if(rpVoiceCall.audioContext)try{rpVoiceCall.audioContext.close();}catch(_){}rpVoiceCall.audioContext=null;rpVoiceCall.analyser=null;var bars=$('rp-call-bars');if(bars)Array.prototype.forEach.call(bars.querySelectorAll('i'),function(b){b.style.height='16px';});}
2955
+ function updateVoiceTimer(){if(!rpVoiceCall.startedAt)return;var sec=Math.floor((Date.now()-rpVoiceCall.startedAt)/1000),m=Math.floor(sec/60),s=sec%60;$('rp-call-timer').textContent=(m<10?'0':'')+m+':'+(s<10?'0':'')+s;}
2956
+ function updateAgentFromParticipant(participant){if(!participant)return;var a=participant.attributes||{},state=a['lk.agent.state']||a['agent.state']||'';if(state==='speaking')setVoiceAgentState('Speaking','character audio is streaming');else if(state==='thinking')setVoiceAgentState('Thinking','language model is composing');else if(state==='listening')setVoiceAgentState('Listening','speak naturally');else if(state==='initializing')setVoiceAgentState('Preparing agent','loading voice pipeline');}
2957
+ function handleVoiceData(payload,participant,kind,topic){
2958
+ try{var msg=JSON.parse(new TextDecoder().decode(payload));
2959
+ if(topic==='response_latency'){var total=Number(msg.total_ms)||0;$('rp-call-lat-total').textContent=total?total+'ms':'--';$('rp-call-lat-stt').textContent=msg.stt_to_llm_ms!=null?Math.round(msg.stt_to_llm_ms)+'ms':'--';$('rp-call-lat-tts').textContent=msg.llm_to_tts_ms!=null?Math.round(msg.llm_to_tts_ms)+'ms':'--';setVoiceEvent('Response complete',total+'ms round trip');return;}
2960
+ if(topic==='wakeword_state'){setVoiceEvent('Wake word',msg.state==='active'?'active - listening now':'waiting for wake word');return;}
2961
+ if(topic==='model_loading_status'){if(msg.is_loading){setVoiceAgentState('Loading model','cold start in progress');setVoiceEvent('Model status','loading model into memory');}return;}
2962
+ if(topic==='tool_status'){var names=msg.tool_names||[];setVoiceEvent('Tool activity',names.length?names.join(', '):'tool cycle complete');return;}
2963
+ if(topic && topic!=='lk-chat-topic')return;var text=String(msg.message||msg.text||'').trim();if(!text)return;var role=msg.role==='user'?'user':'assistant',key=role+'|'+text;if(rpVoiceCall.seen[key])return;rpVoiceCall.seen[key]=1;appendVoiceTurn(role,text);saveVoiceTurn(role,text,Object.keys(rpVoiceCall.seen).length);
2964
+ }catch(_){}
2965
+ }
2966
+ function attachVoiceMedia(track,tag){var video=$('rp-call-video'),box=$('rp-call-media');try{track.attach(video);box.classList.add('on');$('rp-call-media-tag').textContent=tag;}catch(_){} }
2967
+ function toggleVoiceCamera(){if(!rpVoiceCall.room)return;var next=!rpVoiceCall.camera,opts=$('rp-call-cam-device').value?{deviceId:$('rp-call-cam-device').value}:undefined;$('rp-call-camera').disabled=true;rpVoiceCall.room.localParticipant.setCameraEnabled(next,opts).then(function(){rpVoiceCall.camera=next;$('rp-call-camera').classList.toggle('on',next);$('rp-call-camera').textContent=next?'CAM ON':'CAM';if(next){var pub=rpVoiceCall.room.localParticipant.getTrackPublication(rpVoiceCall.LK.Track.Source.Camera);if(pub&&pub.track)attachVoiceMedia(pub.track,'CAMERA');refreshBrowserDevices(false);}else if(!rpVoiceCall.screen){$('rp-call-media').classList.remove('on');$('rp-call-video').srcObject=null;}}).catch(function(e){setVoiceEvent('Camera',e.message);}).finally(function(){$('rp-call-camera').disabled=false;});}
2968
+ function toggleVoiceScreen(){if(!rpVoiceCall.room)return;var next=!rpVoiceCall.screen;$('rp-call-screen').disabled=true;rpVoiceCall.room.localParticipant.setScreenShareEnabled(next).then(function(){rpVoiceCall.screen=next;$('rp-call-screen').classList.toggle('on',next);$('rp-call-screen').textContent=next?'SCR ON':'SCR';if(next){var pub=rpVoiceCall.room.localParticipant.getTrackPublication(rpVoiceCall.LK.Track.Source.ScreenShare);if(pub&&pub.track)attachVoiceMedia(pub.track,'SCREEN');}else if(!rpVoiceCall.camera){$('rp-call-media').classList.remove('on');$('rp-call-video').srcObject=null;}}).catch(function(e){setVoiceEvent('Screen share',e.message);}).finally(function(){$('rp-call-screen').disabled=false;});}
2969
+ function sendVoiceMessage(text){if(!rpVoiceCall.room||!text)return Promise.resolve();var payload=new TextEncoder().encode(JSON.stringify({message:text,role:'user',timestamp:Date.now()}));return rpVoiceCall.room.localParticipant.publishData(payload,{reliable:true,topic:'lk-chat-topic'}).then(function(){var key='user|'+text;if(!rpVoiceCall.seen[key]){rpVoiceCall.seen[key]=1;appendVoiceTurn('user',text);saveVoiceTurn('user',text,Object.keys(rpVoiceCall.seen).length);}});}
2637
2970
  function saveVoiceTurn(role,text,sequence){
2638
2971
  if(!rpVoiceCall.session||!rpVoiceCall.eventToken)return;
2639
2972
  fetch('/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/transcript'+QS,{
@@ -2645,15 +2978,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
2645
2978
  var cid=$('rp-call-character-select').value, sid=$('rp-call-stack-select').value;
2646
2979
  if(!cid){setVoiceCallStatus('select a character first','');return;}
2647
2980
  if(rpVoiceCall.room)return;
2648
- setVoiceCallStatus('creating scheduled session...','busy'); $('rp-call-start').disabled=true;
2649
- rpAction('POST','/robopark/api/sessions/voice-call',{character_preset_id:cid,voice_stack_id:sid||null,client_name:getOperatorName()||'RoboPark Operator'})
2981
+ setVoiceCallStatus('requesting microphone...','busy');setVoiceAgentState('Opening channel','requesting browser media permission'); $('rp-call-start').disabled=true;
2982
+ 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'});})
2650
2983
  .then(function(call){
2651
2984
  if(!call)throw new Error('scheduler returned no call');
2652
- rpVoiceCall.session=call.session_id; rpVoiceCall.eventToken=call.event_token; rpVoiceCall.seen={}; rpVoiceCall.ending=false;
2985
+ rpVoiceCall.session=call.session_id; rpVoiceCall.eventToken=call.event_token; rpVoiceCall.seen={};rpVoiceCall.messages=0;rpVoiceCall.ending=false;
2653
2986
  $('rp-call-session').textContent=call.session_id; $('rp-call-transcript').innerHTML='<div class="rp-tbl-empty">Connecting to '+esc(call.character&&call.character.name||cid)+'...</div>';
2654
2987
  return ensureVoiceLiveKit().then(function(LK){
2655
- var room=new LK.Room({adaptiveStream:true,dynacast:true}); rpVoiceCall.room=room;
2656
- room.on(LK.RoomEvent.TrackSubscribed,function(track){ if(track.kind==='audio'){var el=track.attach();el.autoplay=true;$('rp-call-audio').appendChild(el);} });
2988
+ var room=new LK.Room({adaptiveStream:true,dynacast:true}); rpVoiceCall.room=room;rpVoiceCall.LK=LK;
2989
+ room.on(LK.RoomEvent.TrackSubscribed,function(track){ if(track.kind==='audio'){var el=track.attach();el.autoplay=true;$('rp-call-audio').appendChild(el);applyVoiceOutputDevice();startVoiceVisualizer(track);setVoiceAgentState('Agent online','voice channel connected');} });
2657
2990
  room.on(LK.RoomEvent.TranscriptionReceived,function(segments,participant){
2658
2991
  for(var i=0;i<(segments||[]).length;i++){
2659
2992
  var seg=segments[i], final=seg.final===true||seg.isFinal===true; if(!final||!String(seg.text||'').trim())continue;
@@ -2662,43 +2995,47 @@ button.act:disabled{opacity:0.5;cursor:default;}
2662
2995
  appendVoiceTurn(role,String(seg.text)); saveVoiceTurn(role,String(seg.text),Object.keys(rpVoiceCall.seen).length);
2663
2996
  }
2664
2997
  });
2665
- room.on(LK.RoomEvent.DataReceived,function(payload,participant,kind,topic){
2666
- if(topic && topic!=='lk-chat-topic')return;
2667
- try{
2668
- var msg=JSON.parse(new TextDecoder().decode(payload));
2669
- var text=String(msg.message||msg.text||'').trim(); if(!text)return;
2670
- var role=msg.role==='user'?'user':'assistant';
2671
- var key=role+'|'+text; if(rpVoiceCall.seen[key])return; rpVoiceCall.seen[key]=1;
2672
- appendVoiceTurn(role,text); saveVoiceTurn(role,text,Object.keys(rpVoiceCall.seen).length);
2673
- }catch(_){}
2674
- });
2998
+ room.on(LK.RoomEvent.DataReceived,handleVoiceData);
2999
+ if(LK.RoomEvent.ParticipantAttributesChanged)room.on(LK.RoomEvent.ParticipantAttributesChanged,function(changed,p){updateAgentFromParticipant(p);});
3000
+ room.on(LK.RoomEvent.ParticipantConnected,function(p){updateAgentFromParticipant(p);setVoiceEvent('Agent connected',p.name||p.identity||'ROBOVOICE worker');});
2675
3001
  room.on(LK.RoomEvent.Disconnected,function(){ if(!rpVoiceCall.ending) finishVoiceCall('connection_lost'); });
2676
- return room.connect(call.server_url,call.token).then(function(){return room.localParticipant.setMicrophoneEnabled(true);});
3002
+ 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);});
2677
3003
  }).then(function(){
2678
- setVoiceCallStatus('connected - listening','live'); $('rp-call-mute').disabled=false; $('rp-call-end').disabled=false;
3004
+ 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();
3005
+ $('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;
2679
3006
  return fetch('/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/joined'+QS,{method:'POST',headers:{'X-RoboPark-Session-Token':rpVoiceCall.eventToken}}).catch(function(){});
2680
3007
  });
2681
3008
  }).catch(function(e){
2682
- setVoiceCallStatus('call failed: '+(e&&e.message||e),''); $('rp-call-start').disabled=false;
3009
+ 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;
3010
+ if(rpVoiceCall.session)rpAction('POST','/robopark/api/sessions/'+encodeURIComponent(rpVoiceCall.session)+'/end?reason=connection_failed');
2683
3011
  if(rpVoiceCall.room){try{rpVoiceCall.room.disconnect();}catch(_){} rpVoiceCall.room=null;}
2684
3012
  });
2685
3013
  }
2686
3014
  function toggleVoiceMute(){
2687
3015
  if(!rpVoiceCall.room)return; rpVoiceCall.muted=!rpVoiceCall.muted;
2688
3016
  rpVoiceCall.room.localParticipant.setMicrophoneEnabled(!rpVoiceCall.muted);
2689
- $('rp-call-mute').textContent=rpVoiceCall.muted?'Unmute microphone':'Mute microphone'; setVoiceCallStatus(rpVoiceCall.muted?'connected - microphone muted':'connected - listening','live');
3017
+ $('rp-call-mute').textContent=rpVoiceCall.muted?'MIC OFF':'MIC';$('rp-call-mute').classList.toggle('on',!rpVoiceCall.muted);setVoiceCallStatus(rpVoiceCall.muted?'microphone muted':'secure channel connected','live');
2690
3018
  }
2691
3019
  function finishVoiceCall(reason){
2692
3020
  if(rpVoiceCall.ending)return; rpVoiceCall.ending=true; var session=rpVoiceCall.session, room=rpVoiceCall.room;
2693
- rpVoiceCall.room=null; if(room){try{room.disconnect();}catch(_){}}
3021
+ rpVoiceCall.room=null;if(rpVoiceCall.timer)clearInterval(rpVoiceCall.timer);rpVoiceCall.timer=null;stopVoiceVisualizer();if(room){try{room.disconnect();}catch(_){}}
2694
3022
  if(session) rpAction('POST','/robopark/api/sessions/'+encodeURIComponent(session)+'/end?reason='+encodeURIComponent(reason||'operator'));
2695
- setVoiceCallStatus('call ended',''); $('rp-call-start').disabled=false; $('rp-call-mute').disabled=true; $('rp-call-end').disabled=true;
2696
- $('rp-call-mute').textContent='Mute microphone'; rpVoiceCall.session=null; rpVoiceCall.eventToken=null; rpVoiceCall.muted=false; setTimeout(function(){rpVoiceCall.ending=false;},300);
3023
+ setVoiceCallStatus('call ended','');setVoiceAgentState('Call complete','session transcript saved');setVoiceEvent('Session closed',reason||'operator');$('rp-call-start').disabled=false;['rp-call-mute','rp-call-camera','rp-call-screen','rp-call-end'].forEach(function(id){$(id).disabled=true;$(id).classList.remove('on');});$('rp-call-message').disabled=true;$('rp-call-send').disabled=true;$('rp-call-media').classList.remove('on');$('rp-call-video').srcObject=null;$('rp-call-mute').textContent='MIC';$('rp-call-camera').textContent='CAM';$('rp-call-screen').textContent='SCR';$('rp-call-session').textContent='No active session';$('rp-call-timer').textContent='00:00';rpVoiceCall.session=null;rpVoiceCall.eventToken=null;rpVoiceCall.muted=false;rpVoiceCall.camera=false;rpVoiceCall.screen=false;rpVoiceCall.startedAt=0;setTimeout(function(){rpVoiceCall.ending=false;},300);
2697
3024
  }
2698
3025
  (function(){
2699
- var cs=$('rp-call-character-select'), start=$('rp-call-start'), mute=$('rp-call-mute'), end=$('rp-call-end');
3026
+ var cs=$('rp-call-character-select'),ss=$('rp-call-stack-select'),start=$('rp-call-start'),mute=$('rp-call-mute'),end=$('rp-call-end');
2700
3027
  if(cs)cs.addEventListener('change',paintVoiceCallCharacter); if(start)start.addEventListener('click',startVoiceCall);
2701
- if(mute)mute.addEventListener('click',toggleVoiceMute); if(end)end.addEventListener('click',function(){finishVoiceCall('operator');});
3028
+ if(ss)ss.addEventListener('change',paintVoiceCallStack);if(mute)mute.addEventListener('click',toggleVoiceMute);if(end)end.addEventListener('click',function(){finishVoiceCall('operator');});
3029
+ $('rp-call-camera').addEventListener('click',toggleVoiceCamera);$('rp-call-screen').addEventListener('click',toggleVoiceScreen);$('rp-call-refresh-devices').addEventListener('click',function(){refreshBrowserDevices(true);});
3030
+ $('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');});
3031
+ $('rp-call-new-character').addEventListener('click',function(){openCharacterManager();});
3032
+ $('rp-call-edit-stack').addEventListener('click',function(){var s=selectedVoiceStack();if(s)openVoiceStackManager(s.id);else showOut('Choose a voice stack to edit');});
3033
+ $('rp-call-new-stack').addEventListener('click',function(){openVoiceStackManager();});
3034
+ $('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);
3035
+ function toggleTranscript(){rpVoiceCall.transcriptCollapsed=!rpVoiceCall.transcriptCollapsed;$('rp-call-transcript').classList.toggle('collapsed',rpVoiceCall.transcriptCollapsed);$('rp-call-collapse').textContent=rpVoiceCall.transcriptCollapsed?'expand transcript':'collapse transcript';}
3036
+ $('rp-call-chat-toggle').addEventListener('click',toggleTranscript);$('rp-call-collapse').addEventListener('click',toggleTranscript);
3037
+ $('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;});});
3038
+ refreshBrowserDevices(false);
2702
3039
  })();
2703
3040
 
2704
3041
  var rpHistoryFilter = { robot: '', endReason: '', range: '7d', search: '' };
@@ -6132,7 +6469,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6132
6469
  +'<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"/>'
6133
6470
  +'<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>'
6134
6471
  +'<div class="pk-note" id="pk-camnote">seeking live feed…</div>'
6135
- +'<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>'
6472
+ +'<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>'
6136
6473
  +'<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>')
6137
6474
  + sect('Remote production test','<div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
6138
6475
  + '<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>'
@@ -6155,7 +6492,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6155
6492
  + 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>')
6156
6493
  + 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>')
6157
6494
  + buildShellSection(schedId(n))
6158
- + 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>');
6495
+ + 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>');
6159
6496
  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>');
6160
6497
  // Put live hardware selection beside the camera and audio tests,
6161
6498
  // ahead of persona and service administration in the Park drawer.
@@ -6177,6 +6514,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
6177
6514
  vl.addEventListener('click', function(){ tryVisionOverlay(n, true); if(!visionOn) showOut('RoboVision feed unavailable: robot has no reachable LAN/Tailscale address'); });
6178
6515
  gl.after(vl);
6179
6516
  }
6517
+ var fullCam=$('pk-camera-fullscreen'); if(fullCam) fullCam.addEventListener('click',function(){ openCameraOperations(n); });
6518
+ 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');});
6519
+ 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');});
6520
+ var newStack=$('pk-new-stack'); if(newStack) newStack.addEventListener('click',function(){openVoiceStackManager();});
6180
6521
  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); });
6181
6522
  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'); });
6182
6523
  var svcList=$('pk-svc-list'); if(svcList) svcList.addEventListener('click', function(ev){
@@ -6293,7 +6634,38 @@ button.act:disabled{opacity:0.5;cursor:default;}
6293
6634
  function set(id,v){ var e=$(id); if(e) e.textContent=v; }
6294
6635
  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)'; }
6295
6636
 
6296
- // ── cam: target a LiveKit track; fall back to a styled preview ──
6637
+ var cameraOpsNode=null,cameraOpsTimer=null;
6638
+ function cameraOpsFeedUrl(n){
6639
+ var ref=n&&(n.displayName||n.name||n.nodeId||n.id); if(!ref)return '';
6640
+ return '/fed/media/robots/'+encodeURIComponent(ref)+'/video_feed'+QS+(QS?'&':'?')+'_='+Date.now();
6641
+ }
6642
+ function refreshCameraOperations(){
6643
+ var n=cameraOpsNode;if(!n)return;var row=schedRow(n)||{},device=schedulerDevice(n)||{},hw=hwByNode[n.nodeId]||{};
6644
+ 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');
6645
+ var img=$('pk-camera-ops-img'),cameraLive=!!(img&&img.dataset.live==='true'),clockEl=$('pk-camera-ops-clock');if(clockEl)clockEl.textContent=clock(Date.now());
6646
+ var rec=$('pk-camera-ops-rec');if(rec)rec.textContent=(cameraLive?'REC / LIVE':'REC / CONNECTING')+' / '+String(n.displayName||n.name||'robot').toUpperCase();
6647
+ 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>';
6648
+ 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>';
6649
+ 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('');
6650
+ 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>';
6651
+ var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
6652
+ }
6653
+ function reloadCameraOperations(){
6654
+ 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...';}
6655
+ 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();
6656
+ }
6657
+ function openCameraOperations(n){
6658
+ 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);
6659
+ }
6660
+ function closeCameraOperations(){
6661
+ 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(){});
6662
+ }
6663
+ (function bindCameraOperations(){
6664
+ 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');
6665
+ 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();});
6666
+ })();
6667
+
6668
+ // ── cam: target a LiveKit track; fall back to a styled preview ──
6297
6669
  function startCam(n){
6298
6670
  idleCam('connecting');
6299
6671
  tryVisionOverlay(n, true);