diff --git a/rulesdocs/refector_rules_and_tracking.md b/rulesdocs/refector_rules_and_tracking.md index 9094b574..9550500b 100644 --- a/rulesdocs/refector_rules_and_tracking.md +++ b/rulesdocs/refector_rules_and_tracking.md @@ -20,6 +20,7 @@ ## WebUI frontend - Split large JSX/components and large backing JS files into folderized modules. - Keep modules clear and focused, with title comments. +- Each component must live entirely inside its own folder; do not leave wrapper/compatibility component files outside that folder. - Remove stale compatibility/leftover code only after usage verification. # REFACTOR TRACKING @@ -58,7 +59,7 @@ ### BIGGEST OFFENDERS - [x] mini summary app - [x] spectator app -- [ ] vip audio upload card +- [x] vip audio upload card - [ ] admin panel - [ ] drive dock action - [ ] gamepad mapping settings @@ -71,11 +72,13 @@ - mini summary app - spectator app - video tile +- vip audio upload card ### LARGE CHANGES - Split `webui/src/mini/MiniSummaryApp.jsx` into folderized modules under `webui/src/mini/MiniSummaryApp/` with a compatibility entrypoint preserved. - Split `webui/src/spectate/SpectatorApp.jsx` into folderized modules under `webui/src/spectate/SpectatorApp/` with a compatibility entrypoint preserved. - Split `webui/src/components/VideoTile.jsx` by extracting HUD, overlays, chat input, and constants into `webui/src/components/VideoTile/` while preserving the existing `VideoTile.jsx` public component API. +- Split `webui/src/components/vip/VipAudioUploadCard.jsx` by extracting transport/audio helpers and UI atoms into `webui/src/components/vip/VipAudioUploadCard/` while preserving the existing `VipAudioUploadCard.jsx` import/export API. ## Done criteria (per item) - [ ] Folderized structure created. diff --git a/server/public/assets/index-CtJOO8C0.js b/server/public/assets/index-BB1PfW7u.js similarity index 99% rename from server/public/assets/index-CtJOO8C0.js rename to server/public/assets/index-BB1PfW7u.js index c95d537e..25c98a58 100644 --- a/server/public/assets/index-CtJOO8C0.js +++ b/server/public/assets/index-BB1PfW7u.js @@ -15,7 +15,7 @@ Please change the parent to l&&l>o){const f=Si/100,d=1-f;if(n<=o)return 0;if(n<=l){const p=(n-o)/(l-o);return Math.round(Math.max(0,Math.min(1,p))*Si)}if(n>=s)return 100;const h=(n-l)/(s-l);return Math.round((f+Math.max(0,Math.min(1,h))*d)*100)}return null}const $A=1600,GA=800;function Nn(...n){return n.filter(Boolean).join(" ")}function Uf({visual:n,orientation:s="horizontal",variant:l="inline",compact:o=!1,showLabel:f,className:d=""}){const h=l==="background",p=s==="vertical",x=f??!h;if(!n?.available){if(h){const ee=Si,oe=Nn("absolute z-10",p?"left-0 right-0 h-[2px]":"top-0 bottom-0 w-[2px]","bg-black/70",E&&!S&&"battery-tick-warn"),ue=p?{bottom:`${ee}%`}:{left:`${ee}%`},H=Nn("absolute inset-0 pointer-events-none",d);return r.jsxs("div",{className:H,children:[r.jsx("div",{className:"absolute inset-0 bg-slate-900/35"}),r.jsx("div",{className:oe,style:ue})]})}return p?null:r.jsx("p",{className:"text-xs text-slate-500",children:"Battery telemetry unavailable"})}const g=n.percentDisplay??0,v=Math.max(0,Math.min(100,g)),y=n.warnDisplayPercent??Si,S=!!n.urgentActive,E=!!n.warnActive,C=S?`${GA}ms`:E?`${$A}ms`:void 0,w=h?S||E?"bg-red-500/45":"bg-emerald-500/45":S||E?"bg-red-500":"bg-emerald-500",A=S||E?"bg-red-900/35":"bg-slate-900/35",N=S||E?"bg-red-900/45":"bg-slate-900/35",T=Nn(h?"absolute inset-0 pointer-events-none":"relative pointer-events-auto",p&&!h?"flex flex-col items-center justify-center":"w-full",d);if(h){const ee=p?{height:`${v}%`}:{width:`${v}%`},oe=Nn("absolute",p?"bottom-0 left-0 right-0":"left-0 top-0 bottom-0",w,"transition-[width,height]",S&&"battery-urgent-flash"),ue=Nn("absolute z-10",p?"left-0 right-0 h-[2px]":"top-0 bottom-0 w-[2px]","bg-black/70",E&&!S&&"battery-tick-warn"),H=p?{bottom:`${y}%`}:{left:`${y}%`};return r.jsxs("div",{className:T,children:[r.jsx("div",{className:Nn("absolute inset-0",A,S&&"battery-urgent-flash")}),r.jsx("div",{className:oe,style:{...ee,animationDuration:C}}),r.jsx("div",{className:ue,style:H})]})}const k=Nn("relative overflow-hidden rounded-sm border border-slate-800",S||E?N:"bg-zinc-900/90",S&&"battery-urgent-flash",p?"w-full flex-1":o?"h-4":"h-5"),O=p?{height:`${v}%`}:{width:`${v}%`},Q=Nn("absolute",p?"bottom-0 left-0 right-0":"left-0 top-0 bottom-0",w,"transition-[width,height]",S&&"battery-urgent-flash"),D=Nn("absolute z-10",p?"left-0 right-0 h-[2px]":"top-0 bottom-0 w-[2px]","bg-black/70",E&&!S&&"battery-tick-warn"),U=p?{bottom:`${y}%`}:{left:`${y}%`},K=`Battery ${g}%`,F=Nn("pointer-events-none absolute inset-0 flex items-center justify-center text-center font-semibold",p?o?"text-[0.55rem]":"text-[0.65rem]":"text-xs"),V=Nn("rounded px-1.5 py-0.5",h?"bg-black/30 text-slate-100":"bg-black/50 text-slate-100");return r.jsxs("div",{className:T,children:[r.jsxs("div",{className:k,children:[r.jsx("div",{className:Q,style:{...O,animationDuration:C}}),r.jsx("div",{className:D,style:U})]}),x&&!p?r.jsx("div",{className:F,children:r.jsx("span",{className:V,children:K})}):null,x&&p?r.jsx("div",{className:"mt-0.5 text-[0.65rem] font-semibold text-slate-100",children:r.jsxs("span",{className:V,children:[g,"%"]})}):null]})}function YA({mobileHud:n=!1,isActiveDriver:s=!1,idleSkipSeconds:l=null}){const o=n?"text-3xl":"text-5xl",f=n?"text-xs":"text-sm",d=n?"text-[0.55rem]":"text-[0.75rem]",h=n?"px-4 py-3":"px-6 py-4",p=s&&typeof l=="number";return r.jsx("div",{className:"pointer-events-none absolute inset-0 z-30 flex items-center justify-center bg-black/55",children:r.jsxs("div",{className:`flex flex-col items-center gap-0.5 rounded border border-amber-300/80 bg-black/70 ${h}`,children:[r.jsx("div",{className:`font-semibold text-amber-200 ${o}`,children:"IT IS YOUR TURN!"}),r.jsx("div",{className:`text-amber-200/80 ${f}`,children:"Start driving!"}),p?r.jsxs("div",{className:`text-red-100/90 ${d}`,children:["Idle skip in ",l,"s"]}):null]})})}function Hf({sensors:n={},variant:s="full",size:l,overlay:o=!1}){const f=l||(s==="mini"?190:260),d=f/2,h=f*.07,p=d,x=d+h,g=d*.8,v=g-4,y=g+4,S=g-14,E=g-6,C=g*.65,M=n?.bumpsAndWheelDrops||{},w=n?.wheelOvercurrents||{},A=n?.wheelLeftCurrentMa??0,N=n?.wheelRightCurrentMa??0,T=n?.sideBrushCurrentMa??0,k=n?.mainBrushCurrentMa??0,O=6,Q=M.bumpLeft?O:0,D=M.bumpRight?O:0,U=JA({count:6,totalSpan:140,gap:6,startAngle:-70}),K=["L","FL","CL","CR","FR","R"],F=[n?.lightBumpLeftSignal,n?.lightBumpFrontLeftSignal,n?.lightBumpCenterLeftSignal,n?.lightBumpCenterRightSignal,n?.lightBumpFrontRightSignal,n?.lightBumpRightSignal],V=U.map((H,R)=>({label:K[R],start:H.start,end:H.end,value:F[R]})),ee=[{label:"Cliff L",start:-60,end:-46,value:n?.cliffLeftSignal,active:n?.cliffLeft},{label:"Cliff FL",start:-32,end:-16,value:n?.cliffFrontLeftSignal,active:n?.cliffFrontLeft},{label:"Cliff FR",start:16,end:32,value:n?.cliffFrontRightSignal,active:n?.cliffFrontRight},{label:"Cliff R",start:46,end:60,value:n?.cliffRightSignal,active:n?.cliffRight}],oe=F.filter(H=>H!=null),ue=oe.length?Math.max(...oe,1200):1200;return r.jsxs("div",{className:`${o?"relative":"surface relative p-1"}`,style:o?{width:`${f}px`,height:`${f}px`}:{height:"100%",width:"100%",aspectRatio:"1 / 1"},children:[o?null:r.jsxs(r.Fragment,{children:[r.jsx("div",{className:"absolute left-1 top-1 text-xs text-slate-400",children:"Top-down"}),r.jsxs("div",{className:"absolute right-1 top-1 text-[0.65rem] text-slate-500",children:["0–",ue]})]}),r.jsxs("svg",{width:"100%",height:"100%",viewBox:`0 0 ${f} ${f}`,preserveAspectRatio:"xMidYMid meet",className:"mx-auto block",children:[r.jsx("circle",{cx:p,cy:x,r:g,fill:"#0f172a",stroke:"#334155",strokeWidth:"2"}),r.jsx(ix,{cx:p-C,cy:x,current:A,drop:M.wheelDropLeft,overcurrent:w.leftWheel,label:"L"}),r.jsx(ix,{cx:p+C,cy:x,current:N,drop:M.wheelDropRight,overcurrent:w.rightWheel,label:"R"}),r.jsx(QA,{cx:p+g*.65,cy:x-g*.55,current:T,overcurrent:w.sideBrush}),r.jsx(KA,{cx:p,cy:x,current:k,overcurrent:w.mainBrush,variant:s,dirtLeft:n?.dirtDetectLeft,dirtRight:n?.dirtDetect}),V.map(H=>{const R=ZA(H.value,ue),L=y+4,Z=L+28;return r.jsx(FA,{cx:p,cy:x,rBase:Z,rTip:L,startDeg:H.start,endDeg:H.end,color:R,value:H.value,max:ue},H.label)}),ee.map(H=>{const R=IA(H.value,H.active);return r.jsx(bf,{cx:p,cy:x,rInner:S,rOuter:E,startDeg:H.start,endDeg:H.end,color:R,opacity:1,pulse:!!H.active},H.label)}),r.jsx(bf,{cx:p,cy:x,rInner:v-Q,rOuter:y-Q,startDeg:-70,endDeg:-6,color:M.bumpLeft?"#ef4444":"#475569",opacity:1,pulse:!!M.bumpLeft}),r.jsx(bf,{cx:p,cy:x,rInner:v-D,rOuter:y-D,startDeg:6,endDeg:70,color:M.bumpRight?"#ef4444":"#475569",opacity:1,pulse:!!M.bumpRight})]})]})}function bf({cx:n,cy:s,rInner:l,rOuter:o,startDeg:f,endDeg:d,color:h,pulse:p=!1,opacity:x=1}){const g=(l+o)/2,v=o-l,y=XA(n,s,g,f,d);return r.jsxs(r.Fragment,{children:[r.jsx("path",{d:y,stroke:h,strokeWidth:v,strokeLinecap:"round",fill:"none",opacity:x}),p?r.jsx("path",{d:y,stroke:"#ef4444",strokeWidth:v,strokeLinecap:"round",fill:"none",className:"animate-pulse",opacity:1}):null]})}function FA({cx:n,cy:s,rBase:l,rTip:o,startDeg:f,endDeg:d,color:h,value:p,max:x}){const g=(f+d)/2,v=pi(n,s,o,g),y=ko(p!=null?p/(x||1):0),S=Math.pow(y,.35),E=l-(l-o)*S,C=pi(n,s,E,f),M=pi(n,s,E,d),w=`M ${v.x} ${v.y} L ${C.x} ${C.y} L ${M.x} ${M.y} Z`;return r.jsx("path",{d:w,fill:h,opacity:1,stroke:"none"})}function XA(n,s,l,o,f){let d=o,h=f;h<=d&&(h+=360);const p=pi(n,s,l,d),x=pi(n,s,l,h);return["M",p.x,p.y,"A",l,l,0,0,1,x.x,x.y].join(" ")}function pi(n,s,l,o){const f=Ug(o);return{x:n+l*Math.cos(f),y:s+l*Math.sin(f)}}function Ug(n){return(n-90)*Math.PI/180}function pd(n,s){if(s)return"#ef4444";const l=Math.abs(n);return l>900?"#f59e0b":l>300?"#22c55e":"#64748b"}function ko(n){return Math.max(0,Math.min(1,n))}function ix({cx:n,cy:s,current:l,drop:o,overcurrent:f,label:d}){const h=Math.abs(l),p=ko(h/1200),x=pd(l,f),g=56,v=14,y=9,S=3,E=g*p,C=d==="L"?-1:1,M=C*(-y/2-S/2),w=C*(v/2+S/2),A=v+y+S+2,N=g+4;return r.jsxs("g",{transform:`translate(${n},${s})`,children:[r.jsx("rect",{x:-A/2,y:-N/2,width:A,height:N,rx:"4",fill:"none",stroke:"#64748b",strokeWidth:"1"}),r.jsx("rect",{x:M-v/2,y:-g/2,width:v,height:g,fill:"#0f172a",stroke:"#0f172a",strokeWidth:"1",rx:"2"}),r.jsx("rect",{x:M-v/2,y:g/2-E,width:v,height:E,fill:x,className:f?"animate-pulse":""}),r.jsx("rect",{x:w-y/2,y:-g/2,width:y,height:g,fill:o?"#ef4444":"#475569",className:o?"animate-pulse":"",rx:"2"}),r.jsx("text",{x:0,y:g/2+10,textAnchor:"middle",className:"fill-slate-200 text-[0.7rem]",children:d})]})}function QA({cx:n,cy:s,current:l,overcurrent:o}){var f=Math.abs(l);f<10&&(f=0);const d=pd(l*3,o),h=43,p=f>0?.65:null;return r.jsxs("g",{style:{transformOrigin:`${n}px ${s}px`,animation:p?`spin ${p}s linear infinite`:"none",animationDirection:p?"reverse":"normal"},children:[r.jsx("circle",{cx:n,cy:s,r:10,fill:"#64748b",stroke:"#64748b",strokeWidth:"1"}),[0,120,240].map(g=>{const v=Ug(g),y=n+h*Math.cos(v),S=s+h*Math.sin(v);return r.jsx("line",{x1:n,y1:s,x2:y,y2:S,stroke:d,strokeWidth:"4",strokeLinecap:"round"},g)}),o?r.jsx("circle",{cx:n,cy:s,r:h+8,stroke:"#ef4444",strokeWidth:"3",fill:"none",className:"animate-pulse"}):null]})}function KA({cx:n,cy:s,current:l,overcurrent:o,variant:f,dirtLeft:d,dirtRight:h}){const p=Math.abs(l),x=pd(l,o),g=1,v=96,y=12,S=`main-brush-pattern-a-${f}`,E=`main-brush-pattern-b-${f}`,C=p>0?.6:null,M=l>=0?1:-1;return r.jsxs("g",{children:[r.jsx("defs",{children:[S,E].map((w,A)=>r.jsxs("pattern",{id:w,patternUnits:"userSpaceOnUse",width:"6",height:"6",children:[r.jsx("rect",{width:"6",height:"6",fill:x,opacity:g}),r.jsx("path",{d:"M0 6 L6 0",stroke:"#0f172a",strokeWidth:"1"}),C?r.jsx("animateTransform",{attributeName:"patternTransform",type:"translate",from:"0 0",to:`0 ${M*(A===0?6:-6)}`,dur:`${C}s`,repeatCount:"indefinite"}):null]},w))}),r.jsx("rect",{x:n-v/2,y:s-14,width:v,height:y,rx:"3",fill:`url(#${S})`,stroke:"#64748b",strokeWidth:"1"}),r.jsx("rect",{x:n-v/2,y:s+4,width:v,height:y,rx:"3",fill:`url(#${E})`,stroke:"#64748b",strokeWidth:"1"}),r.jsx("circle",{cx:n-v/4,cy:s-8,r:3.5,fill:"#fbbf24"}),r.jsx("circle",{cx:n+v/4,cy:s+8,r:3.5,fill:"#fbbf24"}),o?r.jsx("rect",{x:n-v/2-4,y:s-16,width:v+8,height:y*2+16,rx:"6",stroke:"#ef4444",strokeWidth:"3",fill:"none",className:"animate-pulse"}):null]})}function ZA(n,s){if(n==null||n<=0)return"hsl(200 60% 18%)";const o=ko(n/(s||1));return`hsl(${(200+Math.pow(o,.45)*360)%360} 100% 60%)`}function IA(n,s){if(s)return"#ef4444";const l=ko(n==null?0:n/4095),o=[47,55,69],f=[245,158,11],[d,h,p]=o.map((x,g)=>Math.round(x+(f[g]-x)*l));return`rgb(${d}, ${h}, ${p})`}function JA({count:n,totalSpan:s,gap:l,startAngle:o}){const d=(s-l*(n-1))/n,h=[];let p=o;for(let x=0;xr.jsxs("span",{className:"flex items-center justify-between gap-0.5",children:[r.jsx("span",{className:"text-slate-400",children:ne}),r.jsx("span",{className:"font-semibold text-white",children:se})]},ne))]})}),r.jsx("div",{className:`absolute ${U} left-1/2`,style:K,children:r.jsxs("div",{className:`flex items-center gap-0.5 bg-black/80 text-slate-100 ${N} ${T}`,children:[r.jsx("span",{className:"font-semibold text-white rounded px-1 py-[1px] border border-transparent",style:oa(l,.18),children:s||"Unnamed Rover"}),x?r.jsxs("span",{className:"text-slate-300",children:["• ",x]}):null]})})]}),g?r.jsx("div",{className:"pointer-events-none absolute rounded",style:{...oe},children:r.jsx(Hf,{sensors:n,size:240,overlay:!0})}):null]})}return r.jsxs("div",{className:"pointer-events-none absolute inset-0 flex items-center justify-center",children:[r.jsx("div",{className:`absolute ${k} font-medium text-slate-100 ${w}`,children:r.jsxs("div",{className:"flex flex-col gap-0.5 leading-none",children:[r.jsxs("span",{children:["Status: ",o]}),f?r.jsxs("span",{children:["Audio: ",f]}):null,d?r.jsx("span",{className:"text-cyan-300",children:d}):null]})}),S?r.jsx("div",{className:`absolute left-1/2 top-0.5 -translate-x-1/2 rounded bg-black/70 text-slate-100 ${Q} ${O}`,children:S}):null,r.jsx("div",{className:`absolute ${U} left-1/2`,style:K,children:r.jsx("div",{className:`flex gap-0.5 bg-black/80 text-slate-100 ${N} ${T}`,children:r.jsxs("span",{children:["Rover:"," ",r.jsxs("span",{className:"rounded px-1 py-[1px] border border-transparent",style:oa(l,.18),children:['"',s||"Unnamed Rover",'"']})]})})}),g&&p!=="spectator"?r.jsx("div",{className:"pointer-events-none absolute rounded",style:{...oe},children:r.jsx(Hf,{sensors:n,size:240,overlay:!0})}):null]})}const rx=2e3,WA=3e3,eN=3e3,tN=40,nN=1e3,aN={leftWheel:"Left wheel",rightWheel:"Right wheel",mainBrush:"Main brush",sideBrush:"Side brush",limiter:"Overcurrent limit"};function sN({motors:n,fill:s=0,compact:l=!1}){if(!n?.length)return null;const o=n.map(v=>aN[v]||v),f=l?"w-[12rem] h-[3.5rem]":"w-[20rem] h-[7rem]",d=l?"px-2 py-1":"px-4 py-2",h=l?"text-lg":"text-4xl",p=l?"text-xs":"text-xl",x=Math.max(0,Math.min(1,s)),g=`${Math.round(x*100)}%`;return r.jsx("div",{className:`pointer-events-none absolute flex items-center justify-center bg-red-900/50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 ${f}`,children:r.jsxs("div",{className:"relative h-full w-full",children:[r.jsx("div",{className:"absolute inset-0 overflow-hidden",children:r.jsx("div",{className:"h-full bg-red-700/60",style:{width:g}})}),r.jsxs("div",{className:`relative z-10 flex h-full w-full flex-col items-center justify-center text-center font-semibold text-white animate-pulse ${h} ${d}`,children:[r.jsx("div",{children:"OVERCURRENT"}),r.jsx("div",{className:`mt-0 font-medium text-white ${p}`,children:o.join(", ")})]})]})})}function lN({battery:n,compact:s=!1}){if(!n?.available||!n.warnActive&&!n.urgentActive)return null;const l=n.urgentActive?"BATTERY VERY LOW, DOCK THE ROVER AND CHARGE IMMEDIATELY!!":"Battery low! please dock and charge the rover soon.",o=s?"p-2 top-6":"p-4 top-10",f=s?"text-sm":"text-2xl";return r.jsx("div",{className:`pointer-events-none absolute flex items-center justify-center bg-amber-900/60 left-1/2 -translate-x-1/2 ${o}`,children:r.jsx("div",{className:`text-center font-semibold text-white animate-pulse ${f}`,children:r.jsx("div",{children:l})})})}function iN({sensors:n}){const s=[n?.lightBumpLeftSignal,n?.lightBumpFrontLeftSignal,n?.lightBumpCenterLeftSignal,n?.lightBumpCenterRightSignal,n?.lightBumpFrontRightSignal,n?.lightBumpRightSignal],l=s.filter(p=>p!=null).reduce((p,x)=>Math.max(p,x),1200),o=p=>Math.pow(Math.max(0,Math.min(1,(p??0)/l)),.35),f=p=>p==null||p<=0?"hsl(200 60% 18%)":`hsl(${(200+o(p)*360)%360} 100% 60%)`,d=6,h=12;return r.jsx("div",{className:"flex w-full items-center justify-center gap-0.5",children:s.map((p,x)=>{const g=o(p),v=xD.session),{sendMessage:l,onInputFocus:o,onInputBlur:f,blurChat:d,registerInputRef:h,setTypingActive:p}=od(),{value:x}=Ge("tts",{engine:"flite",voice:"rms",pitch:50}),[g,v]=m.useState(""),[y,S]=m.useState(!1),E=s?.role!=="spectator",C=s?.role==="spectator",M=s?.assignment?.roverId||null,A=!!m.useMemo(()=>s?.roster?.find(D=>String(D.id)===String(M))||null,[M,s?.roster])?.audio?.ttsEnabled,N=m.useMemo(()=>{if(!A)return null;const D=x?.engine==="espeak"?"espeak":"flite";if(D==="espeak"){let K=Number.isFinite(x?.pitch)?Math.round(x.pitch):void 0;return typeof K=="number"&&(K=Math.max(0,Math.min(99,K))),{speak:!0,engine:D,pitch:K}}const U=typeof x?.voice=="string"?x.voice:void 0;return{speak:!0,engine:D,voice:U}},[x?.engine,x?.pitch,x?.voice,A]),T=n?"pointer-events-auto absolute bottom-0.5 right-0.5 flex w-[9rem] max-w-[70vw] items-center gap-0.5 rounded bg-black/70 px-0.4 py-0.2":"pointer-events-auto absolute bottom-1 right-1 flex w-[12rem] max-w-[70vw] items-center gap-0.5 rounded bg-black/70 px-0.5 py-0.25",k=n?"min-w-0 flex-1 bg-transparent text-[0.55rem] text-slate-100 placeholder:text-slate-400 focus:outline-none":"min-w-0 flex-1 bg-transparent text-[0.7rem] text-slate-100 placeholder:text-slate-400 focus:outline-none",O=n?"rounded bg-cyan-500/80 px-0.35 py-0.2 text-[0.55rem] font-semibold text-black disabled:opacity-50":"rounded bg-cyan-500/80 px-0.5 py-0.25 text-[0.7rem] font-semibold text-black disabled:opacity-50";async function Q(D){if(D.preventDefault(),!E)return;const U=g.trim();if(U){S(!0);try{await l(U,N),v(""),d(),p(!1)}catch(K){alert(K.message)}finally{S(!1)}}}return C?null:r.jsxs("form",{onSubmit:Q,className:T,children:[r.jsx("input",{className:k,value:g,onChange:D=>{const U=D.target.value;v(U),p(!!U.trim())},onFocus:D=>{o(D),p(!!g.trim())},onBlur:D=>{f(D),p(!1)},onKeyDown:D=>{D.key==="Enter"&&!g.trim()&&(D.preventDefault(),d(),p(!1))},ref:D=>h(D,{target:"hud"}),placeholder:E?"Chat (TTS)":"Spectator",disabled:!E}),r.jsx("button",{type:"submit",disabled:!E||y,className:O,children:"Speak"})]})}function ji({sessionInfo:n,audioSessionInfo:s,videoMode:l="whep",snapshotFeed:o=null,qualityNotice:f=null,label:d,roverColor:h=null,forceMute:p=!1,telemetryFrame:x,batteryConfig:g,layoutFormat:v="desktop",hudVariant:y="default",driverLabel:S=null,hudForceMap:E=!1,hudMapPosition:C="top-center",hudLabelScale:M=1,fitParent:w=!1,overcurrentLimiter:A=null,showTurnCue:N=!1,turnTimerText:T=null,isActiveDriver:k=!1,idleSkipSeconds:O=null}){const Q=it(ye=>ye.session),D=m.useRef(null),U=m.useRef(null),K=m.useRef(null),F=m.useRef(null),V=m.useRef(null),ee=m.useRef(null),oe=m.useRef(null),ue=m.useRef(null),[H,R]=m.useState("idle"),[L,Z]=m.useState(null),[ae,ce]=m.useState("idle"),[q,$]=m.useState(null),[ne,se]=m.useState(0),[pe,re]=m.useState(0),[fe,be]=m.useState(!0),ve=!!s?.url,xe=l==="snapshot",Le=x?.sensors,{value:De}=Ge("audio",st),Ee=Number.isFinite(De?.masterVolume)?De.masterVolume:st.masterVolume,nt=Number.isFinite(De?.roverVolume)?De.roverVolume:st.roverVolume,Ve=typeof De?.mainBrushDuckEnabled=="boolean"?De.mainBrushDuckEnabled:typeof De?.autoLevelEnabled=="boolean"?De.autoLevelEnabled:st.mainBrushDuckEnabled,mt=Number.isFinite(De?.mainBrushDuckAmount)?Math.max(0,Math.min(1,De.mainBrushDuckAmount)):st.mainBrushDuckAmount,ie=Math.max(0,Math.min(1,Ee*nt)),me=Le?.batteryChargeMah??null,J=!(v==="desktop"),he=J?"top-right":C,[Ae]=Lg(),ht=E||J?!0:Ae,Oe=zg({charge:me,config:g}),rt=Le?.wheelOvercurrents||null,ot=rt==null?[]:Object.entries(rt).filter(([,ye])=>!!ye).map(([ye])=>ye),gt=A?.caps||null,kt=A?.overcurrent?.groups||null,fs=typeof window<"u"&&new URLSearchParams(window.location.search).has("debugAudio"),Lt=typeof window<"u"&&new URLSearchParams(window.location.search).has("debugHud"),fl=m.useMemo(()=>{if(!gt)return null;const ye=Number.isFinite(gt?.drive?.cap)?gt.drive.cap:1,Ie=Number.isFinite(gt?.aux?.cap)?gt.aux.cap:1;return Math.max(0,Math.min(1,1-Math.min(ye,Ie)))},[gt]),dl=!!A?.isActive,yn=ot.length?ot:dl?["limiter"]:[],wt=fl??(ot.length?1:0),an=!!yn.length,vt=!!((Number(Le?.mainBrushCurrentMa)||0)>tN||Le?.wheelOvercurrents?.mainBrush),Bo=Ve&&vt?1-mt:1,ds=Math.max(0,Math.min(1,ie*Bo)),Oo=Ve&&vt&&mt>0?`Volume decreased ${Math.round(mt*1e3)/10}%`:null,Oa=m.useRef({hasDedicatedAudio:!1,audioUrl:null,mainBrushDuckEnabled:!1,mainBrushDuckAmount:0,mainBrushActive:!1,baseRoverGain:0,effectiveRoverGain:0});m.useEffect(()=>{Oa.current={hasDedicatedAudio:ve,audioUrl:s?.url||null,mainBrushDuckEnabled:Ve,mainBrushDuckAmount:mt,mainBrushActive:vt,baseRoverGain:ie,effectiveRoverGain:ds}},[ve,s?.url,Ve,mt,vt,ie,ds]);const We=m.useCallback((ye,Ie={})=>{if(!fs)return;const Je=U.current,ke=Oa.current,Ye={event:ye,ts:Date.now(),roverLabel:d||null,hasDedicatedAudio:ke.hasDedicatedAudio,audioUrl:ke.audioUrl,mainBrushDuckEnabled:ke.mainBrushDuckEnabled,mainBrushDuckAmount:ke.mainBrushDuckAmount,mainBrushActive:ke.mainBrushActive,baseRoverGain:ke.baseRoverGain,effectiveRoverGain:ke.effectiveRoverGain,element:Je?{muted:Je.muted,volume:Je.volume,paused:Je.paused,ended:Je.ended,readyState:Je.readyState,networkState:Je.networkState}:null,...Ie};try{console.log(`[AudioDebug] ${ye} ${JSON.stringify(Ye)}`)}catch{console.log("[AudioDebug]",ye,Ye)}},[fs,d]),ms=Q?.socials?.find(ye=>String(ye?.id||ye?.label||"").toLowerCase()==="discord")?.url||Q?.discord?.invite||null;m.useEffect(()=>{Lt&&console.log("[OvercurrentHUD]",{overlayVisible:an,overlayMotors:yn,overlayFill:wt,limiterActive:dl,limiterCaps:gt,limiterGroups:kt,wheelOvercurrents:rt})},[Lt,wt,yn,an,dl,gt,kt,rt]),m.useEffect(()=>{We("settings/update")},[We]);const Ft=m.useCallback(()=>{clearTimeout(K.current),K.current=setTimeout(()=>se(Date.now()),rx)},[]),ua=m.useCallback(()=>{clearTimeout(F.current),F.current=setTimeout(()=>re(Date.now()),rx)},[]),Mn=m.useCallback(async()=>{const ye=D.current;if(ye)try{ye.muted=!0,await ye.play()}catch{}},[]),qi=m.useCallback((ye=0)=>{if(p||ve)return;clearTimeout(ee.current);const Ie=()=>{clearTimeout(ee.current),ee.current=setTimeout(()=>{p||Je()},WA)},Je=async()=>{const ke=D.current;if(ke)try{await Mn(),ke.muted=!1,await ke.play(),be(!1)}catch{ke.muted=!0,be(!0),Ie()}};ee.current=setTimeout(Je,ye)},[Mn,p,ve]);m.useEffect(()=>()=>{clearTimeout(K.current),clearTimeout(F.current),clearTimeout(ee.current),clearInterval(V.current),oe.current&&(cancelAnimationFrame(oe.current),oe.current=null)},[]),m.useEffect(()=>{H==="playing"&&qi(0)},[H,qi]),m.useEffect(()=>{xe&&(R("snapshot"),Z(null))},[xe]),m.useEffect(()=>{if(xe||!n?.url||!D.current)return;let ye=!0,Ie;const Je=setTimeout(()=>be(!0),0),ke=(Ye,Mt)=>{ye&&(We("video/status",{nextStatus:Ye,info:Mt||null}),R(Ye),Z(Mt||null),Ye==="playing"&&Mn(),["error","failed","disconnected","closed"].includes(Ye)&&Ft())};return Ie=new lx({url:n.url,token:n.token,video:D.current,receiveAudio:!ve,onStatus:ke}),Ie.start().catch(Ye=>{ye&&(R("error"),Z(Ye.message),Ft())}),()=>{ye=!1,clearTimeout(Je),Ie?.stop()}},[xe,n?.url,n?.token,ne,Ft,Mn,ve,We]),m.useEffect(()=>{H==="stopped"&&n?.url&&Ft()},[H,n?.url,Ft]),m.useEffect(()=>{const ye=U.current;if(!ye||!s?.url){We("route/no-audio-url"),ue.current=null;return}const Ie=Math.max(0,Math.min(1,ds)),Je=ue.current==null?Math.max(0,Math.min(1,ye.volume)):ue.current;if(oe.current&&(cancelAnimationFrame(oe.current),oe.current=null),Ie>Je){const ke=performance.now(),Ye=Je,Mt=Ie-Ye,zt=ct=>{const ze=Math.min(1,(ct-ke)/nN),ln=Ye+Mt*ze;ye.volume=ln,ue.current=ln,ze<1?oe.current=requestAnimationFrame(zt):oe.current=null};oe.current=requestAnimationFrame(zt)}else ye.volume=Ie,ue.current=Ie;We("route/direct",{elementVolume:Ie,duckEnabled:Ve,mainBrushActive:vt,duckAmount:mt})},[s?.url,ds,Ve,vt,mt,We]),m.useEffect(()=>{if(!s?.url||!U.current)return;let ye=!0,Ie;const Je=(ke,Ye)=>{ye&&(We("audio/status",{nextStatus:ke,info:Ye||null}),$(Ye||(ke==="connected"?"connected":null)),ce(Mt=>ke==="connected"&&(Mt==="playing"||Mt==="connecting")||ke==="new"?Mt:ke),["error","failed"].includes(ke)&&ua())};return Ie=new lx({url:s.url,token:s.token,video:U.current,audioOnly:!0,onStatus:Je}),Ie.start().catch(ke=>{ye&&(ce("error"),$(ke.message),ua())}),()=>{ye=!1,Ie?.stop()}},[s?.url,s?.token,pe,ua,We]),m.useEffect(()=>{const ye=U.current;if(!s?.url||!ye){clearInterval(V.current);return}if(!["connecting","connected","playing","paused"].includes(ae)){clearInterval(V.current);return}const Je=async()=>{const ke=U.current;ke&&(ke.muted=!1,!(!ke.paused&&!ke.ended)&&(We("retry/play-attempt"),ke.play().then(()=>{We("retry/play-ok"),ce(Ye=>Ye==="connected"?"playing":Ye),$(Ye=>Ye==="paused"?null:Ye)}).catch(Ye=>{We("retry/play-fail",{error:Ye?.message}),$(Mt=>Mt||Ye?.message||"autoplay blocked")})))};return Je(),V.current=setInterval(Je,eN),()=>clearInterval(V.current)},[s?.url,ae,We]),m.useEffect(()=>{const ye=U.current;if(!ye)return;const Ie=()=>{We("element/play"),ce(ze=>ze==="error"?ze:"playing"),$(null)},Je=()=>{We("element/pause"),ce(ze=>["error","failed","disconnected","closed","stopped"].includes(ze)?ze:"paused"),$(ze=>ze||"paused")},ke=()=>{We("element/ended"),ce(ze=>ze==="error"?ze:"stopped"),$(ze=>ze||"ended")},Ye=()=>{const{error:ze}=ye,ln=ze?.message||"audio error";We("element/error",{error:ln}),ce("error"),$(ln)},Mt=()=>We("element/waiting"),zt=()=>We("element/canplay"),ct=()=>We("element/stalled");return ye.addEventListener("play",Ie),ye.addEventListener("pause",Je),ye.addEventListener("ended",ke),ye.addEventListener("error",Ye),ye.addEventListener("waiting",Mt),ye.addEventListener("canplay",zt),ye.addEventListener("stalled",ct),()=>{ye.removeEventListener("play",Ie),ye.removeEventListener("pause",Je),ye.removeEventListener("ended",ke),ye.removeEventListener("error",Ye),ye.removeEventListener("waiting",Mt),ye.removeEventListener("canplay",zt),ye.removeEventListener("stalled",ct)}},[s?.url,We]);const Ti=o?.error?`Error: ${o.error}`:o?.objectUrl?"snapshot":o?.status||"waiting",ml=xe?Ti:n?.url?H==="error"?`Error: ${L||"unknown"}`:L?`${H} (${L})`:H:"waiting",La=s?.error?`Error: ${s.error}`:s?.url?ae==="error"?`Error: ${q||"unknown"}`:q?`${ae} (${q})`:ae:null,Di=y==="spectator",sn=y==="none";return r.jsxs("div",{className:`flex flex-col gap-0.5 ${w?"h-full":""}`,children:[r.jsxs("div",{className:`relative w-full overflow-hidden bg-black ${w?"h-full flex-1":"aspect-[4/3]"}`,children:[xe?o?.objectUrl?r.jsx("img",{src:o.objectUrl,alt:d,className:"h-full w-full object-contain",draggable:!1}):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-300",children:"Waiting for frame…"}):r.jsx("video",{ref:D,muted:p||fe||ve,playsInline:!0,autoPlay:!0,controls:!1,className:"h-full w-full object-contain"}),r.jsx("audio",{ref:U,autoPlay:!0,hidden:!0}),!sn&&N?r.jsx(YA,{mobileHud:J,isActiveDriver:k,idleSkipSeconds:O}):null,sn?null:r.jsx(PA,{sensors:Le,label:d,roverColor:h,status:ml,audioStatus:La,levelStatus:Oo,layoutFormat:v,variant:y,driverLabel:S,showTopDown:ht,mobileHud:J,mapPosition:he,turnTimerText:T,labelScale:M}),sn?null:r.jsx(rN,{compact:J}),!sn&&Lt?r.jsx("div",{className:"pointer-events-none absolute left-1 top-1 z-40 rounded bg-black/80 px-1 py-0.5 text-[0.6rem] text-lime-200",children:`OC vis:${an?1:0} motors:${yn.length} fill:${Math.round(wt*100)}%`}):null,sn?null:r.jsx(sN,{motors:yn,fill:wt,compact:J}),sn?null:r.jsx(lN,{battery:Oe,compact:J}),!sn&&Di&&Oe.available?r.jsx("div",{className:"pointer-events-none absolute right-1 top-1/2 flex h-[70%] -translate-y-1/2 flex-col items-center justify-center rounded bg-black/60 px-0.5 pb-1 pt-1",children:r.jsx(Uf,{visual:Oe,orientation:"vertical",variant:"inline",compact:J,className:"h-full w-4"})}):null,!sn&&f?r.jsx("div",{className:"pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2",children:r.jsxs("div",{className:`mx-auto w-fit rounded border border-amber-300/80 bg-black/75 text-amber-200 ${J?"px-2 py-1 text-[0.6rem]":"px-3 py-1.5 text-sm"}`,children:[r.jsx("div",{className:"text-center",children:f}),r.jsx("div",{className:"pointer-events-auto mt-0",children:r.jsx(hd,{id:"discord",label:"Join our Discord server while you wait!",url:ms})})]})}):null]}),!sn&&!Di&&r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(iN,{sensors:Le}),r.jsx("div",{className:"panel-section space-y-0.5 text-sm",children:r.jsx(Uf,{visual:Oe,compact:J})})]})]})}function xd({layoutFormat:n="desktop"}){const s=it(se=>se.session),{state:{song:l,lastControlIntentAt:o},overcurrentLimiter:f}=_t(),[d,h]=m.useState(()=>Date.now()),[p,x]=m.useState(!1),[g,v]=m.useState(null),y=m.useRef({active:!1,roverId:null});m.useEffect(()=>{if(s?.mode!=="turns")return;const se=setInterval(()=>h(Date.now()),250);return()=>clearInterval(se)},[s?.mode]);const S=s?.assignment?.roverId,C=!!(S&&s?.roster?s.roster.find(se=>String(se.id)===String(S)):null)?.media?.audioPublishUrl,M=S?s?.turnQueues?.[S]:null,w=s?.socketId||null,A=S?s?.activeDrivers?.[S]:null,N=!!(w&&A===w),T=m.useMemo(()=>{const se=M?.queue||[];if(!se.length||!M?.current||se.length<=1)return null;const pe=se.findIndex(re=>re===M.current);return pe===-1?se[0]||null:se[(pe+1)%se.length]||null},[M?.queue,M?.current]),k=!!(w&&T===w),O=M?.deadline||null,Q=M?.idleDeadline||null,D=O?O-d:null,U=Q?Q-d:null,K=s?.mode==="turns"&&k&&D!=null&&D<=5e3&&D>0,F=s?.mode!=="turns"||N||K,V=D!=null&&Number.isFinite(D)?Math.max(0,Math.ceil(D/1e3)):null,ee=U!=null&&Number.isFinite(U)?Math.max(0,Math.ceil(U/1e3)):null,oe=N?V!=null?`${V}s left`:null:k&&V!=null?`Your turn in ${V}s`:null,ue=S?[...F?[{type:"rover",id:S,key:S}]:[],...C?[{type:"rover",id:`${S}-audio`,key:`${S}-audio`}]:[]]:[],H=yi(ue),R=S&&F?H[S]:null,L=S&&C?H[`${S}-audio`]:null,Z=md(S?[S]:[],{enabled:!!S,version:s?.mode}),ae=S&&Z[S]||null,ce=ul(S),q=S&&s?.roster?s.roster.find(se=>String(se.id)===String(S)):null,$=q?.battery??null,ne=q?.name||(S?`Rover ${S}`:"");return m.useEffect(()=>{if(s?.mode!=="turns"){x(!1),v(null),y.current={active:!1,roverId:null};return}const se=y.current,pe=N&&!se.active,re=N&&S&&S!==se.roverId;pe||re?(x(!0),v(Date.now())):!N&&se.active&&(x(!1),v(null)),y.current={active:N,roverId:S}},[N,S,s?.mode]),m.useEffect(()=>{!p||!g||o>g&&x(!1)},[o,g,p]),r.jsx("section",{className:"panel",children:S?r.jsx(ji,{sessionInfo:R,videoMode:F?"whep":"snapshot",snapshotFeed:ae,audioSessionInfo:L,label:ne,roverColor:q?.color||null,telemetryFrame:ce,batteryConfig:$,layoutFormat:n,overcurrentLimiter:f,songNote:l?.note,qualityNotice:F?null:"Preview feed (low FPS) until your turn.",showTurnCue:p,turnTimerText:oe,turnSeconds:V,isActiveDriver:N,idleSkipSeconds:ee}):r.jsxs("div",{className:"panel-muted content-center text-center text-sm text-slate-400 aspect-[4/3]",children:[r.jsx("p",{children:"You are not assigned to a rover."}),r.jsx("p",{className:"mt-0",children:r.jsx("a",{href:"/spectate",className:"text-blue-400 underline hover:text-blue-500",children:"Click here to visit the spectator page."})})]})})}function yo({label:n,tone:s="muted"}){const l=s==="success"?"bg-emerald-900 text-emerald-100":s==="warn"?"bg-amber-900 text-amber-100":"bg-slate-800 text-slate-200";return r.jsx("span",{className:`rounded px-1 py-0.5 text-xs font-semibold leading-none ${l}`,children:n})}function Mo(n){if(!Number.isFinite(n))return 0;const s=n%360;return s<0?s+360:s}function oN(n){if(!Array.isArray(n)||n.length<3)return 0;const[s,l,o]=n,f=Math.max(0,Math.min(255,Number(s)))/255,d=Math.max(0,Math.min(255,Number(l)))/255,h=Math.max(0,Math.min(255,Number(o)))/255,p=Math.max(f,d,h),x=Math.min(f,d,h),g=p-x;if(g===0)return 0;let v=0;return p===f?v=(d-h)/g%6:p===d?v=(h-f)/g+2:v=(f-d)/g+4,Mo(Math.round(v*60))}function ox(n){const s=Mo(n),l=1,o=1-Math.abs(s/60%2-1);let f=0,d=0,h=0;return s<60?(f=l,d=o):s<120?(f=o,d=l):s<180?(d=l,h=o):s<240?(d=o,h=l):s<300?(f=o,h=l):(f=l,h=o),[Math.round(f*255),Math.round(d*255),Math.round(h*255)]}function cx(n){return n?Array.isArray(n.hsColor)?Mo(Number(n.hsColor[0])):Array.isArray(n.rgbColor)?oN(n.rgbColor):0:0}function cN({entity:n,connected:s,controlsLocked:l,onToggle:o,onSetColor:f,onSetWhite:d}){const h=n.state==="unavailable"||!n.available,p=n.state==="on",x=n.type==="light"&&n.supportsColor,g=h?"warn":p?"success":"muted",v=h?"Unavailable":p?"On":"Off",y=l||!s||h,S=y||!x,[E,C]=m.useState(()=>cx(n)),M=m.useRef(E),w=m.useRef(!1),A=h?"border-slate-800 bg-slate-900 text-slate-400 cursor-not-allowed":l?"border-slate-800 bg-slate-900 text-slate-300 cursor-not-allowed":p?"border-emerald-700 bg-emerald-900/80 text-emerald-50 hover:bg-emerald-800":"border-rose-800 bg-rose-900/80 text-rose-50 hover:bg-rose-800";m.useEffect(()=>{if(!x||w.current)return;const U=cx(n);M.current=U,C(U)},[n.rgbColor,n.hsColor,x]);const N=U=>{const K=Mo(Number(U.target.value));M.current=K,C(K)},T=()=>{S||!f||f(n.id,ox(M.current))},k=U=>{U.stopPropagation()},O=U=>{k(U),!(S||!d)&&d(n.id)},D=`rgb(${(x?ox(M.current):[255,255,255]).join(",")})`;return r.jsxs("button",{type:"button",onClick:()=>o(n.id),disabled:y,className:`relative flex min-w-[12rem] flex-[1_1_12rem] items-start justify-between gap-0.5 rounded px-1 py-0.5 text-left transition-colors ${A} disabled:opacity-60 disabled:hover:bg-inherit`,children:[r.jsxs("div",{className:"min-w-0 flex-1",children:[r.jsxs("div",{className:"flex items-center gap-0.5 text-sm leading-normal",children:[r.jsx("span",{className:"truncate font-semibold text-white",children:n.name||n.id}),x&&r.jsxs(r.Fragment,{children:[r.jsx(yo,{label:v,tone:g}),!s&&r.jsx("span",{className:"text-xs text-amber-200",children:"Offline"})]})]}),x?r.jsx("div",{className:"-mt-0.5 w-full",onClick:k,onPointerDown:U=>{k(U),w.current=!0},onPointerUp:U=>{k(U),w.current=!1,T()},onPointerCancel:U=>{k(U),w.current=!1,T()},onKeyUp:U=>{k(U),T()},onBlur:T,children:r.jsx("div",{className:"relative w-full",children:r.jsx("input",{type:"range",min:"0",max:"360",step:"1",value:E,onChange:N,disabled:S,"aria-label":`Set color for ${n.name||n.id}`,className:"ha-hue-slider w-full"})})}):r.jsxs("div",{className:"flex items-center gap-0.5 text-xs text-slate-400",children:[r.jsx(yo,{label:v,tone:g}),!s&&r.jsx("span",{className:"text-amber-200",children:" · Offline"})]})]}),x&&r.jsxs("div",{className:"absolute right-1 top-1 flex items-center gap-0.5",children:[r.jsx("span",{role:"button",tabIndex:S?-1:0,onClick:O,onPointerDown:k,onKeyDown:U=>{S||(U.key==="Enter"||U.key===" ")&&(U.preventDefault(),O(U))},title:"Set white color temperature","aria-label":`Set ${n.name||n.id} to white`,"aria-disabled":S,className:`inline-flex items-center justify-center rounded border border-slate-200 bg-white px-1 py-0.5 text-xs font-semibold leading-none text-slate-900 ${S?"cursor-not-allowed opacity-60":"cursor-pointer hover:bg-slate-100"}`,children:"white"}),r.jsx("span",{className:"pointer-events-none h-2.5 w-2.5 rounded-full border border-white/60",style:{backgroundColor:D}})]}),!x&&r.jsx("div",{className:"self-center text-xs font-semibold text-white/90",children:p?"Turn off":"Turn on"})]})}function Hg(){const{state:{keymap:n}}=_t(),s=it(E=>E.session),{homeAssistantToggle:l,homeAssistantSetLightColor:o,homeAssistantSetLightWhite:f}=kn(),d=s?.homeAssistant,h=m.useMemo(()=>d?.entities||[],[d?.entities]),p=d?.lightPolicy||null,x=!!(p?.locked||p?.lockedOn),g=p?.lockState||(p?.lockedOn?"on":null),v=Cn(n?.homeAssistantOn?.[0]),y=Cn(n?.homeAssistantOff?.[0]);if(!d?.enabled)return r.jsxs("section",{className:"panel-section space-y-0.5 text-sm text-slate-400",children:[r.jsx("p",{className:"text-slate-300",children:"Room Controls"}),r.jsx("p",{className:"text-slate-500",children:"Not configured on the server."})]});if(h.length===0)return r.jsxs("section",{className:"panel-section space-y-0.5 text-sm text-slate-400",children:[r.jsx("p",{className:"text-slate-300",children:"Room Controls"}),r.jsx("p",{className:"text-slate-500",children:"No lights or switches configured."})]});const S=!!d?.connected;return r.jsxs("section",{className:"panel-section space-y-0.5 text-base",children:[r.jsxs("header",{className:"flex items-center justify-between gap-0.5 text-sm text-slate-400",children:[r.jsxs("div",{className:"flex items-center gap-0.5",children:[r.jsx("p",{children:"Room Controls"}),r.jsx("span",{className:"text-xs text-slate-500",children:h.length}),x?r.jsx(yo,{label:g==="off"?"Locked Off":"Locked On",tone:"warn"}):null,r.jsxs("div",{className:"flex items-center gap-0.5 text-xs text-slate-300 background-black",children:[r.jsxs("span",{className:"flex items-center gap-0.5",children:[r.jsx("span",{children:"On"}),v?r.jsx(ux,{label:v}):null]}),r.jsxs("span",{className:"flex items-center gap-0.5",children:[r.jsx("span",{children:"Off"}),y?r.jsx(ux,{label:y}):null]})]})]}),r.jsx(yo,{label:S?"Connected":"Offline",tone:S?"success":"warn"})]}),x?r.jsx("p",{className:"rounded border border-amber-600/60 bg-amber-900/40 px-1 py-0.5 text-xs text-amber-100",children:g==="off"?"Lights are locked off. Room controls are disabled.":"Lights are locked on. Room controls are disabled."}):null,r.jsx("div",{className:"flex flex-wrap gap-0.5",children:h.map(E=>r.jsx(cN,{entity:E,connected:S,controlsLocked:x,onToggle:l,onSetColor:o,onSetWhite:f},E.id))})]})}function ux({label:n}){return n?r.jsx("span",{className:"rounded border border-white/40 px-1 text-[0.7rem] text-white",children:n}):null}function Vg(){const{login:n,setRole:s}=kn(),[l,o]=m.useState(""),[f,d]=m.useState(""),[h,p]=m.useState(!1);async function x(g){g.preventDefault(),p(!0);try{await n(l,f),o(""),d("")}catch(v){alert(v.message)}finally{p(!1)}}return r.jsxs("div",{className:"panel-section space-y-0.5 text-base",children:[r.jsx("p",{className:"text-sm text-slate-400",children:"Admin login"}),r.jsxs("form",{className:"flex flex-col gap-0.5",onSubmit:x,children:[r.jsx("input",{className:"field-input",placeholder:"Username",value:l,onChange:g=>o(g.target.value)}),r.jsx("input",{className:"field-input",placeholder:"Password",type:"password",value:f,onChange:g=>d(g.target.value)}),r.jsx("button",{type:"submit",disabled:h,className:"button-dark w-full disabled:opacity-50",children:h?"Logging in…":"Login"})]})]})}function fx(...n){return n.filter(Boolean).join(" ")}function uN(n){const s=n?.batteryState?.percentDisplay;return s==null?"--":`${s}%`}function fN(n){return n?.batteryState?n.batteryState.urgentActive?"text-red-400":n.batteryState.warnActive?"text-amber-300":"text-emerald-300":"text-slate-400"}function dN({title:n,roster:s=[],emptyText:l="No roster data.",renderActions:o=null}){return r.jsxs("div",{className:"space-y-0.5 text-sm",children:[n&&r.jsx("p",{className:"text-sm text-slate-400",children:n}),s.length===0?r.jsx("p",{className:"text-sm text-slate-500",children:l}):r.jsx("ul",{className:"space-y-0.5 text-sm",children:s.map(f=>{const d=!!(f?.private?.enabled&&f?.private?.open),h=!!f?.locked;return r.jsxs("li",{className:fx("surface flex flex-wrap items-center justify-between gap-0.5",h?"bg-red-900/40":d?"bg-amber-700/35 border border-amber-200/30":null),children:[r.jsxs("div",{children:[r.jsx("p",{className:"text-slate-200",children:r.jsx("span",{className:"rounded px-1 py-[1px] border border-transparent",style:oa(f.color,.16),children:f.name})}),r.jsxs("p",{className:"text-xs text-slate-500 flex flex-wrap items-center gap-0.5",children:[r.jsx("span",{children:f.locked?"locked":"free"}),f.lockReason&&r.jsx("span",{className:"rounded bg-black/30 px-1",children:f.lockReason}),r.jsxs("span",{className:fx("font-semibold",fN(f)),children:["Battery ",uN(f)]})]})]}),o?o(f):null]},f.id)})})]})}const mN=[{key:"open",label:"Open"},{key:"turns",label:"Turns"},{key:"admin",label:"Admin"},{key:"lockdown",label:"Lockdown"}];function hN(){const{session:n,lockRover:s,setMode:l,requestControl:o,setCommunityGoal:f,setAdminReason:d,rebootRover:h,rebootServer:p,setAudioLevels:x,setPrivateSafety:g,llmControl:v,adminLogs:y,llmCommentaryState:S}=nn(),E=m.useMemo(()=>n?.roster??[],[n?.roster]),[C,M]=m.useState({}),[w,A]=m.useState({}),[N,T]=m.useState(!1),[k,O]=m.useState(!1),Q=n?.health||null,D=n?.communityGoal?.text||"",U=n?.communityGoal?.updatedAt||null,[K,F]=m.useState(D),V=n?.adminReason?.text||"",ee=n?.adminReason?.updatedAt||null,[oe,ue]=m.useState(V),H=n?.audioLevels||{},[R,L]=m.useState({hornGain:Number.isFinite(H.hornGain)?H.hornGain:1,ttsGain:Number.isFinite(H.ttsGain)?H.ttsGain:1,forwardGain:Number.isFinite(H.forwardGain)?H.forwardGain:1}),[Z,ae]=m.useState({}),ce=n?.role==="admin"||n?.role==="lockdown"||n?.role==="lockdown-admin",q=n?.role==="lockdown"||n?.role==="lockdown-admin",$=n?.mode??"open",ne=async(le,J)=>{try{await s(le,J),M(he=>({...he,[le]:J}))}catch(he){alert(he.message)}},se=async le=>{const J=le.target.value;try{await l(J)}catch(he){alert(he.message)}},pe=async le=>{try{await o(le,{force:!0})}catch(J){alert(J.message)}},re=async le=>{if(!(!le?.id||!window.confirm(`Reboot rover "${le.name||le.id}" now?`))){A(he=>({...he,[le.id]:!0}));try{await h(le.id)}catch(he){alert(he.message)}finally{A(he=>({...he,[le.id]:!1}))}}},fe=async()=>{if(window.confirm("Reboot the server host now? This will disconnect all users.")){T(!0);try{await p()}catch(J){alert(J.message),T(!1)}}},be=async()=>{if(window.confirm("Clear LLM commentary history now? This resets chat context, bot memory, and rover activity metrics for narration.")){O(!0);try{await v("clearHistory")}catch(J){alert(J.message)}finally{O(!1)}}},ve=async()=>{try{await f(K)}catch(le){alert(le.message)}},xe=async()=>{try{await f(null)}catch(le){alert(le.message)}},Le=async()=>{try{await d(oe)}catch(le){alert(le.message)}},De=async()=>{try{await d(null)}catch(le){alert(le.message)}},Ee=le=>J=>{const he=Number(J.target.value);L(Ae=>({...Ae||{},[le]:Number.isFinite(he)?he:1}))},nt=async()=>{try{await x(R)}catch(le){alert(le.message)}},Ve=async()=>{window.dispatchEvent(new CustomEvent("buttonBox:rewardRunLocalTest",{detail:{rewardName:"Admin Mode",count:1e3,goal:1e3}}))};m.useEffect(()=>{F(D)},[D]),m.useEffect(()=>{ue(V)},[V]),m.useEffect(()=>{L({hornGain:Number.isFinite(H.hornGain)?H.hornGain:1,ttsGain:Number.isFinite(H.ttsGain)?H.ttsGain:1,forwardGain:Number.isFinite(H.forwardGain)?H.forwardGain:1})},[H.forwardGain,H.hornGain,H.ttsGain]),m.useEffect(()=>{const le={};(E||[]).forEach(J=>{J?.private?.enabled&&(le[J.id]={speedLimitEnabled:!!J?.private?.safety?.speedLimitEnabled,speedLimitMaxWheelSpeed:Number.isFinite(J?.private?.safety?.speedLimitMaxWheelSpeed)?J.private.safety.speedLimitMaxWheelSpeed:250,hardOvercurrentEnabled:!!J?.private?.safety?.hardOvercurrentEnabled,overcurrentStopMs:Number.isFinite(J?.private?.safety?.overcurrentStopMs)?J.private.safety.overcurrentStopMs:300,hardBumpEnabled:!!J?.private?.safety?.hardBumpEnabled,bumpBackoffSpeed:Number.isFinite(J?.private?.safety?.bumpBackoffSpeed)?J.private.safety.bumpBackoffSpeed:250,bumpBackoffMs:Number.isFinite(J?.private?.safety?.bumpBackoffMs)?J.private.safety.bumpBackoffMs:350,cliffEnabled:!!J?.private?.safety?.cliffEnabled,cliffBackoffSpeed:Number.isFinite(J?.private?.safety?.cliffBackoffSpeed)?J.private.safety.cliffBackoffSpeed:250,cliffBackoffMs:Number.isFinite(J?.private?.safety?.cliffBackoffMs)?J.private.safety.cliffBackoffMs:500,triggerCooldownMs:Number.isFinite(J?.private?.safety?.triggerCooldownMs)?J.private.safety.triggerCooldownMs:800})}),ae(le)},[E]);const mt=m.useMemo(()=>{const le={};return E.forEach(J=>{le[J.id]=C[J.id]??J.locked}),le},[E,C]),ie=(le,J={})=>{ae(he=>({...he||{},[le]:{...he?.[le]||{},...J||{}}}))},me=async le=>{try{const J=Z?.[le];if(!J)return;await g(le,J)}catch(J){alert(J.message)}};return ce?r.jsxs("section",{className:"panel-section space-y-0.5 text-base",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5 text-sm",children:[r.jsx("span",{children:"Admin controls"}),r.jsx("select",{value:$,onChange:se,className:"field-input text-sm",children:mN.map(le=>r.jsx("option",{value:le.key,children:le.label},le.key))})]}),r.jsxs("div",{className:"flex gap-0.5 text-xs",children:[r.jsx("button",{type:"button",onClick:fe,disabled:N,className:"button-danger disabled:cursor-not-allowed disabled:opacity-60",children:N?"Server rebooting...":"Reboot Server"}),r.jsx("button",{type:"button",onClick:Ve,className:"button-dark",children:"Test Reward Popup"})]}),r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-400",children:[r.jsx("span",{children:"Global audio levels"}),n?.audioLevels?.updatedAt?r.jsxs("span",{children:["Updated ",new Date(n.audioLevels.updatedAt).toLocaleString()]}):null]}),r.jsxs("label",{className:"grid gap-0.5 text-xs text-slate-200",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsx("span",{children:"Horn gain"}),r.jsxs("span",{children:[R.hornGain.toFixed(2),"x"]})]}),r.jsx("input",{type:"range",min:"0",max:"4",step:"0.01",value:R.hornGain,onChange:Ee("hornGain"),className:"w-full accent-emerald-500"})]}),r.jsxs("label",{className:"grid gap-0.5 text-xs text-slate-200",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsx("span",{children:"TTS gain"}),r.jsxs("span",{children:[R.ttsGain.toFixed(2),"x"]})]}),r.jsx("input",{type:"range",min:"0",max:"4",step:"0.01",value:R.ttsGain,onChange:Ee("ttsGain"),className:"w-full accent-emerald-500"})]}),r.jsxs("label",{className:"grid gap-0.5 text-xs text-slate-200",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsx("span",{children:"Forward gain"}),r.jsxs("span",{children:[R.forwardGain.toFixed(2),"x"]})]}),r.jsx("input",{type:"range",min:"0",max:"4",step:"0.01",value:R.forwardGain,onChange:Ee("forwardGain"),className:"w-full accent-emerald-500"})]}),r.jsx("div",{className:"flex gap-0.5 text-xs",children:r.jsx("button",{type:"button",onClick:nt,className:"button-dark",children:"Apply audio levels"})})]}),r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-400",children:[r.jsx("span",{children:"Community goal"}),U?r.jsxs("span",{children:["Updated ",new Date(U).toLocaleString()]}):null]}),r.jsx("input",{type:"text",value:K,onChange:le=>F(le.target.value),placeholder:"Set a community goal",className:"field-input text-sm"}),r.jsxs("div",{className:"flex gap-0.5 text-xs",children:[r.jsx("button",{type:"button",onClick:ve,className:"button-dark",children:"Set goal"}),r.jsx("button",{type:"button",onClick:xe,className:"button-danger",children:"Clear"})]})]}),r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-400",children:[r.jsx("span",{children:"Admin mode reason"}),ee?r.jsxs("span",{children:["Updated ",new Date(ee).toLocaleString()]}):null]}),r.jsx("textarea",{value:oe,onChange:le=>ue(le.target.value),placeholder:"Set an admin mode reason",className:"field-input text-sm min-h-[3.5rem]"}),r.jsxs("div",{className:"flex gap-0.5 text-xs",children:[r.jsx("button",{type:"button",onClick:Le,className:"button-dark",children:"Set reason"}),r.jsx("button",{type:"button",onClick:De,className:"button-danger",children:"Clear"})]})]}),r.jsx(dN,{roster:E,renderActions:le=>r.jsxs("div",{className:"flex flex-wrap items-center gap-0.5 text-xs",children:[r.jsx("button",{type:"button",onClick:()=>ne(le.id,!mt[le.id]),className:"button-dark",children:mt[le.id]?"Unlock":"Lock"}),r.jsx("button",{type:"button",onClick:()=>pe(le.id),className:"button-dark",children:"Force"}),r.jsx("button",{type:"button",onClick:()=>re(le),disabled:!!w[le.id],className:"button-danger disabled:cursor-not-allowed disabled:opacity-60",children:w[le.id]?"Rebooting...":"Reboot"}),le?.private?.enabled?r.jsxs("div",{className:"w-full rounded border border-slate-700/70 p-0.5 space-y-0.5 text-[0.7rem]",children:[r.jsx("div",{className:"text-slate-300",children:"Private safety"}),r.jsxs("label",{className:"flex items-center gap-0.5",children:[r.jsx("input",{type:"checkbox",checked:!!Z?.[le.id]?.speedLimitEnabled,disabled:!q,onChange:J=>ie(le.id,{speedLimitEnabled:!!J.target.checked})}),r.jsx("span",{children:"Speed limit"})]}),r.jsx("input",{type:"number",min:"1",max:"500",className:"field-input text-xs w-20",disabled:!q,value:Z?.[le.id]?.speedLimitMaxWheelSpeed??250,onChange:J=>ie(le.id,{speedLimitMaxWheelSpeed:Number(J.target.value)||250})}),r.jsxs("label",{className:"flex items-center gap-0.5",children:[r.jsx("input",{type:"checkbox",checked:!!Z?.[le.id]?.hardOvercurrentEnabled,disabled:!q,onChange:J=>ie(le.id,{hardOvercurrentEnabled:!!J.target.checked})}),r.jsx("span",{children:"Hard overcurrent"})]}),r.jsxs("label",{className:"flex items-center gap-0.5",children:[r.jsx("input",{type:"checkbox",checked:!!Z?.[le.id]?.hardBumpEnabled,disabled:!q,onChange:J=>ie(le.id,{hardBumpEnabled:!!J.target.checked})}),r.jsx("span",{children:"Hard bump"})]}),r.jsxs("label",{className:"flex items-center gap-0.5",children:[r.jsx("input",{type:"checkbox",checked:!!Z?.[le.id]?.cliffEnabled,disabled:!q,onChange:J=>ie(le.id,{cliffEnabled:!!J.target.checked})}),r.jsx("span",{children:"Cliff safety"})]}),r.jsx("button",{type:"button",disabled:!q,onClick:()=>me(le.id),className:"button-dark disabled:opacity-50",children:"Apply Safety"})]}):null]})}),r.jsx(vN,{health:Q,roster:E}),r.jsx(pN,{state:S,onClearHistory:be,clearingHistory:k}),r.jsx(bN,{entries:y})]}):null}function pN({state:n,onClearHistory:s,clearingHistory:l}){const[o,f]=m.useState(null);if(!n)return r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("div",{className:"panel-muted text-xs uppercase",children:"LLM Commentary"}),r.jsx("div",{className:"surface text-xs text-slate-300",children:"No status received yet."})]});const d=n.runtime||{},h=n.counters||{},p=n.timings||{},x=n.input||{},g=n.output||{},v=n.errors||{},y=Array.isArray(n.history)?n.history:[],S=y.find(w=>w.runId===o)||(y.length?y[y.length-1]:null),E=xN(n),C=gN(x.modelMessages,g.raw),M=[{label:"running",value:d.running?"yes":"no"},{label:"in flight",value:d.inFlight?"yes":"no"},{label:"phase",value:d.phase||"--"},{label:"run id",value:d.currentRunId??"--"},{label:"tick count",value:d.tickCount??0},{label:"last tick",value:d.lastTickAt?new Date(d.lastTickAt).toLocaleString():"never"},{label:"next run",value:d.nextRunAt?new Date(d.nextRunAt).toLocaleString():"n/a"},{label:"outcome",value:d.outcome||"--"},{label:"reason",value:d.reason||"--"},{label:"skip streak",value:h.skipStreak??0},{label:"clear count",value:h.clearCount??0},{label:"last gen",value:p.lastGenerationMs!=null?`${p.lastGenerationMs} ms`:"--"},{label:"avg gen",value:p.avgGenerationMs!=null?`${p.avgGenerationMs} ms`:"--"},{label:"gen count",value:p.generationCount??0},{label:"prompt chars",value:h.promptChars??0},{label:"active drivers",value:h.snapshotSummary?.activeDrivers??0},{label:"snapshot rovers",value:h.snapshotSummary?.rovers??0},{label:"snapshot chat",value:h.snapshotSummary?.chatMessages??0}];return r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("div",{className:"panel-muted text-xs uppercase",children:"LLM Commentary"}),r.jsx("div",{className:"flex gap-0.5 text-xs",children:r.jsx("button",{type:"button",onClick:s,disabled:!!l,className:"button-danger disabled:cursor-not-allowed disabled:opacity-60",children:l?"Clearing...":"Clear LLM History"})}),r.jsxs("div",{className:`surface border text-center ${E.className}`,children:[r.jsx("div",{className:"text-[1.1rem] font-bold tracking-wide",children:E.label}),r.jsx("div",{className:"text-xs text-slate-200",children:E.detail})]}),r.jsx("div",{className:"surface flex flex-wrap gap-0.5 text-xs",children:M.map(w=>r.jsxs("span",{className:"rounded border border-slate-600/60 bg-slate-800/70 px-0.5 py-0.25 text-[0.72rem] leading-tight text-slate-200",children:[w.label,": ",w.value]},w.label))}),v.message?r.jsxs("div",{className:"surface text-xs text-red-300 break-words",children:["Error: ",v.message]}):null,v.details?r.jsxs("details",{className:"surface text-xs text-red-200",children:[r.jsx("summary",{className:"cursor-pointer select-none text-red-300",children:"Failure details"}),r.jsx("pre",{className:"mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-red-200",children:JSON.stringify(v.details,null,2)})]}):null,g.generated?r.jsxs("div",{className:"surface text-xs text-slate-200 break-words",children:["Generated: ",g.generated]}):null,g.posted?r.jsxs("div",{className:"surface text-xs text-emerald-200 break-words",children:["Posted: ",g.posted]}):null,r.jsxs("div",{className:"grid gap-0.5 md:grid-cols-2",children:[r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("div",{className:"panel-muted text-xs uppercase",children:"Live Input Conversation"}),r.jsx("div",{className:"surface max-h-72 space-y-0.5 overflow-y-auto",children:C.length?C.map(w=>r.jsx(ld,{message:w.message},w.id)):r.jsx("div",{className:"text-xs text-slate-300",children:"No model conversation captured yet."})})]}),r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("div",{className:"panel-muted text-xs uppercase",children:"Output + Error"}),r.jsxs("div",{className:"surface space-y-0.5 text-xs text-slate-200",children:[r.jsxs("div",{children:["Raw output: ",g.raw?.trim()?g.raw:""]}),r.jsxs("div",{children:["Posted: ",g.posted||""]}),r.jsxs("div",{children:["Output at:"," ",g.modelOutputAt?new Date(g.modelOutputAt).toLocaleString():"n/a"]}),r.jsxs("div",{children:["Failed at: ",v.failedAt?new Date(v.failedAt).toLocaleString():"n/a"]})]})]})]}),r.jsxs("details",{className:"surface text-xs text-slate-200",children:[r.jsx("summary",{className:"cursor-pointer select-none text-slate-300",children:"Full Monitor Payload"}),r.jsx("pre",{className:"mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200",children:JSON.stringify(n,null,2)})]}),r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("div",{className:"panel-muted text-xs uppercase",children:"Recent Runs"}),r.jsx("div",{className:"surface max-h-52 space-y-0.5 overflow-y-auto text-xs",children:y.length?y.slice().reverse().map(w=>r.jsxs("button",{type:"button",onClick:()=>f(w.runId),className:`w-full text-left surface ${S?.runId===w.runId?"border border-sky-400/50":""}`,children:[r.jsxs("span",{className:"text-slate-300",children:["#",w.runId]})," ",r.jsx("span",{className:"text-slate-200",children:w.outcome||w.phase||"--"})," ",r.jsx("span",{className:"text-slate-400",children:w.durationMs!=null?`${w.durationMs}ms`:"--"})," ",r.jsx("span",{className:"text-slate-500",children:w.reason||""})]},w.runId)):r.jsx("div",{className:"text-slate-300",children:"No runs recorded yet."})})]}),S?r.jsxs("details",{className:"surface text-xs text-slate-200",open:!0,children:[r.jsxs("summary",{className:"cursor-pointer select-none text-slate-300",children:["Run #",S.runId," details"]}),r.jsx("pre",{className:"mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200",children:JSON.stringify(S,null,2)})]}):null]})}function xN(n){const s=n?.runtime||{},l=n?.output||{},o=n?.errors||{};return s.inFlight?{label:"IN FLIGHT",detail:s.reason||"Generating commentary now",className:"border-amber-400/60 bg-amber-700/20 text-amber-200"}:s.outcome==="posted"?{label:"POSTED",detail:l.posted?`Last: ${l.posted}`:"Commentary posted",className:"border-emerald-400/60 bg-emerald-700/20 text-emerald-200"}:s.outcome==="skipped"?{label:"SKIPPED",detail:s.reason||"Model chose to skip",className:"border-slate-400/60 bg-slate-700/30 text-slate-200"}:s.outcome==="failed"?{label:"FAILED",detail:o.message||s.reason||"Tick failed",className:"border-red-400/60 bg-red-700/20 text-red-200"}:{label:s.running?"IDLE":"STOPPED",detail:s.reason||"Waiting for next tick",className:"border-sky-400/50 bg-sky-700/20 text-sky-200"}}function gN(n,s){const l=Date.now(),f=(Array.isArray(n)?n:[]).map((d,h)=>{const p=String(d?.role||"").toLowerCase(),x=typeof d?.content=="string"?d.content:JSON.stringify(d?.content??null,null,2),g=p==="system"?"LLM System":p==="assistant"?"LLM Context":p==="user"?"LLM Input":"LLM Message";return{id:`llm-msg-${h}`,message:{ts:l+h,nickname:g,text:x,role:"spectator",system:p==="system"}}});if(s!=null){const d=String(s);f.push({id:"llm-output",message:{ts:l+f.length+1,nickname:"LLM Output",text:d.trim()?d:"",role:"spectator",system:!0}})}return f}function vN({health:n,roster:s=[]}){if(!n)return null;const l=n.replay||{sources:[],readyCount:0,totalCount:0},o=n.snapshots||{rovers:[],rooms:[]},f=x=>s.find(g=>String(g.id)===String(x))?.color||null,d=`${l.readyCount}/${l.totalCount} sources ready`,h=o.rovers.filter(x=>x.stale).length,p=o.rooms.filter(x=>x.stale).length;return r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("div",{className:"panel-muted text-xs uppercase",children:"Health"}),r.jsxs("div",{className:"surface space-y-0.5 text-xs text-slate-200",children:[r.jsxs("div",{className:"flex items-center justify-between",children:[r.jsx("span",{children:"Replay segments"}),r.jsx("span",{className:"text-slate-400",children:d})]}),r.jsxs("div",{className:"flex items-center justify-between",children:[r.jsx("span",{children:"Rover snapshots"}),r.jsxs("span",{className:h?"text-amber-300":"text-emerald-300",children:[o.rovers.length-h,"/",o.rovers.length," ok"]})]}),r.jsxs("div",{className:"flex items-center justify-between",children:[r.jsx("span",{children:"Room cameras"}),r.jsxs("span",{className:p?"text-amber-300":"text-emerald-300",children:[o.rooms.length-p,"/",o.rooms.length," ok"]})]})]}),r.jsx("div",{className:"space-y-0.5 text-xs text-slate-300",children:l.sources.map(x=>r.jsxs("div",{className:"flex items-center justify-between",children:[r.jsx("span",{className:`${x.type==="rover"?"rounded px-1 py-[1px] border border-transparent":""}`,style:x.type==="rover"?oa(f(x.id),.16):void 0,children:x.label}),r.jsxs("span",{className:x.ready?"text-emerald-300":"text-amber-300",children:[x.recentCount,"/",x.neededCount]})]},`${x.type}:${x.id}`))}),r.jsx("div",{className:"space-y-0.5 text-xs text-slate-300",children:o.rovers.map(x=>r.jsxs("div",{className:"flex items-center justify-between",children:[r.jsx("span",{className:"rounded px-1 py-[1px] border border-transparent",style:oa(f(x.id),.16),children:x.name}),r.jsx("span",{className:x.stale?"text-amber-300":"text-emerald-300",children:x.stale?"stale":"ok"})]},`rover:${x.id}`))}),r.jsx("div",{className:"space-y-0.5 text-xs text-slate-300",children:o.rooms.map(x=>r.jsxs("div",{className:"flex items-center justify-between",children:[r.jsx("span",{children:x.name}),r.jsx("span",{className:x.stale?"text-amber-300":"text-emerald-300",children:x.error?"error":x.stale?"stale":"ok"})]},`room:${x.id}`))})]})}function bN({entries:n}){const s=n||[];return r.jsxs("div",{className:"panel-section space-y-0.5 text-base",children:[r.jsxs("div",{className:"flex items-center justify-between text-sm text-slate-400",children:[r.jsx("span",{children:"Admin IP log"}),r.jsx("span",{children:s.length})]}),r.jsx("div",{className:"surface h-64 overflow-y-auto font-mono text-xs",children:s.length===0?r.jsx("p",{children:"No admin log entries yet."}):s.slice().reverse().map(l=>r.jsxs("div",{className:"surface",children:[r.jsx("span",{className:"text-amber-400",children:l.ts?new Date(l.ts).toLocaleTimeString():"--"})," ",l.label&&r.jsxs("span",{className:"text-teal-400",children:["[",l.label,"]"]})," ",r.jsx("span",{className:"text-slate-200",children:l.message})," ",l.ip&&r.jsx("span",{className:"text-cyan-300",children:l.ip})," ",l.meta&&r.jsx("span",{className:"text-slate-500",children:JSON.stringify(l.meta)})]},l.id))}),r.jsx("p",{className:"text-xs text-slate-500",children:"Admin-only log stream; IPs never appear in user data."})]})}const yN=[{id:"driveForward",label:"Drive Forward",group:"Driving"},{id:"driveBackward",label:"Drive Backward",group:"Driving"},{id:"driveLeft",label:"Turn Left",group:"Driving"},{id:"driveRight",label:"Turn Right",group:"Driving"},{id:"boostModifier",label:"Boost Modifier",group:"Driving"},{id:"slowModifier",label:"Slow Modifier",group:"Driving"},{id:"auxMainForward",label:"Main Brush Forward",group:"Aux Motors"},{id:"auxMainReverse",label:"Main Brush Reverse",group:"Aux Motors"},{id:"auxSideForward",label:"Side Brush Forward",group:"Aux Motors"},{id:"auxSideReverse",label:"Side Brush Reverse",group:"Aux Motors"},{id:"auxVacuumFast",label:"Vacuum Max",group:"Aux Motors"},{id:"auxVacuumSlow",label:"Vacuum Low",group:"Aux Motors"},{id:"auxAllForward",label:"All Aux Forward",group:"Aux Motors"},{id:"cameraUp",label:"Camera Up",group:"Camera"},{id:"cameraDown",label:"Camera Down",group:"Camera"},{id:"nightVisionToggle",label:"Toggle Night Vision",group:"Camera"},{id:"hornHonk",label:"Horn (Hold)",group:"Audio"},{id:"micPtt",label:"Mic Push To Talk",group:"Audio"},{id:"driveMacro",label:"Drive Macro",group:"Macros"},{id:"dockMacro",label:"Dock Macro",group:"Macros"},{id:"chatFocus",label:"Toggle Chat",group:"Chat"},{id:"songNoteUp",label:"Song Note Up",group:"Audio"},{id:"songNoteDown",label:"Song Note Down",group:"Audio"},{id:"homeAssistantOn",label:"Room Controls On (Cycle)",group:"Room Controls"},{id:"homeAssistantOff",label:"Room Controls Off (Cycle)",group:"Room Controls"}];function SN(n){return n.reduce((s,l)=>((s[l.group]||(s[l.group]=[])).push(l),s),{})}function jN(n,s){const l=Number(n);return Number.isFinite(l)?Math.max(0,Math.min(500,l)):s}const gd=5,Ai=500,Ni=1,vd=100;function AN(n,s){const l=Number(n);return Number.isFinite(l)?Math.max(gd,Math.min(Ai,l)):s}function fo(n,s){const l=Number(n);return Number.isFinite(l)?Math.max(Ni,Math.min(vd,l)):s}function NN(n){const l=(fo(n,n)-Ni)/(vd-Ni),o=Ai-l*(Ai-gd);return Math.round(o)}function wN(n){const s=AN(n,n),l=(Ai-s)/(Ai-gd),o=Ni+l*(vd-Ni);return Math.round(o)}function CN(n){const[s,l]=m.useState(null),o=m.useCallback(h=>{lf(!0),l(h)},[]),f=m.useCallback(()=>{lf(!1),l(null)},[]),d=m.useCallback((h,p)=>{if(p.preventDefault(),p.stopPropagation(),p.key==="Escape"){f();return}const x=Ri(p.key??"");x&&(n(h,x,p),f())},[f,n]);return m.useEffect(()=>()=>lf(!1),[]),{active:s,startCapture:o,cancel:f,captureFromEvent:d}}function eo({label:n,description:s,value:l,onChange:o,min:f=0,max:d=500,step:h=5}){return r.jsxs("label",{className:"surface-muted block p-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-300",children:[r.jsx("span",{className:"font-semibold text-slate-100",children:n}),r.jsx("input",{type:"number",min:f,max:d,value:l,onChange:p=>o(p.target.value),className:"w-16 rounded border border-slate-700 bg-slate-900 px-1 py-[2px] text-right text-[0.75rem] font-mono text-slate-100"})]}),s&&r.jsx("p",{className:"text-[0.65rem] text-slate-500",children:s}),r.jsxs("div",{className:"mt-0 flex items-center gap-0.5",children:[r.jsx("input",{type:"range",min:f,max:d,step:h,value:l,onChange:p=>o(p.target.value),className:"h-2 flex-1 accent-emerald-400"}),r.jsx("span",{className:"w-12 text-right text-[0.75rem] font-mono text-slate-400",children:l})]})]})}function EN(){const{state:{keymap:n},actions:{updateKeyBinding:s,resetKeyBindings:l}}=_t(),{value:o,save:f}=Ge("inputs",_n),d={..._n.keyboard,...o.keyboard??{}},h=m.useMemo(()=>typeof d.tiltSpeed=="number"?fo(d.tiltSpeed,_n.keyboard.tiltSpeed):typeof d.tiltIntervalMs=="number"?fo(wN(d.tiltIntervalMs),_n.keyboard.tiltSpeed):_n.keyboard.tiltSpeed,[d.tiltIntervalMs,d.tiltSpeed]),p=m.useMemo(()=>SN(yN),[]),{active:x,startCapture:g,cancel:v,captureFromEvent:y}=CN((M,w)=>{s(M,w)}),S=m.useCallback(M=>n?.[M]?.[0]||hi[M]?.[0]||"",[n]),E=m.useCallback((M,w)=>{const A=_n.keyboard,N=jN(w,A[M]);f(T=>({...T??{},keyboard:{...T?.keyboard??A,[M]:N}}))},[f]),C=m.useCallback(M=>{const w=_n.keyboard,A=fo(M,w.tiltSpeed),N=NN(A);f(T=>({...T??{},keyboard:{...T?.keyboard??w,tiltSpeed:A,tiltIntervalMs:N}}))},[f]);return r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsxs("div",{className:"flex items-center justify-between",children:[r.jsxs("div",{children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Keyboard layout"}),r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:"Per-browser · click to change a binding"})]}),r.jsx("button",{type:"button",onClick:()=>l(),className:"button-dark text-xs",children:"Reset defaults"})]}),r.jsxs("div",{className:"space-y-0.5 surface",children:[r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:"Keyboard speeds"}),r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(eo,{label:"Base speed",description:"Normal driving speed",value:d.baseSpeed,onChange:M=>E("baseSpeed",M)}),r.jsx(eo,{label:"Turbo speed",description:"Used when holding the boost modifier",value:d.turboSpeed,onChange:M=>E("turboSpeed",M)}),r.jsx(eo,{label:"Precision speed",description:"Used when holding the precision modifier",value:d.precisionSpeed,onChange:M=>E("precisionSpeed",M)})]})]}),r.jsxs("div",{className:"space-y-0.5 surface",children:[r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:"Camera tilt"}),r.jsx(eo,{label:"Tilt speed",description:"Higher = faster when holding tilt keys",value:h,onChange:C,min:1,max:100,step:1})]}),r.jsx("div",{className:"space-y-0.5",children:Object.entries(p).map(([M,w])=>r.jsxs("div",{className:"space-y-0.5 surface",children:[r.jsx("p",{className:"text-[0.75rem] text-slate-400",children:M}),r.jsx("div",{className:"space-y-0.5",children:w.map(A=>{const N=S(A.id),T=x===A.id;return r.jsxs("div",{className:"surface-muted flex items-center justify-between text-xs",children:[r.jsxs("div",{children:[r.jsx("p",{className:"font-semibold text-slate-100",children:A.label}),r.jsx("p",{className:"text-[0.65rem] text-slate-400",children:Cn(N)})]}),r.jsxs("div",{className:"flex items-center gap-0.5",children:[T&&r.jsx("button",{type:"button",onClick:()=>v(),className:"button-danger text-[0.7rem]",children:"Cancel"}),r.jsx("button",{type:"button",onClick:()=>g(A.id),onKeyDown:T?k=>y(A.id,k):void 0,autoFocus:T,className:`${T?"px-0.5 py-0.5 bg-emerald-500 text-emerald-950 hover:bg-emerald-400":"button-dark"} text-[0.7rem] font-medium transition-colors`,children:T?"Press a key…":"Change"})]})]},A.id)})})]},M))})]})}const sa=new Intl.NumberFormat(void 0,{maximumFractionDigits:2}),kN=[{id:"drive",label:"Drive stick",kind:"axisPair",section:"Driving",invertDefaults:{invertX:!1,invertY:!0}},{id:"cameraTilt",label:"Camera tilt",kind:"axis",section:"Camera",invertDefaults:{invert:!0}},{id:"mainBrush",label:"Main brush",kind:"axis",section:"Brushes",invertDefaults:{invert:!1}},{id:"sideBrush",label:"Side brush",kind:"axis",section:"Brushes",invertDefaults:{invert:!1}},{id:"vacuum",label:"Vacuum",kind:"button",section:"Aux buttons"},{id:"allAux",label:"All aux",kind:"button",section:"Aux buttons"},{id:"mainReverse",label:"Main reverse toggle",kind:"button",section:"Brush toggles"},{id:"sideReverse",label:"Side reverse toggle",kind:"button",section:"Brush toggles"},{id:"driveMacro",label:"Drive macro",kind:"button",section:"Mode macros"},{id:"dockMacro",label:"Dock macro",kind:"button",section:"Mode macros"},{id:"nightVisionToggle",label:"Night vision toggle",kind:"button",section:"Camera"}],dx=.45,MN=.6;function ui({label:n,description:s,min:l,max:o,step:f,value:d,onChange:h}){return r.jsxs("label",{className:"surface-muted block p-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-300",children:[r.jsx("span",{className:"font-semibold text-slate-100",children:n}),r.jsx("span",{className:"font-mono text-slate-400",children:sa.format(d)})]}),s&&r.jsx("p",{className:"text-[0.65rem] text-slate-500",children:s}),r.jsx("input",{type:"range",min:l,max:o,step:f,value:d,onChange:p=>h(Number(p.target.value)),className:"mt-0 w-full accent-emerald-400"})]})}function RN(n){if(!n)return"Unassigned";if(n.kind==="axisPair"){const s=`${n.invertX?"X inv ":""}${n.invertY?"Y inv":""}`.trim();return`Axes ${n.x}/${n.y}${s?` (${s})`:""}`}if(n.kind==="axis")return`Axis ${n.index}${n.invert?" (inv)":""}`;if(n.kind==="button")return`Button ${n.index}`;if(n.kind==="buttonAxis")return`Button ${n.index} (analog)`;if(n.kind==="axisButton"){const s=n.direction<0?"<":">";return`Axis ${n.index} ${s} ${sa.format(n.threshold??.6)}`}return"Unassigned"}function qN(n){return n.reduce((s,l)=>((s[l.section]||(s[l.section]=[])).push(l),s),{})}function TN(n,s){if(!n||n.length===0)return null;if(s){const l=n.find(o=>o.signature===s);if(l)return l}return n[0]}function DN(n){return{axes:Array.from(n.axes??[]),buttons:(n.buttons??[]).map(s=>({pressed:!!s?.pressed,value:typeof s?.value=="number"?s.value:s?.pressed?1:0}))}}function _N(n,s,l){const o=(n.axes??[]).map((d,h)=>({index:h,delta:Math.abs((d??0)-(s.axes?.[h]??0)),value:d}));if(o.sort((d,h)=>h.delta-d.delta),l.kind==="axisPair"){const d=o.filter(h=>h.delta>dx).slice(0,2);return d.length<2?null:{kind:"axisPair",x:d[0].index,y:d[1].index,...l.invertDefaults??{}}}const f=o.find(d=>d.delta>dx);return f?{kind:"axis",index:f.index,...l.invertDefaults??{}}:null}function BN(n,s,l){const o=n.buttons??[];for(let d=0;dMN)return l.kind==="axis"?{kind:"buttonAxis",index:d}:{kind:"button",index:d}}const f=n.axes??[];for(let d=0;d.7&&p>.5)return l.kind==="axis"?{kind:"axis",index:d,...l.invertDefaults??{}}:{kind:"axisButton",index:d,direction:h>=0?1:-1,threshold:.6}}return null}function ON(n,s,l){if(!n)return null;if(l.kind==="axis"||l.kind==="axisPair"){const o=_N(n,s,l);if(o)return o}return BN(n,s,l)}function LN(){const n=dA(),{value:s,save:l}=Ge("gamepad",_a),[o,f]=m.useState(null),d=m.useRef(null),h=m.useMemo(()=>qN(kN),[]),p=m.useMemo(()=>TN(n.pads,s.activeSignature),[n.pads,s.activeSignature]),x=p?.signature??null,g=m.useMemo(()=>x?s?.profiles?.[x]??s?.defaults?.profile??la:s?.defaults?.profile??la,[x,s?.defaults?.profile,s?.profiles]);m.useEffect(()=>{!p||!x||s?.profiles?.[x]||l(w=>{const A=w??_a;if(A.profiles?.[x])return A;const N=A?.defaults?.profile??la,T=wg(p,N);return{...A,profiles:{...A.profiles??{},[x]:T}}})},[p,x,s?.profiles,l]),m.useEffect(()=>{d.current=null},[o,x]),m.useEffect(()=>{if(!o||!p)return;if(!d.current){d.current=DN(p);return}const w=ON(p,d.current,o);w&&(l(A=>{const N=A??_a,T=N.profiles?.[x]??N?.defaults?.profile??la,k={...T,bindings:{...T.bindings??{},[o.id]:{...T.bindings?.[o.id]??{},kind:o.kind,sources:[w]}}};return{...N,profiles:{...N.profiles??{},[x]:k}}}),f(null))},[p,x,o,l]);const v=m.useCallback(w=>{l(A=>({...A??_a,activeSignature:w||null}))},[l]),y=m.useCallback(w=>{l(A=>{const N=A??_a,T=(x&&N.profiles?.[x])??N?.defaults?.profile??la,k={...T,calibration:{...T.calibration??{},...w}};return x?{...N,profiles:{...N.profiles??{},[x]:k}}:{...N,defaults:{...N.defaults??{},profile:k}}})},[x,l]),S=m.useCallback((w,A)=>{l(N=>{const T=N??_a,k=(x&&T.profiles?.[x])??T?.defaults?.profile??la,O=A(k.bindings?.[w]??{}),Q={...k,bindings:{...k.bindings??{},[w]:O}};return x?{...T,profiles:{...T.profiles??{},[x]:Q}}:{...T,defaults:{...T.defaults??{},profile:Q}}})},[x,l]),E=m.useCallback(w=>{S(w.id,A=>({...A,sources:[]}))},[S]),C=m.useCallback((w,A)=>{S(w.id,N=>{const T=Array.isArray(N.sources)?[...N.sources]:[];if(!T[0])return N;const k={...T[0]};return A==="invertX"?k.invertX=!k.invertX:A==="invertY"?k.invertY=!k.invertY:k.invert=!k.invert,T[0]=k,{...N,sources:T}})},[S]),M=m.useMemo(()=>p?{outputs:Cg(p,g)}:null,[p,g]);return r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsx("div",{className:"flex items-center justify-between",children:r.jsxs("div",{children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Controller"}),r.jsx("p",{className:"text-[0.65rem] text-slate-500",children:p?"Move sticks or press buttons to bind":"Connect a controller to configure."}),o&&r.jsxs("p",{className:"mt-0 text-[0.7rem] text-emerald-400",children:["Capturing ",o.label,"…"]})]})}),r.jsxs("div",{className:"space-y-0.5 surface",children:[r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:"Connected controller"}),n.pads.length===0?r.jsx("p",{className:"text-[0.7rem] text-slate-400",children:"No controller detected."}):r.jsxs("div",{className:"flex items-center justify-between gap-0.5 text-xs",children:[r.jsx("select",{value:x??"",onChange:w=>v(w.target.value),className:"flex-1 rounded border border-slate-700 bg-slate-900 px-1 py-[2px] text-[0.75rem] text-slate-100",children:n.pads.map(w=>r.jsx("option",{value:w.signature,children:w.id||"Unknown controller"},w.signature))}),r.jsx("span",{className:"text-[0.7rem] text-slate-400",children:p?.mapping??"unknown"})]})]}),r.jsxs("div",{className:"space-y-0.5 surface",children:[r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:"Calibration"}),r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(ui,{label:"Drive deadzone",description:"Ignore small drive stick drift",min:0,max:.6,step:.01,value:g.calibration?.driveDeadzone??.18,onChange:w=>y({driveDeadzone:w})}),r.jsx(ui,{label:"Camera deadzone",description:"Ignore small camera tilt drift",min:0,max:.4,step:.01,value:g.calibration?.cameraDeadzone??.08,onChange:w=>y({cameraDeadzone:w})}),r.jsx(ui,{label:"Aux deadzone",description:"Ignore small trigger noise",min:0,max:.4,step:.01,value:g.calibration?.auxDeadzone??.05,onChange:w=>y({auxDeadzone:w})}),r.jsx(ui,{label:"Side brush scale",description:"Scale side brush output",min:.3,max:1,step:.05,value:g.calibration?.auxSideScale??.55,onChange:w=>y({auxSideScale:w})}),r.jsxs("label",{className:"surface-muted block p-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-300",children:[r.jsx("span",{className:"font-semibold text-slate-100",children:"Camera mode"}),r.jsxs("select",{value:g.calibration?.cameraMode??"absolute",onChange:w=>y({cameraMode:w.target.value}),className:"rounded border border-slate-700 bg-slate-900 px-1 py-[2px] text-[0.75rem] text-slate-100",children:[r.jsx("option",{value:"absolute",children:"Absolute"}),r.jsx("option",{value:"velocity",children:"Velocity"})]})]}),r.jsx("p",{className:"text-[0.65rem] text-slate-500",children:"Absolute maps stick to angle; velocity moves over time."})]}),r.jsx(ui,{label:"Camera sensitivity",description:"Velocity mode degrees per second",min:10,max:180,step:5,value:g.calibration?.cameraSensitivity??60,onChange:w=>y({cameraSensitivity:w})})]})]}),r.jsx("div",{className:"space-y-0.5",children:Object.entries(h).map(([w,A])=>r.jsxs("div",{className:"space-y-0.5 surface",children:[r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:w}),r.jsx("div",{className:"space-y-0.5",children:A.map(N=>{const k=g.bindings?.[N.id]?.sources?.[0]??null;return r.jsxs("div",{className:"surface-muted flex items-center justify-between text-xs",children:[r.jsxs("div",{children:[r.jsx("p",{className:"font-semibold text-slate-100",children:N.label}),r.jsx("p",{className:"text-[0.65rem] text-slate-400",children:RN(k)})]}),r.jsxs("div",{className:"flex items-center gap-0.5",children:[N.kind==="axisPair"&&r.jsxs(r.Fragment,{children:[r.jsx("button",{type:"button",disabled:!k,onClick:()=>C(N,"invertX"),className:"button-dark text-[0.7rem] font-medium disabled:opacity-50",children:"Invert X"}),r.jsx("button",{type:"button",disabled:!k,onClick:()=>C(N,"invertY"),className:"button-dark text-[0.7rem] font-medium disabled:opacity-50",children:"Invert Y"})]}),N.kind==="axis"&&r.jsx("button",{type:"button",disabled:!k,onClick:()=>C(N),className:"button-dark text-[0.7rem] font-medium disabled:opacity-50",children:"Invert"}),r.jsx("button",{type:"button",onClick:()=>E(N),className:"button-dark text-[0.7rem]",children:"Clear"}),r.jsx("button",{type:"button",onClick:()=>f(N),className:`${o?.id===N.id?"px-0.5 py-0.5 bg-emerald-500 text-emerald-950 hover:bg-emerald-400":"button-dark"} text-[0.7rem] font-medium`,children:o?.id===N.id?"Waiting…":"Capture"})]})]},N.id)})})]},w))}),r.jsxs("div",{className:"space-y-0.5 surface",children:[r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:"Diagnostics"}),p?r.jsxs("div",{className:"space-y-0.5 text-[0.7rem] text-slate-300",children:[r.jsx("p",{className:"text-slate-400",children:"Raw axes"}),r.jsx("div",{className:"grid grid-cols-2 gap-0.5",children:p.axes.map((w,A)=>r.jsxs("span",{className:"font-mono text-slate-300",children:["A",A,": ",sa.format(w)]},`axis-${A}`))}),r.jsx("p",{className:"text-slate-400",children:"Raw buttons"}),r.jsx("div",{className:"grid grid-cols-2 gap-0.5",children:p.buttons.map((w,A)=>r.jsxs("span",{className:"font-mono text-slate-300",children:["B",A,": ",sa.format(w.value)," ",w.pressed?"●":""]},`btn-${A}`))}),M?.outputs&&r.jsxs(r.Fragment,{children:[r.jsx("p",{className:"text-slate-400",children:"Mapped outputs"}),r.jsxs("div",{className:"grid grid-cols-2 gap-0.5",children:[r.jsxs("span",{className:"font-mono text-slate-300",children:["Drive: ",sa.format(M.outputs.driveVector.x),","," ",sa.format(M.outputs.driveVector.y)]}),r.jsxs("span",{className:"font-mono text-slate-300",children:["Camera: ",sa.format(M.outputs.cameraAxis)]}),r.jsxs("span",{className:"font-mono text-slate-300",children:["Main: ",sa.format(M.outputs.auxAxis.main)]}),r.jsxs("span",{className:"font-mono text-slate-300",children:["Side: ",sa.format(M.outputs.auxAxis.side)]})]})]})]}):r.jsx("p",{className:"text-[0.7rem] text-slate-400",children:"No controller detected."})]})]})}const zN={drive:"Drive wheels",aux:"Aux motors"};function mx(n){return Number.isFinite(n)?`${Math.round(n*100)}%`:"--"}function UN({value:n,color:s="bg-emerald-500"}){const l=`${Math.round(Math.max(0,Math.min(1,n))*100)}%`;return r.jsx("div",{className:"h-2 w-full overflow-hidden rounded bg-slate-800",children:r.jsx("div",{className:`h-full ${s}`,style:{width:l}})})}function HN(){const{state:{roverId:n},overcurrentLimiter:s}=_t(),l=m.useMemo(()=>is.map(o=>o.key),[]);return r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-400",children:[r.jsx("span",{children:"Overcurrent limiter"}),r.jsx("span",{children:s?.adminImmune?"Admin immune":"Active"})]}),n?r.jsxs("div",{className:"space-y-0.5",children:[l.map(o=>{const f=s?.caps?.[o]?.cap??0,d=s?.overcurrent?.groups?.[o]??!1,h=s?.scales?.perGroup?.[o]??1;return r.jsxs("div",{className:"surface space-y-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs",children:[r.jsx("span",{className:"text-slate-200",children:zN[o]||o}),r.jsx("span",{className:d?"text-red-300":"text-slate-400",children:d?"overcurrent":"ok"})]}),r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-[0.7rem] text-slate-400",children:[r.jsx("span",{children:"Cap"}),r.jsx("span",{children:mx(f)})]}),r.jsx(UN,{value:f,color:"bg-amber-500"}),r.jsxs("div",{className:"flex items-center justify-between text-[0.7rem] text-slate-400",children:[r.jsx("span",{children:"Scale"}),r.jsx("span",{children:mx(h)})]})]})]},o)}),r.jsxs("div",{className:"surface text-[0.7rem] text-slate-400",children:[r.jsx("div",{children:`Down rate ${s?.config?.downRatePerSec}/s · Up rate ${s?.config?.upRatePerSec}/s`}),r.jsx("div",{children:`Release delay ${s?.config?.releaseDelaySec}s`}),r.jsx("div",{children:`Output rate ${s?.config?.outputRateMs}ms`})]})]}):r.jsx("p",{className:"text-xs text-slate-500",children:"Assign a rover to view limiter status."})]})}const $g=m.createContext(null);function Gg(){const n=m.useContext($g);if(!n)throw new Error("Tabs components must be used within a provider.");return n}const hx={primary:{base:"flex-1 px-0.5 py-0.5 text-sm font-medium focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-slate-500 rounded-md border border-slate-800",active:"bg-sky-600 text-white border-white",inactive:"bg-zinc-900 text-slate-300 hover:bg-sky-500 hover:text-white"}},px={none:"",pink:"bg-pink-500 text-white border-pink-200 hover:bg-pink-400",green:"bg-emerald-500 text-white border-emerald-200 hover:bg-emerald-400"},Yg="primary";function bd(...n){return n.filter(Boolean).join(" ")}function yd({children:n,defaultTab:s,currentTab:l,onTabChange:o,variant:f=Yg}){const[d,h]=m.useState(s??null),[p,x]=m.useState([]),g=m.useCallback(E=>E?(x(C=>C.includes(E)?C:[...C,E]),()=>x(C=>C.includes(E)?C.filter(M=>M!==E):C)):()=>{},[]),v=l??d??p[0]??null;m.useEffect(()=>{s&&l===void 0&&h(s)},[s,l]);const y=m.useCallback(E=>{E&&(l===void 0&&h(E),o&&o(E))},[l,o]),S=m.useMemo(()=>({activeTab:v,setActiveTab:y,registerTab:g,variant:f}),[v,y,g,f]);return r.jsx($g.Provider,{value:S,children:n})}function Sd({children:n,className:s=""}){return r.jsx("div",{className:bd("flex gap-0.5",s),children:n})}function Wt({id:n,children:s,className:l="",disabled:o=!1,highlight:f="none"}){const{activeTab:d,setActiveTab:h,registerTab:p,variant:x}=Gg();m.useEffect(()=>p(n),[n,p]);const g=hx[x]??hx[Yg],v=d===n,y=px[f]||px.none,S=bd(g.base,y||(v?g.active:g.inactive),o?"cursor-not-allowed opacity-50":"",l);return r.jsx("button",{type:"button",className:S,onClick:()=>!o&&h(n),"aria-pressed":v,"aria-disabled":o,children:s})}function jd({children:n,className:s=""}){return r.jsx("div",{className:bd("mt-0 space-y-0.5",s),children:n})}function en({id:n,children:s,keepMounted:l=!1}){const{activeTab:o}=Gg(),f=o===n;return!f&&!l?null:!f&&l?r.jsx("div",{hidden:!0,"aria-hidden":"true",children:s}):r.jsx(r.Fragment,{children:s})}function VN(){const{session:n}=nn(),s=m.useMemo(()=>JSON.stringify(n??{},null,2),[n]);return r.jsxs("div",{className:"panel-section flex min-h-0 flex-col gap-0.5 text-xs",children:[r.jsx("p",{className:"text-sm text-slate-400",children:"Session snapshot"}),r.jsx("pre",{className:"surface min-h-0 flex-1 overflow-y-auto font-mono text-[0.7rem] text-lime-300",children:s})]})}const xx=300,gx=240;function $N(n,s){if(n===void 0)return"undefined";if(n===null)return"null";if(typeof n=="string")return n;if(typeof n=="number"||typeof n=="boolean")return String(n);if(typeof n=="function")return"[Function]";if(n instanceof Error)return`${n.name}: ${n.message}`;if(n instanceof Date)return n.toISOString();if(s||(s=new WeakSet),typeof n=="object"){if(s.has(n))return"[Circular]";s.add(n)}try{return JSON.stringify(n,(l,o)=>typeof o=="function"?"[Function]":o instanceof Error?`${o.name}: ${o.message}`:o,0)}catch{return String(n)}}function GN(n){const l=n.map(o=>$N(o)).join(" ");return l.length>gx?`${l.slice(0,gx)}…`:l}function YN(){const n=tn(),[s,l]=m.useState([]),o=m.useMemo(()=>s.reduce((f,d)=>(d.direction==="in"&&(f.in+=1),d.direction==="out"&&(f.out+=1),f),{in:0,out:0}),[s]);return m.useEffect(()=>{const f=(x,g,v)=>{l(y=>{const S=[...y,{id:`${Date.now()}-${Math.random().toString(16).slice(2)}`,direction:x,event:g,payload:GN(v),timestamp:new Date().toLocaleTimeString()}];return S.length>xx?S.slice(-xx):S})},d=(x,...g)=>f("in",x,g),h=(x,...g)=>f("out",x,g);n.onAny(d);let p=null;if(typeof n.onAnyOutgoing=="function")n.onAnyOutgoing(h),p=()=>n.offAnyOutgoing(h);else{const x=n.emit.bind(n);n.emit=(g,...v)=>(h(g,...v),x(g,...v)),p=()=>{n.emit=x}}return()=>{n.offAny(d),p&&p()}},[n]),r.jsxs("section",{className:"panel-section space-y-0.5 text-base",children:[r.jsxs("div",{className:"flex items-center justify-between text-sm text-slate-400",children:[r.jsx("span",{children:"Socket activity"}),r.jsxs("div",{className:"flex items-center gap-0.5 text-xs text-slate-400",children:[r.jsxs("span",{className:"surface-muted",children:["in ",o.in]}),r.jsxs("span",{className:"surface-muted",children:["out ",o.out]}),r.jsx("button",{type:"button",className:"button-dark text-xs",onClick:()=>l([]),children:"Clear"})]})]}),r.jsx("div",{className:"surface h-64 overflow-y-auto font-mono text-xs",children:s.length===0?r.jsx("p",{children:"No socket activity yet."}):s.slice().reverse().map(f=>r.jsxs("div",{className:"surface",children:[r.jsx("span",{className:"text-slate-400",children:f.timestamp})," ",r.jsxs("span",{className:f.direction==="in"?"text-emerald-400":"text-amber-400",children:["[",f.direction.toUpperCase(),"]"]})," ",r.jsx("span",{className:"text-cyan-300",children:f.event})," ",r.jsx("span",{className:"text-slate-200",children:f.payload})]},f.id))}),r.jsx("p",{className:"text-xs text-slate-500",children:"Logs are collected only while this panel is open."})]})}const FN=[{key:"start",label:"Start OI"},{key:"safe",label:"Safe"},{key:"full",label:"Full"},{key:"passive",label:"Passive"},{key:"dock",label:"Dock"}];function Fg(){const{state:{roverId:n},actions:{sendOiCommand:s,setSensorStream:l}}=_t(),o=!!n,[f,d]=Lg(),h=tn(),{value:p,save:x}=Ge("page",{hudMapDesktop:!1,connectionTransport:"websocket",swapMobileControlColumns:!1,driveMacroBackoffEnabled:!0}),{value:g,save:v}=Ge("audio",st),y=p?.connectionTransport||"websocket",S=!!p?.swapMobileControlColumns,E=typeof p?.driveMacroBackoffEnabled=="boolean"?p.driveMacroBackoffEnabled:!0,C=Number.isFinite(g?.masterVolume)?g.masterVolume:st.masterVolume,M=Number.isFinite(g?.alertVolume)?g.alertVolume:st.alertVolume,w=Number.isFinite(g?.roverVolume)?g.roverVolume:st.roverVolume,A=typeof g?.mainBrushDuckEnabled=="boolean"?g.mainBrushDuckEnabled:typeof g?.autoLevelEnabled=="boolean"?g.autoLevelEnabled:st.mainBrushDuckEnabled,N=Number.isFinite(g?.mainBrushDuckAmount)?Math.max(0,Math.min(1,g.mainBrushDuckAmount)):st.mainBrushDuckAmount,T=m.useMemo(()=>[{key:"start",label:"Enable stream",enable:!0},{key:"stop",label:"Disable stream",enable:!1}],[]),k=F=>{n&&l(F)},O=F=>{const V=F.target.value;x(ee=>({...ee??{},connectionTransport:V})),h?.io?.opts&&(h.io.opts.transports=V==="polling"?["polling"]:["websocket","polling"],h.disconnect(),h.connect())},Q=F=>V=>{const ee=Number(V.target.value),oe=Number.isFinite(ee)?Math.max(0,Math.min(1,ee)):0;v(ue=>({...ue??{},[F]:oe}))},D=F=>{const V=!!F.target.checked;v(ee=>({...ee??{},mainBrushDuckEnabled:V}))},U=F=>{const V=!!F.target.checked;x(ee=>({...ee??{},swapMobileControlColumns:V}))},K=F=>{const V=!!F.target.checked;x(ee=>({...ee??{},driveMacroBackoffEnabled:V}))};return r.jsxs(yd,{defaultTab:"keybindings",children:[r.jsxs(Sd,{children:[r.jsx(Wt,{id:"keybindings",children:"Keybindings"}),r.jsx(Wt,{id:"controller",children:"Controller"}),r.jsx(Wt,{id:"page",children:"Page settings"}),r.jsx(Wt,{id:"admin",children:"Admin"})]}),r.jsxs(jd,{children:[r.jsx(en,{id:"keybindings",children:r.jsx("div",{className:"space-y-0.5",children:r.jsx(EN,{})})}),r.jsx(en,{id:"controller",children:r.jsx("div",{className:"space-y-0.5",children:r.jsx(LN,{})})}),r.jsx(en,{id:"page",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsx("p",{className:"text-slate-400",children:"HUD"}),r.jsxs("label",{className:"flex items-center gap-0.5 text-slate-200",children:[r.jsx("input",{type:"checkbox",className:"accent-emerald-500",checked:f,onChange:F=>d(F.target.checked)}),r.jsx("span",{children:"Show top-down map in HUD (desktop)"})]}),r.jsx("p",{className:"text-xs text-slate-500",children:"Mobile HUD keeps the map on by default."})]}),r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsx("p",{className:"text-slate-400",children:"Mobile controls"}),r.jsxs("label",{className:"flex items-center gap-0.5 text-slate-200",children:[r.jsx("input",{type:"checkbox",className:"accent-emerald-500",checked:S,onChange:U}),r.jsx("span",{children:"Swap control columns (put joystick on the left)"})]})]}),r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsx("p",{className:"text-slate-400",children:"Macros"}),r.jsxs("label",{className:"flex items-center gap-0.5 text-slate-200",children:[r.jsx("input",{type:"checkbox",className:"accent-emerald-500",checked:E,onChange:K}),r.jsx("span",{children:"Enable backward bump in drive macro"})]})]}),r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsx("p",{className:"text-slate-400",children:"Audio"}),r.jsxs("label",{className:"grid gap-0.5 text-slate-200",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsx("span",{children:"Master volume"}),r.jsxs("span",{className:"text-xs text-slate-400",children:[Math.round(C*100),"%"]})]}),r.jsx("input",{type:"range",min:"0",max:"1",step:"0.01",value:C,onChange:Q("masterVolume"),className:"w-full accent-emerald-500"})]}),r.jsxs("label",{className:"grid gap-0.5 text-slate-200",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsx("span",{children:"Alert/page sounds"}),r.jsxs("span",{className:"text-xs text-slate-400",children:[Math.round(M*100),"%"]})]}),r.jsx("input",{type:"range",min:"0",max:"1",step:"0.01",value:M,onChange:Q("alertVolume"),className:"w-full accent-emerald-500"})]}),r.jsxs("label",{className:"grid gap-0.5 text-slate-200",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsx("span",{children:"Rover audio"}),r.jsxs("span",{className:"text-xs text-slate-400",children:[Math.round(w*100),"%"]})]}),r.jsx("input",{type:"range",min:"0",max:"1",step:"0.01",value:w,onChange:Q("roverVolume"),className:"w-full accent-emerald-500"})]}),r.jsxs("label",{className:"flex items-center justify-between gap-0.5 text-slate-200",children:[r.jsx("span",{children:"Main brush ducking"}),r.jsx("input",{type:"checkbox",className:"accent-emerald-500",checked:A,onChange:D})]}),r.jsxs("label",{className:"grid gap-0.5 text-slate-200",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsx("span",{children:"Main brush duck amount"}),r.jsxs("span",{className:"text-xs text-slate-400",children:[Math.round(N*100),"%"]})]}),r.jsx("input",{type:"range",min:"0",max:"1",step:"0.01",value:N,onChange:Q("mainBrushDuckAmount"),className:"w-full accent-emerald-500",disabled:!A})]}),r.jsx("p",{className:"text-xs text-slate-500",children:"Lowers rover audio only while the main brush is running."})]}),r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsx("p",{className:"text-slate-400",children:"Connection"}),r.jsxs("label",{className:"flex items-center justify-between gap-0.5 text-slate-200",children:[r.jsx("span",{children:"Transport"}),r.jsxs("select",{value:y,onChange:O,className:"field-input text-sm",children:[r.jsx("option",{value:"websocket",children:"WebSocket"}),r.jsx("option",{value:"polling",children:"Polling"})]})]}),r.jsx("p",{className:"text-xs text-slate-500",children:"Switching reconnects your session."})]})]})}),r.jsx(en,{id:"admin",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsx("p",{className:"text-slate-400",children:"Manual OI commands"}),r.jsx("div",{className:"surface flex flex-wrap gap-0.5",children:FN.map(F=>r.jsx("button",{type:"button",onClick:()=>s(F.key),disabled:!o,className:"button-dark text-xs disabled:opacity-30",children:F.label},F.key))})]}),r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsx("p",{className:"text-slate-400",children:"Sensor stream"}),r.jsx("div",{className:"surface flex gap-0.5",children:T.map(F=>r.jsx("button",{type:"button",onClick:()=>k(F.enable),disabled:!o,className:"flex-1 button-dark text-xs disabled:opacity-30",children:F.label},F.key))}),!o&&r.jsx("p",{className:"text-xs text-slate-500",children:"Assign a rover to toggle streams."})]}),r.jsx(Vg,{}),r.jsx(HN,{}),r.jsx(hN,{}),r.jsx(VN,{}),r.jsx(YN,{})]})})]})]})}const vx={desktop:{main:[{type:"list",title:"Chat and nicknames",items:["Set a nickname in the user list panel, on the bottom left of the page below the rover video.",{segments:["Toggle chat focus with ",{action:"chatFocus"},". Press ",{action:"chatFocus"}," again to send."]}]},{type:"list",title:"Driving the rover",items:[{segments:['Press the "Start Driving" button onscreen, or press ',{action:"driveMacro"}," on your keyboard to put the rover into driving mode."]},"Refer to the controls for the controls for the rover."]},{type:"list",title:"Docking the rover",items:["Line up the rover to the dock, about a foot away, then:",{segments:['Press the "Dock and Charge" button onscreen, or press ',{action:"dockMacro"}," on your keyboard to start docking."]},"Wait for the rover to confirm it is docked and charging before leaving it unattended."]}],aside:[{type:"keyboard",title:"Keyboard controls",footnote:"Per-browser; adjust bindings and speeds in Settings → Keybindings.",groups:[{id:"movement",title:"Movement",items:[{action:"driveForward",label:"Forward"},{action:"driveBackward",label:"Backward"},{action:"driveLeft",label:"Turn left"},{action:"driveRight",label:"Turn right"},{action:"boostModifier",label:"Boost speed"},{action:"slowModifier",label:"Precision speed"}]},{id:"macros",title:"Rover modes & chat",items:[{action:"driveMacro",label:"Drive macro"},{action:"dockMacro",label:"Dock macro"},{action:"chatFocus",label:"Chat focus"}]},{id:"camera",title:"Camera",items:[{action:"cameraUp",label:"Tilt up"},{action:"cameraDown",label:"Tilt down"},{action:"nightVisionToggle",label:"Toggle night vision"}]},{id:"motors",title:"Rover Aux Motors",items:[{action:"auxMainForward",label:"Main brush forward"},{action:"auxMainReverse",label:"Main brush reverse"},{action:"auxSideForward",label:"Side brush forward"},{action:"auxSideReverse",label:"Side brush reverse"},{action:"auxVacuumFast",label:"Vacuum max"},{action:"auxVacuumSlow",label:"Vacuum low"},{action:"auxAllForward",label:"All motors forward"}]},{id:"audio",title:"Audio / Song",items:[{action:"songNoteUp",label:"Song note up"},{action:"songNoteDown",label:"Song note down"}]},{id:"room-controls",title:"Room Controls",items:[{action:"homeAssistantOn",label:"Next room control on"},{action:"homeAssistantOff",label:"Next room control off (reverse)"}]}]},{type:"gamepad",title:"Gamepad / joystick",items:["Gamepad controls are fully configurable to handle inconsistent browser mappings.","Use Settings → Controller to bind inputs, calibrate deadzones, and view diagnostics.","If inputs behave oddly, the diagnostics panel will show raw axis/button values."]}]},"mobile-portrait":{main:[{type:"list",title:"Chat and nicknames",items:["Set a nickname in the user list panel below.","Tap in the chat box to send messages in the chat."]},{type:"list",title:"Driving the rover",items:[{segments:['Press the "Start Driving" button onscreen to put the rover into driving mode.']},"Look below the rover video. Use the joystick column to move the rover, and hold the aux buttons in the other control column."]},{type:"list",title:"Docking the rover",items:["Line up the rover to the dock, about a foot away, then:",{segments:['Press the "Dock and Charge" button onscreen to start docking.']},"Wait for the rover to confirm it is docked and charging before leaving it unattended."]}]},"mobile-landscape":{main:[{type:"list",title:"Chat and nicknames",items:["Scroll down to see more of the page.","Set a nickname in the user list panel below.","Tap in the chat box to send messages in the chat."]},{type:"list",title:"Driving the rover",items:[{segments:['Press the "Start Driving" button, or the "Drive" button to put the rover into driving mode.']},"Use the joystick column beside the video feed to move the rover, and hold the aux buttons in the other control column."]},{type:"list",title:"Docking the rover",items:["Line up the rover to the dock, about a foot away, then:",{segments:['Press the "Dock and Charge" or the "Dock" button onscreen to start docking.']},"Wait for the rover to confirm it is docked and charging before leaving it unattended."]}]}};function XN(n){return vx[n]||vx.desktop}function Xg({actionId:n,keymap:s}){const l=s?.[n]?.[0]??"";return r.jsx("span",{className:"rounded border border-slate-600 bg-slate-900/40 px-1 text-[0.7rem] text-slate-200",children:Cn(l)})}function bx(n,s){return n.map((l,o)=>typeof l=="string"?r.jsx("span",{children:l},`text-${o}`):l?.action?r.jsx(Xg,{actionId:l.action,keymap:s},`pill-${l.action}-${o}`):l?.text?r.jsx("span",{children:l.text},`span-${o}`):null)}function Ro(n,s,l){if(typeof n=="string")return n;if(Array.isArray(n?.segments))return bx(n.segments,s);if(n&&typeof n=="object"){if(typeof n.text=="string")return n.text;if(Array.isArray(n))return bx(n,s)}return String(n??"")}function QN({hero:n,keymap:s}){return n?r.jsxs("div",{className:"surface space-y-0.5 px-0.5 py-0.5",children:[r.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-0.5",children:[r.jsxs("div",{children:[r.jsx("p",{className:"text-sm font-semibold text-white",children:n.title}),n.subtitle&&r.jsx("p",{className:"text-xs text-slate-300",children:n.subtitle})]}),n.chips&&r.jsx("div",{className:"flex flex-wrap gap-0.5 text-[0.7rem] text-slate-200",children:n.chips.map(l=>r.jsx("span",{className:"rounded border border-slate-700 px-1 py-[2px]",children:l},l))})]}),n.bullets&&r.jsx("ul",{className:"space-y-0.5 text-[0.85rem] text-slate-200",children:n.bullets.map((l,o)=>{const f=Array.isArray(l?.segments)?`hero-${o}`:`hero-${o}`;return r.jsx("li",{className:"surface-muted px-0.5 py-0.25",children:Ro(l,s)},f)})})]}):null}function KN({block:n,keymap:s}){return r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs font-semibold text-slate-200",children:n.title}),r.jsx("ul",{className:"space-y-0.5 text-[0.8rem] text-slate-300",children:n.items.map((l,o)=>{const f=`item-${o}`;return r.jsx("li",{className:"surface-muted flex flex-wrap items-center gap-0.5 px-0.5 py-0.25",children:Ro(l,s)},f)})})]})}function ZN({block:n}){const s=n.tone==="info"?"border-cyan-500/40":"border-slate-700";return r.jsxs("div",{className:`surface space-y-0.5 border ${s} px-0.5 py-0.5`,children:[r.jsx("p",{className:"text-xs font-semibold text-slate-100",children:n.title}),n.body&&r.jsx("ul",{className:"space-y-0.5 text-[0.8rem] text-slate-300",children:n.body.map((l,o)=>r.jsx("li",{className:"surface-muted px-0.5 py-0.25",children:Ro(l,{})},`callout-${o}`))})]})}function IN({group:n,keymap:s}){return r.jsxs("div",{className:"space-y-0.5 surface",children:[r.jsx("p",{className:"px-0.5 py-0.25 text-[0.75rem] font-semibold text-slate-200",children:n.title}),r.jsx("div",{className:"space-y-0.5 px-0.5 pb-0.25",children:n.items.map(l=>r.jsxs("div",{className:"surface-muted flex items-center justify-between gap-0.5 px-0.5 py-0.25 text-[0.8rem]",children:[r.jsx("span",{className:"text-slate-200",children:l.label}),r.jsx(Xg,{actionId:l.action,keymap:s})]},l.action))})]})}function JN({block:n,keymap:s}){return n?r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-200",children:[r.jsx("span",{className:"font-semibold",children:n.title}),n.footnote&&r.jsx("span",{className:"text-[0.7rem] text-slate-400",children:n.footnote})]}),r.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-0.5",children:n.groups?.map(l=>r.jsx(IN,{group:l,keymap:s},l.id))})]}):null}function PN({block:n}){return n?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs font-semibold text-slate-200",children:n.title}),r.jsx("ul",{className:"space-y-0.5 text-[0.8rem] text-slate-300",children:n.items?.map((s,l)=>r.jsx("li",{className:"surface-muted px-0.5 py-0.25",children:Ro(s,{})},`gamepad-${l}`))})]}):null}function yx({block:n,keymap:s}){if(!n)return null;switch(n.type){case"list":return r.jsx(KN,{block:n,keymap:s});case"callout":return r.jsx(ZN,{block:n});case"keyboard":return r.jsx(JN,{block:n,keymap:s});case"gamepad":return r.jsx(PN,{block:n});default:return null}}function Qg({layout:n,keymap:s}){const l=m.useMemo(()=>XN(n),[n]),o=s||{},f=l.main||[],d=l.aside||[];return r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(QN,{hero:l.hero,keymap:o}),r.jsxs("div",{className:"grid gap-0.5",style:{gridTemplateColumns:"repeat(auto-fit, minmax(360px, 1fr))"},children:[r.jsx("div",{className:"space-y-0.5",children:f.map((h,p)=>r.jsx(yx,{block:h,keymap:o},h.title||p))}),r.jsx("div",{className:"space-y-0.5",children:d.map((h,p)=>r.jsx(yx,{block:h,keymap:o},h.title||p))})]})]})}function Kg({layout:n,onOpenOverlay:s}){const{state:l}=_t();return r.jsxs("section",{className:"panel-section space-y-0.5 text-sm",children:[r.jsxs("div",{className:"flex items-center justify-between text-xs text-slate-400",children:[r.jsx("span",{children:"Help"}),r.jsx("button",{type:"button",onClick:s,className:"button-dark px-1 py-0.25 text-[0.75rem]",children:"Open full help"})]}),r.jsx(Qg,{layout:n,keymap:l?.keymap||{}})]})}const WN=["kal","rms","slt","ksp","bdl"],ew=Array.from({length:10},(n,s)=>s*10);function qo({hideInput:n=!1,hideSpectatorNotice:s=!1,fillHeight:l=!1,allowSpectatorInput:o=!1}){const f=it(q=>q.session),{messages:d,typing:h,sendMessage:p,registerInputRef:x,onInputFocus:g,onInputBlur:v,blurChat:y,setTypingActive:S}=od(),{value:E,save:C}=Ge("tts",{engine:"flite",voice:"rms",pitch:50}),[M,w]=m.useState(""),[A,N]=m.useState(!1),[T,k]=m.useState(!1),[O,Q]=m.useState(()=>E?.engine||"flite"),[D,U]=m.useState(()=>E?.voice||"rms"),[K,F]=m.useState(()=>Number.isFinite(E?.pitch)?E.pitch:50),V=f?.role!=="spectator"||o,ee=m.useRef(null),oe=f?.assignment?.roverId||null,H=!!m.useMemo(()=>f?.roster?.find(q=>String(q.id)===String(oe))||null,[oe,f?.roster])?.audio?.ttsEnabled,R=m.useMemo(()=>d.slice(-200),[d]),L=m.useMemo(()=>h||[],[h]);m.useEffect(()=>{ee.current&&(ee.current.scrollTop=ee.current.scrollHeight)},[R,L]),m.useEffect(()=>{const q=E?.engine||"flite",$=E?.voice||"rms",ne=Number.isFinite(E?.pitch)?E.pitch:50;O!==q&&Q(q),D!==$&&U($),K!==ne&&F(ne)},[O,K,E?.engine,E?.pitch,E?.voice,D]),m.useEffect(()=>{k(!!H)},[H]);const Z=m.useMemo(()=>!H||!T?null:O==="espeak"?{speak:!0,engine:O,pitch:K}:{speak:!0,engine:O,voice:D},[O,K,T,H,D]);async function ae(q){if(q.preventDefault(),!V||n)return;const ne=M.replace(/\\n/g,` -`).trim();if(ne){N(!0);try{await p(ne,Z),w(""),y(),S(!1)}catch(se){alert(se.message)}finally{N(!1)}}}const ce=l?"flex-1 min-h-0 overflow-y-auto":"h-48 overflow-y-auto";return r.jsxs("section",{className:`panel-section space-y-0.5 text-base ${l?"flex h-full flex-col overflow-hidden":""}`,children:[r.jsxs("div",{className:`surface overflow-y-auto space-y-0.5 px-0 ${ce}`,ref:ee,children:[R.length===0&&L.length===0?r.jsx("p",{className:"text-sm text-slate-500",children:"No messages yet."}):R.map(q=>r.jsx(ld,{message:q},q.id)),L.map(q=>r.jsx(pg,{message:q},`typing-${q.typingId||q.id}`))]}),!n&&r.jsxs("form",{className:"flex flex-wrap items-stretch gap-0.5",onSubmit:ae,children:[r.jsx("input",{className:"field-input flex-1 min-w-[10rem]",value:M,onChange:q=>{const $=q.target.value;w($),S(!!$.trim())},onFocus:q=>{g(q),S(!!M.trim())},onBlur:q=>{v(q),S(!1)},onKeyDown:q=>{q.key==="Enter"&&!M.trim()&&(q.preventDefault(),y(),S(!1))},ref:q=>x(q,{target:"panel"}),placeholder:V?"Type a message…":s?"":"Spectators cannot chat",disabled:!V}),H&&r.jsxs("div",{className:"flex flex-wrap items-center gap-0.5 basis-full sm:basis-auto",children:[r.jsxs("label",{className:"flex items-center gap-0.5 text-xs text-slate-300",children:[r.jsx("input",{type:"checkbox",checked:T,onChange:q=>k(q.target.checked),className:"accent-cyan-500"}),r.jsx("span",{children:"Speak"})]}),r.jsxs("select",{value:O,onChange:q=>{const $=q.target.value;Q($),C(ne=>({...ne||{},engine:$}))},className:"field-input text-xs",children:[r.jsx("option",{value:"flite",children:"flite"}),r.jsx("option",{value:"espeak",children:"espeak"})]}),O==="flite"?r.jsx("select",{value:D,onChange:q=>{const $=q.target.value;U($),C(ne=>({...ne||{},voice:$}))},className:"field-input text-xs",children:WN.map(q=>r.jsx("option",{value:q,children:q},q))}):r.jsx("select",{value:K,onChange:q=>{const $=Number(q.target.value);F($),C(ne=>({...ne||{},pitch:$}))},className:"field-input text-xs",children:ew.map(q=>r.jsxs("option",{value:q,children:["pitch ",q]},q))})]}),r.jsx("button",{type:"submit",disabled:!V||A,className:"button-dark h-full disabled:opacity-50 self-stretch",children:A?"...":"Send"})]})]})}function To({compact:n=!1}){const{session:s,setNickname:l}=nn(),{value:o,save:f}=Ge("profile",{nickname:""}),[d,h]=m.useState(o.nickname||""),[p,x]=m.useState(!1),g=s?.role!=="spectator";m.useEffect(()=>{h(o.nickname||"")},[o.nickname]);async function v(y){if(y.preventDefault(),!g)return;const S=(d||"").trim().slice(0,32);if(S){x(!0);try{await l(S),f({nickname:S})}catch(E){alert(E.message)}finally{x(!1)}}}return r.jsxs("form",{className:"grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-stretch gap-0.5",onSubmit:v,children:[r.jsx("input",{className:"field-input flex-1 min-w-0",value:d,onChange:y=>h(y.target.value),maxLength:32,placeholder:"Enter a nickname",disabled:!g}),r.jsx("button",{type:"submit",disabled:!g||p,className:"button-dark h-full shrink-0 whitespace-nowrap px-0.5 py-0 disabled:opacity-50",children:p?"Saving…":n?"Set":"Save"})]})}function tw(n){const s=Array.isArray(n?.socials)?n.socials:null;if(s&&s.length)return s;const l=[];return n?.discord?.invite&&l.push({id:"discord",label:"Discord",url:n.discord.invite}),n?.kofi?.link&&l.push({id:"kofi",label:"Ko-fi",url:n.kofi.link}),l}function nw(n){if(!n||typeof n!="object")return null;const s=n.id||n.service||n.key||n.name||null,l=n.label||n.title||n.name||s||"Link",o=n.url||n.link||n.href||null;return o?{id:s,label:l,url:o}:null}function Zg({className:n=""}){const{session:s}=nn(),l=tw(s).map(nw).filter(Boolean).slice(0,4);return l.length?r.jsx("div",{className:`grid grid-cols-2 grid-rows-2 gap-0.5 ${n}`,children:l.map(o=>r.jsx(hd,{...o},`${o.id||o.label}-${o.url}`))}):null}function aw({compact:n=!1}){return r.jsx("section",{className:"panel-section flex h-full min-h-0 flex-col gap-0.5 text-base",children:r.jsx("div",{className:"surface flex w-full items-center px-0 py-0",children:r.jsx(To,{compact:n})})})}function sw(){return r.jsx("section",{className:"panel-section flex h-full min-h-0 flex-col gap-0.5 text-base",children:r.jsx(Zg,{className:"flex-1 min-h-0"})})}function yf(...n){return n.filter(Boolean).join(" ")}function lw(n){const s=n?.batteryState?.percentDisplay;return s==null?"--":`${s}%`}function iw(n){return n?.batteryState?n.batteryState.urgentActive?"text-red-400":n.batteryState.warnActive?"text-amber-300":"text-emerald-300":"text-slate-400"}function rw(n){switch(n){case"admin":case"lockdown":case"lockdown-admin":return"text-amber-300";case"spectator":return"text-slate-400";default:return"text-sky-300"}}function ow(n,s){if(!n)return"";const l=n.nickname||n.socketId?.slice(0,6)||"unknown";return n.socketId&&n.socketId===s?`${l} (you)`:l}function Do({title:n="Rovers"}){const s=it(A=>A.session),{requestControl:l}=kn(),[o,f]=m.useState({}),[d,h]=m.useState(()=>Date.now()),p=m.useMemo(()=>s?.role&&s.role!=="spectator",[s?.role]),x=m.useMemo(()=>s?.role==="admin"||s?.role==="lockdown"||s?.role==="lockdown-admin",[s?.role]),g=s?.roster??[],v=s?.turnQueues??{},y=s?.users??[],S=s?.socketId||null,E=m.useMemo(()=>Object.values(v||{}).some(A=>A?.deadline||A?.idleDeadline),[v]);m.useEffect(()=>{if(!E)return;const A=setInterval(()=>{h(Date.now())},1e3);return()=>clearInterval(A)},[E]);const C=m.useMemo(()=>{const A=new Set(g.map(T=>String(T.id))),N=Object.keys(v||{}).filter(T=>!A.has(String(T))).map(T=>({id:T,name:T,locked:!1,batteryState:null}));return[...g,...N]},[g,v]);async function M(A){if(A){f(N=>({...N,[A]:!0}));try{await l(A)}catch(N){alert(N.message)}finally{f(N=>({...N,[A]:!1}))}}}const w=A=>y.find(N=>N.socketId===A)||{socketId:A,nickname:null,role:null};return r.jsxs("div",{className:"space-y-0.5 text-sm",children:[n&&r.jsx("p",{className:"text-sm text-slate-400",children:n}),C.length===0?r.jsx("p",{className:"text-sm text-slate-500",children:"No rovers registered."}):r.jsx("ul",{className:"space-y-0.5 text-sm",children:C.map(A=>{const N=String(A.id),T=v?.[N]||null,k=T?.queue||[],O=T?.idleDeadline||T?.deadline||null,Q=O&&O>d?Math.ceil((O-d)/1e3):O?0:null,D=T?.current||null,U=D?k.findIndex(ae=>ae===D):-1,K=k.length>1?U>=0?k[(U+1)%k.length]:k[0]:null,F=!!(S&&D&&D===S),V=!!(S&&K&&K===S),ee=Q!=null&&(F||V),oe=!!A.locked,ue=oe&&!x,H=!!(A?.private?.enabled&&A?.private?.open),R=A.lockReason?`locked: ${A.lockReason}`:"locked",L=oe?R:o[N]?"...":"request",Z=p&&!ue&&!o[N];return r.jsxs("li",{className:yf("surface flex flex-wrap items-start justify-between gap-0.5",Z&&"cursor-pointer",oe?"bg-red-900/40":H?"bg-amber-700/35 border border-amber-200/30":null),onClick:()=>{Z&&M(A.id)},children:[r.jsxs("div",{className:"flex min-w-0 flex-1 flex-col gap-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsxs("div",{className:"flex items-center gap-0.5",children:[r.jsx("p",{className:"text-slate-200",children:r.jsx("span",{className:"rounded px-1 py-[1px] border border-transparent",style:oa(A.color,.16),children:A.name})}),ee?r.jsx("span",{className:"rounded bg-slate-800 px-1 text-[0.7rem] text-slate-200",children:F?`${Q}s left`:`Your turn in ${Q}s`}):null]}),r.jsx("span",{className:yf("text-[0.75rem] font-semibold",iw(A)),children:lw(A)})]}),k.length===0?r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:"No queue."}):r.jsx("div",{className:"flex flex-wrap items-center gap-0.5",children:k.map((ae,ce)=>{const q=w(ae),$=ae===D,ne=!!(K&&ae===K&&!$),se=$?"bg-sky-600 text-white ring-2 ring-amber-300":ne?"bg-emerald-700/60 text-emerald-100 ring-1 ring-emerald-300/70":"bg-slate-800 text-slate-200";return r.jsxs("span",{className:`flex items-center gap-0.5 rounded px-1 text-[0.7rem] ${se}`,children:[r.jsx("span",{className:`${rw(q.role)} font-semibold`,children:ow(q,S)}),$&&r.jsx("span",{className:"text-[0.65rem] text-slate-200",children:"now"}),ne&&r.jsx("span",{className:"text-[0.65rem] text-emerald-100",children:"next"})]},`${N}-${ae}-${ce}`)})})]}),p?r.jsx("button",{type:"button",onClick:ae=>{ae.stopPropagation(),M(A.id)},disabled:o[N]||ue,className:yf("button-dark disabled:opacity-40",oe&&"bg-red-600/70 text-white hover:bg-red-600"),children:L}):null]},A.id)})})]})}function cw(n){switch(n){case"admin":case"lockdown":case"lockdown-admin":return"text-amber-300";case"spectator":return"text-slate-400";default:return"text-sky-300"}}function uw(n,s){if(!n)return"";const l=n.nickname||n.socketId?.slice(0,6)||"unknown";return n.socketId&&n.socketId===s?`${l} (you)`:l}function So({hideNicknameForm:n=!1,hideHeader:s=!1,className:l="",fillHeight:o=!1,compact:f=!1}){const{session:d}=nn(),h=d?.role!=="spectator",p=d?.users??[],x=d?.socketId||null,g=m.useMemo(()=>[...p].sort((y,S)=>y.socketId===x?-1:S.socketId===x?1:(y.nickname||"").localeCompare(S.nickname||"")),[x,p]),v=o?"flex-1 min-h-0 overflow-y-auto":f?"h-28 overflow-y-auto":"h-48 overflow-y-auto";return r.jsxs("section",{className:`panel-section space-y-0.5 text-base ${o?"flex h-full min-h-0 flex-col overflow-hidden":""} ${l}`,children:[!n&&r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]",children:[r.jsx("div",{className:"min-w-0",children:r.jsx("div",{className:"surface flex w-full items-center px-0 py-0",children:r.jsx(To,{compact:f})})}),r.jsx(Zg,{})]}),!h&&r.jsx("p",{className:"text-xs text-slate-500",children:"Spectators cannot set nicknames."})]}),r.jsxs("div",{className:`space-y-0.5 ${o?"flex flex-1 min-h-0 flex-col":""}`,children:[!s&&r.jsxs("div",{className:`flex items-center justify-between text-sm text-slate-400 ${f?"text-xs":""}`,children:[r.jsx("span",{children:"Users"}),r.jsx("span",{className:"text-xs text-slate-500",children:g.length})]}),r.jsx("div",{className:`surface flex flex-wrap content-start items-start gap-0.5 px-0 pb-0 ${v} ${f?"text-[0.8rem]":""}`,children:g.length===0?r.jsx("p",{className:"text-sm text-slate-500",children:"Waiting for users…"}):g.map(y=>r.jsx("span",{className:`rounded bg-slate-800/80 px-1 py-0.25 text-[0.7rem] ${cw(y.role)}`,children:uw(y,x)},y.socketId))})]})]})}const fw=/^cu_[a-f0-9]{32}$/,wi="field-input w-full max-w-sm text-left focus:ring-emerald-500",_o="mx-auto w-full max-w-xl flex justify-center",xi="mx-auto flex w-full max-w-md flex-col items-center space-y-0.5 text-center";function dw(n){const s=String(n||"").trim();return s?s.length<=10?`${s.slice(0,2)}***${s.slice(-2)}`:`${s.slice(0,6)}...${s.slice(-6)}`:""}const Sx=8*1024*1024,ss=16e3,mw={iceServers:[{urls:"stun:stun.l.google.com:19302"}],bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"};function jx(n){let s="";for(let o=0;o{if(!n||n.iceGatheringState==="complete"){l();return}const o=setTimeout(()=>{n.removeEventListener("icegatheringstatechange",f),l()},s);function f(){n.iceGatheringState==="complete"&&(clearTimeout(o),n.removeEventListener("icegatheringstatechange",f),l())}n.addEventListener("icegatheringstatechange",f)})}function Ax(n){if(!n)return!1;const s=n.connectionState,l=n.iceConnectionState;return s==="connected"||l==="connected"||l==="completed"}function gw(n,s=1e4){return new Promise((l,o)=>{if(!n){o(new Error("Peer connection missing"));return}if(Ax(n)){l();return}const f=setTimeout(()=>{h(),o(new Error("Peer connection timeout"))},s),d=()=>{Ax(n)?(h(),l()):(n.connectionState==="failed"||n.connectionState==="closed"||n.iceConnectionState==="failed")&&(h(),o(new Error(`Peer connection ${n.connectionState||n.iceConnectionState}`)))};function h(){clearTimeout(f),n.removeEventListener("connectionstatechange",d),n.removeEventListener("iceconnectionstatechange",d)}n.addEventListener("connectionstatechange",d),n.addEventListener("iceconnectionstatechange",d)})}async function vw(n){if(!n?.getParameters||!n?.setParameters)return;const s=n.getParameters()||{},l=s.encodings&&s.encodings[0]||{};s.encodings=[{...l,maxBitrate:64e3,dtx:"disabled"}];try{await n.setParameters(s)}catch{}}function bw(n,s=6e3){return new Promise((l,o)=>{if(!n){o(new Error("Peer connection missing"));return}const f=Date.now();let d=-1;const h=setInterval(async()=>{if(Date.now()-f>s){clearInterval(h),o(new Error("WHIP connected but no outbound audio flow"));return}try{const p=n.getSenders().filter(x=>x.track?.kind==="audio");for(const x of p){const g=await x.getStats();for(const v of g.values()){if(v.type!=="outbound-rtp"||v.kind!=="audio")continue;const y=Number(v.bytesSent||0),S=Number(v.packetsSent||0);if(d<0)d=y;else if(y>d+200||S>5){clearInterval(h),l();return}}}}catch{}},250)})}function vn({label:n,active:s,detail:l=""}){return r.jsxs("div",{className:`rounded-md px-0.5 py-0.5 text-xs text-slate-100 ${s?"bg-emerald-500":"bg-slate-700"}`,children:[r.jsx("div",{className:"text-center font-medium",children:n}),r.jsx("div",{className:"text-center text-[0.72rem] opacity-90",children:l||(s?"active":"idle")})]})}function Nx({label:n}){return n?r.jsx("span",{className:"rounded border border-white/40 px-1 text-[0.7rem] text-white",children:n}):null}function yw(n){const s=n.reduce((f,d)=>f+d.length,0),l=new Float32Array(s);let o=0;for(const f of n)l.set(f,o),o+=f.length;return l}function Sw(n){const s=new Int16Array(n.length);for(let l=0;l{for(let v=0;vv&&s?.[v]||null,[s,v]),ne=!!($&&$.state!=="offline"&&!$.error),se=$?.source==="upload"&&$?.state==="playing",pe=$?.source==="mic-whip"&&($?.state==="starting"||$?.state==="playing"),re=!!(!A&&K.current&&(M||q)),fe=!A&&(N==="live"||N==="starting"),be=A&&k==="recording",ve=A&&k==="sending",xe=Cn(p?.keymap?.micPtt?.[0])||"M",Le=m.useCallback(J=>{const he=J==="clip"?"clip":"live";g(Ae=>({...Ae||{},pttMode:he}))},[g]),De=async()=>{if(!v){D("Take control of your rover first.");return}if(!y){D("Select an audio file first.");return}if(y.size>Sx){D(`File too large (max ${Sx} bytes).`);return}C(!0),D("");try{const J=await y.arrayBuffer(),he=jx(new Uint8Array(J));await l?.({roverId:v,name:y.name,mime:y.type||"",dataBase64:he}),D("Upload playback started.")}catch(J){D(J?.message||"Failed to play upload.")}finally{C(!1)}},Ee=async()=>{if(!v){D("Take control of your rover first.");return}C(!0),D("");try{await o?.(v),D("Upload playback stopped.")}catch(J){D(J?.message||"Failed to stop upload.")}finally{C(!1)}},nt=m.useCallback(async J=>{const he=String(J||ee.current||"").trim();if(V.current=!1,T("idle"),F.current){try{F.current.getSenders().forEach(Ae=>Ae.track?.stop())}catch{}try{F.current.close()}catch{}}if(F.current=null,U.current)try{U.current.getTracks().forEach(Ae=>Ae.stop())}catch{}if(U.current=null,K.current=null,he)try{await h?.(he)}catch{}ee.current=""},[h]),Ve=m.useCallback(async J=>{const he=await f?.(J),Ae=String(he?.whipUrl||"").trim(),ht=String(he?.token||"").trim();if(!Ae||!ht)throw new Error("WHIP endpoint unavailable");const Oe=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:ss,echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1}});U.current=Oe;const rt=Oe.getAudioTracks()?.[0];if(K.current=rt||null,rt&&(rt.enabled=!!(M||q)),rt?.applyConstraints)try{await rt.applyConstraints({channelCount:1,sampleRate:ss,echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1})}catch{}const ot=new RTCPeerConnection(mw);F.current=ot,ot.onconnectionstatechange=()=>{if(!V.current)return;const Lt=ot.connectionState;if(Lt==="connected"){T("live");return}(Lt==="failed"||Lt==="disconnected"||Lt==="closed")&&(T("error"),D(`WHIP transport ${Lt}.`))},Oe.getAudioTracks().forEach(Lt=>{const fl=ot.addTrack(Lt,Oe);vw(fl)});const gt=await ot.createOffer({offerToReceiveAudio:!1,offerToReceiveVideo:!1});await ot.setLocalDescription(gt),await xw(ot,1800);const kt=await fetch(Ae,{method:"POST",headers:{"Content-Type":"application/sdp",...pw(ht)},body:ot.localDescription?.sdp||gt.sdp});if(!kt.ok)throw new Error(`WHIP request failed: ${kt.status}`);const fs=await kt.text();await ot.setRemoteDescription({type:"answer",sdp:fs}),await gw(ot,1e4),await bw(ot,6e3),await d?.(J)},[M,q,d,f]),mt=m.useCallback(async()=>{if(ae.current=!1,R.current){try{R.current.disconnect()}catch{}R.current.onaudioprocess=null}if(H.current)try{H.current.disconnect()}catch{}if(L.current)try{L.current.disconnect()}catch{}if(oe.current)try{oe.current.getTracks().forEach(J=>J.stop())}catch{}if(ue.current)try{await ue.current.close()}catch{}Z.current=[],R.current=null,H.current=null,L.current=null,oe.current=null,ue.current=null,ce.current=ss},[]),ie=m.useCallback(async()=>{if(oe.current&&ue.current&&R.current)return;O(rt=>rt==="recording"?rt:"arming");const J=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:ss,echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1}}),he=new AudioContext({latencyHint:"interactive"}),Ae=he.createMediaStreamSource(J),ht=he.createScriptProcessor(1024,1,1),Oe=he.createGain();Oe.gain.value=0,ht.onaudioprocess=rt=>{if(!ae.current)return;const ot=rt.inputBuffer.getChannelData(0);Z.current.push(new Float32Array(ot))},Ae.connect(ht),ht.connect(Oe),Oe.connect(he.destination),oe.current=J,ue.current=he,H.current=Ae,R.current=ht,L.current=Oe,ce.current=he.sampleRate||ss},[]),me=m.useCallback(async({send:J})=>{const he=ae.current;if(ae.current=!1,!he){J||O("idle");return}const Ae=Z.current;if(Z.current=[],!J){O("idle");return}if(!v){O("idle");return}if(!Ae.length){O("idle");return}O("sending");try{const ht=yw(Ae),Oe=jw(ht,ce.current||ss),rt=jx(Oe);await l?.({roverId:v,name:`ptt-${Date.now()}.wav`,mime:"audio/wav",dataBase64:rt}),O("idle")}catch(ht){O("error"),D(ht?.message||"Failed to send PTT clip.")}},[l,v]),le=m.useCallback(async()=>{if(!v){D("Take control of your rover first.");return}await ie(),Z.current=[],ae.current=!0,O("recording")},[ie,v]);return m.useEffect(()=>{let J=!1;async function he(){if(A){await nt(ee.current||v);return}if(!v){await nt(ee.current);return}if(!(V.current&&ee.current===v)&&!(!M&&!q))try{T("starting"),D(""),V.current=!0,ee.current=v,await Ve(v),J||T("live")}catch(Ae){J||(T("error"),D(Ae?.message||"Failed to start mic forwarding.")),await nt(v)}}return he(),()=>{J=!0}},[A,M,q,v,Ve,nt]),m.useEffect(()=>{const J=K.current;J&&(J.enabled=!!(!A&&(M||q)))},[A,M,q]),m.useEffect(()=>{let J=!1;async function he(){if(!A||!v){ae.current&&await me({send:!1}),O("idle");return}try{await ie(),!J&&!ae.current&&O("idle")}catch(Ae){J||(O("error"),D(Ae?.message||"Microphone access failed for clip mode."))}}return he(),()=>{J=!0}},[A,ie,me,v]),m.useEffect(()=>{let J=!1;async function he(){if(A){if(!v){ae.current&&await me({send:!1});return}if(q){if(!ae.current)try{D(""),await le()}catch(Ae){J||(O("error"),D(Ae?.message||"Failed to start clip recording."))}}else ae.current&&await me({send:!0})}}return he(),()=>{J=!0}},[A,me,q,v,le]),m.useEffect(()=>()=>{nt(ee.current||v),mt()},[v,nt,mt]),r.jsx("section",{className:"surface",children:r.jsxs("div",{className:"grid gap-1",children:[r.jsx("p",{className:"text-sm text-slate-100 text-center",children:"Audio Controls"}),r.jsx("section",{className:"surface",children:r.jsxs("div",{className:"flex items-center justify-center gap-0.5 py-0.25 text-xs text-slate-300",children:[r.jsx("span",{children:"Push-to-Talk Key"}),r.jsx(Nx,{label:xe})]})}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-1 lg:grid-cols-2",children:[r.jsx("section",{className:"surface h-full",children:r.jsxs("div",{className:"grid h-full gap-0.5 grid-rows-[auto_auto_1fr]",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"PTT Mode"}),r.jsx("p",{className:"text-xs text-slate-400 text-center",children:"Choose how holding your PTT key behaves: live mic stream, or record-and-send clip. If the live stream doesn't work, switch to clip mode."}),r.jsxs("div",{className:"grid grid-cols-2 gap-0.5",children:[r.jsx("button",{type:"button",className:`button-dark w-full text-sm ${A?"":"bg-emerald-500 text-white hover:bg-emerald-500"}`,onClick:()=>Le("live"),children:"Live Mic (WHIP)"}),r.jsx("button",{type:"button",className:`button-dark w-full text-sm ${A?"bg-emerald-500 text-white hover:bg-emerald-500":""}`,onClick:()=>Le("clip"),children:"Record Clip"})]})]})}),r.jsx("section",{className:"surface h-full",children:r.jsxs("div",{className:"grid h-full gap-0.5 grid-rows-[auto_1fr]",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"PTT Clip Status"}),r.jsxs("div",{className:"grid gap-0.5 content-start",children:[r.jsx(vn,{label:"Mode",active:A,detail:A?"record clip":"live mic"}),r.jsx(vn,{label:"PTT hold",active:q,detail:q?"held":"released"}),r.jsx(vn,{label:"Recording",active:be,detail:be?"capturing":"idle"}),r.jsx(vn,{label:"Playback",active:A&&se,detail:A&&se?"playing":"idle"})]})]})})]}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-1 lg:grid-cols-2",children:[r.jsx("section",{className:"surface h-full",children:r.jsxs("div",{className:"grid h-full gap-0.5 grid-rows-[auto_auto_1fr_auto]",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"Audio Upload"}),r.jsxs("label",{className:"mx-auto grid w-full max-w-sm gap-0.5 text-xs text-slate-300 text-center",children:[r.jsx("span",{children:"Audio file (mp3 / wav / ogg)"}),r.jsx("input",{className:`${wi} text-center`,type:"file",accept:".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg",disabled:E||!v,onChange:J=>S(J.target.files?.[0]||null)})]}),r.jsx("div",{className:"mx-auto w-full max-w-sm text-center",children:y?r.jsxs("div",{className:"surface-muted text-xs text-slate-300",children:[y.name," (",y.size," bytes)"]}):r.jsx("div",{className:"surface-muted text-xs text-slate-500",children:"No file selected"})}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:E||!v,onClick:De,children:E?"Working...":"Play Upload"}),r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:E||!v,onClick:Ee,children:"Stop"})]})]})}),r.jsx("section",{className:"surface h-full",children:r.jsxs("div",{className:"grid h-full gap-0.5 grid-rows-[auto_auto_1fr]",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"Live Microphone"}),r.jsxs("label",{className:"surface-muted mx-auto flex w-full max-w-sm items-center justify-center gap-0.5 px-0.5 py-0.5 text-xs text-slate-300",children:[r.jsx("input",{type:"checkbox",checked:M,disabled:!v||A,onChange:J=>g(he=>({...he||{},openMicEnabled:!!J.target.checked}))}),r.jsx("span",{children:A?"Open mic (live mode only)":"Open mic"})]}),r.jsxs("div",{className:"grid gap-0.5 content-start",children:[r.jsx(vn,{label:"WHIP link",active:fe,detail:N}),r.jsx(vn,{label:"Mic hot",active:re,detail:re?"transmitting":"muted"}),r.jsxs("div",{className:"surface-muted text-center text-xs text-slate-400",children:["Hold ",r.jsx(Nx,{label:xe})," to talk"]})]})]})})]}),r.jsx("section",{className:"surface",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"Audio Status"}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-6",children:[r.jsx(vn,{label:"Forward pipe",active:ne,detail:ne?"connected":"offline"}),r.jsx(vn,{label:"Upload playback",active:se,detail:se?"playing":"idle"}),r.jsx(vn,{label:"Mic relay",active:pe,detail:pe?"active":"idle"}),r.jsx(vn,{label:"WHIP transport",active:fe,detail:N}),r.jsx(vn,{label:"Clip recording",active:be,detail:be?"recording":"idle"}),r.jsx(vn,{label:"Clip sending",active:ve,detail:ve?"sending":"idle"})]}),Q?r.jsx("div",{className:"text-xs text-slate-400 text-center",children:Q}):null]})})]})})}function Nw({pendingRequestId:n,currentStoredKey:s,nickname:l,requestVerification:o,applyIdentityKey:f,onMessage:d,fullWidth:h=!1}){const[p,x]=m.useState(0),[g,v]=m.useState(""),[y,S]=m.useState(!1),[E,C]=m.useState(!1),M=()=>{x(1),v(s),S(!1),d?.("")},w=()=>{x(0),S(!1)},A=async T=>{if(T.preventDefault(),!y){d?.("Please confirm your nickname agreement before sending.");return}if(p<3){d?.("Complete all request steps before sending.");return}C(!0),d?.("");try{const k=await f(g);await o?.(),v(k),x(0),S(!1),d?.("Verification request sent to lockdown admins.")}catch(k){d?.(k.message||"Failed to submit request.")}finally{C(!1)}},N=h?"w-full":_o;return n?r.jsx("section",{className:`surface text-sm text-slate-300 ${N}`,children:r.jsxs("div",{className:xi,children:["Verification request pending: ",n]})}):p===0?r.jsx("section",{className:`surface ${N}`,children:r.jsxs("div",{className:xi,children:[r.jsx("p",{className:"text-sm text-slate-300",children:"Verification"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:M,disabled:E,children:"Request Verification"})]})}):r.jsx("form",{className:`surface ${N}`,onSubmit:A,children:r.jsxs("div",{className:xi,children:[r.jsx("p",{className:"text-sm text-slate-300",children:"Request verification"}),r.jsxs("p",{className:"text-xs text-slate-500",children:["Step ",p," of 3"]}),p===1?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Confirm your nickname, which is used as part of the verification process. You can edit it here before requesting."}),r.jsx("div",{className:"mx-auto w-full max-w-sm",children:r.jsx(To,{compact:!0})}),r.jsxs("div",{className:"surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center",children:["Current nickname: ",r.jsx("span",{className:"font-semibold",children:l||"(not set)"})]}),r.jsxs("label",{className:"flex items-center justify-center gap-0.5 text-xs text-slate-300 text-center",children:[r.jsx("input",{type:"checkbox",className:"accent-emerald-500",checked:y,onChange:T=>S(T.target.checked)}),r.jsx("span",{children:"I understand this nickname is tied to my verification."})]}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:!l||!y,onClick:()=>x(2),children:"Continue"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:w,children:"Cancel"})]})]}):null,p===2?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Save your identity key in a safe place. You can use it to restore your identity in another browser."}),r.jsx("input",{className:wi,type:"password",name:"identity_key_request",autoComplete:"current-password",maxLength:35,value:g,onChange:T=>v(T.target.value.toLowerCase()),placeholder:"Identity key for this request"}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:!g,onClick:()=>navigator.clipboard?.writeText(g),children:"Copy Key"}),r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:!g,onClick:()=>x(3),children:"Continue"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:()=>x(1),children:"Back"})]})]}):null,p===3?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Final step: confirm and send your verification request."}),r.jsx("input",{className:wi,type:"password",name:"identity_key_request_final",autoComplete:"current-password",maxLength:35,value:g,onChange:T=>v(T.target.value.toLowerCase()),placeholder:"Identity key for this request"}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"submit",className:"button-dark text-sm",disabled:E||!g,children:"Confirm Request"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:()=>x(2),children:"Back"})]})]}):null]})})}function ww({currentStoredKey:n,applyIdentityKey:s,onMessage:l,fullWidth:o=!1}){const[f,d]=m.useState(0),[h,p]=m.useState(""),[x,g]=m.useState(!1),v=()=>{d(1),p(""),l?.("")},y=()=>{d(0),p("")},S=async C=>{if(C.preventDefault(),!(f<2)){g(!0),l?.("");try{await s(h),d(0),p(""),l?.("Identity key restored.")}catch(M){l?.(M.message||"Failed to restore key.")}finally{g(!1)}}},E=o?"w-full":_o;return r.jsx("section",{className:`surface ${E}`,children:r.jsxs("div",{className:xi,children:[r.jsx("p",{className:"text-sm text-slate-300",children:"Identity key"}),r.jsxs("p",{className:"text-xs text-slate-500",children:["Current: ",dw(n)||"not set yet"]}),r.jsx("input",{className:wi,type:"password",name:"identity_key_current",autoComplete:"current-password",maxLength:35,value:n,readOnly:!0,placeholder:"Identity key"}),f===0?r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:!n,onClick:()=>navigator.clipboard?.writeText(n),children:"Copy Key"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:v,disabled:x,children:"Restore Key"})]}):null,f===1?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Restoring your key should only be done when needed. Use this to move your identity to another browser."}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:()=>d(2),children:"Continue"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:y,children:"Cancel"})]})]}):null,f===2?r.jsxs("form",{className:"space-y-0.5",onSubmit:S,children:[r.jsx("input",{className:wi,type:"password",name:"identity_key_restore",autoComplete:"current-password",maxLength:35,value:h,onChange:C=>p(C.target.value.toLowerCase()),placeholder:"Paste key to restore"}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"submit",className:"button-dark text-sm",disabled:x||!h,children:"Confirm Restore"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:()=>d(1),children:"Back"})]})]}):null]})})}function Cw({requestableRovers:n=[],pendingRequests:s=[],requestPrivateRoverAccess:l,onMessage:o,fullWidth:f=!1}){const[d,h]=m.useState({}),p=m.useMemo(()=>{const v=new Map;return(s||[]).forEach(y=>{y?.roverId&&v.set(String(y.roverId),y)}),v},[s]),x=f?"w-full":_o,g=async v=>{if(v){h(y=>({...y,[v]:!0})),o?.("");try{(await l?.(v))?.existing?o?.("You already have a pending request for that rover."):o?.("Private rover access request sent to lockdown admins.")}catch(y){o?.(y.message||"Failed to send request.")}finally{h(y=>({...y,[v]:!1}))}}};return r.jsx("section",{className:`surface text-sm text-slate-300 ${x}`,children:r.jsxs("div",{className:xi,children:[r.jsx("p",{className:"text-sm text-slate-300",children:"Private rover access requests"}),n.length===0?r.jsx("p",{className:"text-xs text-slate-500",children:"No closed private rovers are available to request right now."}):r.jsx("div",{className:"w-full space-y-0.5",children:n.map(v=>{const y=String(v.id),S=!!d[y],E=p.get(y);return r.jsxs("div",{className:"surface-muted flex items-center justify-between gap-0.5 px-1 py-0.5",children:[r.jsxs("div",{className:"min-w-0 text-left",children:[r.jsx("p",{className:"truncate text-xs font-semibold text-slate-100",children:v.name||y}),r.jsx("p",{className:"truncate text-[0.7rem] text-slate-500",children:y})]}),E?r.jsx("span",{className:"rounded bg-amber-700/30 px-1 py-0.5 text-[0.7rem] text-amber-200",children:"Pending"}):r.jsx("button",{type:"button",onClick:()=>g(y),disabled:S,className:"button-dark text-xs disabled:opacity-50",children:S?"Sending...":"Request"})]},y)})})]})})}function Vf(n){return String(n||"").trim()}function Ew(n){const s=Vf(n);return s?s.includes("DOCKINGRUNNING")?"Returning":s.includes("PAUSED")?"Paused":s.includes("HOUSECLEANINGRUNNING")?"Cleaning":s.includes("SPOTCLEANINGRUNNING")?"Spot cleaning":s.includes("STATE_START")?"Starting":s.includes("STATE_IDLE")||s.includes("STATE_STANDBY")?"Idle":s:"--"}function Ig(n){return n==="good"?"bg-emerald-600 text-white":n==="warn"?"bg-amber-500 text-slate-900":n==="danger"?"bg-rose-600 text-white":n==="info"?"bg-sky-600 text-white":"bg-slate-700 text-slate-100"}function ls({label:n,value:s,tone:l="muted",valueClass:o=""}){return r.jsxs("div",{className:`rounded-md px-1 py-0.75 text-center ${Ig(l)}`,children:[r.jsx("div",{className:"text-[0.72rem] opacity-90",children:n}),r.jsx("div",{className:`text-sm font-semibold ${o}`,title:String(s||""),children:s})]})}function kw({neato:n,onStart:s,onSendHome:l,onLocate:o,onClearErrors:f,fullWidth:d=!1}){const[h,p]=m.useState(""),x=d?"w-full":"w-full max-w-xl",g=!!n?.configured,v=!!(n?.enabled&&n?.connected),y=!!n?.telemetry?.extPowerPresent,S=!!n?.telemetry?.chargingActive,E=Ew(n?.telemetry?.uiState),C=n?.telemetry?.batteryPercent,M=Number.isFinite(C)?`${C}%`:"--",w=n?.telemetry?.batteryVoltage,A=Number.isFinite(w)?`${w.toFixed(2)} V`:"--",N=Vf(n?.telemetry?.robotError)||"--",T=Vf(n?.telemetry?.robotAlert)||"--",k=N!=="--"&&!/^no errors$/i.test(N)&&!/^200/.test(N),O=T!=="--"&&!/^200/.test(T),Q=n?.controls||{},D=!!Q?.start?.available,U=!!Q?.sendHome?.available,K=!!Q?.locate?.available,F=!!Q?.clearErrors?.available,V=m.useMemo(()=>y?{key:"start",label:"Start cleaning",pending:"Starting...",toneClass:"bg-emerald-600 hover:bg-emerald-500 text-white",canRun:D,fn:s}:{key:"home",label:"Send to dock",pending:"Sending...",toneClass:"bg-sky-600 hover:bg-sky-500 text-white",canRun:U,fn:l},[y,D,s,U,l]),ee=g&&v&&V.canRun,oe=g&&v&&K,ue=g&&v&&F,H=async(ce,q)=>{if(q){p(ce);try{await q()}catch{}finally{p("")}}},R=g?v?"Online":"Offline":"Not configured",L=g?v?"good":"danger":"warn",Z=C==null?"muted":C>=60?"good":C>=25?"warn":"danger",ae=y?"Docked":E!=="--"?E:"Away from dock";return r.jsx("section",{className:`surface text-sm text-slate-200 ${x}`,children:r.jsxs("div",{className:"grid gap-0.5",children:[r.jsxs("div",{className:"relative flex items-center justify-center min-h-[1.5rem]",children:[r.jsxs("div",{className:"text-center",children:[r.jsx("p",{className:"text-sm text-slate-100",children:"Neato Controls"}),r.jsx("p",{className:"text-xs text-slate-400",children:"Control the autonomous Neato robovac"})]}),r.jsx("span",{className:`absolute right-0 inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${Ig(L)}`,children:R})]}),r.jsx("section",{className:"surface-muted px-0.5 py-0.5",children:r.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-0.5",children:[r.jsxs("div",{className:"grid gap-0.5",children:[r.jsxs("div",{className:"rounded-md bg-slate-800 px-1 py-0.75 text-center",children:[r.jsx("div",{className:"text-xs text-slate-300",children:"State"}),r.jsx("div",{className:"text-base font-semibold text-slate-100",children:ae})]}),r.jsx("button",{type:"button",disabled:!ee||!!h,onClick:()=>H(V.key,V.fn),className:`rounded-md px-1 py-1 text-base font-semibold transition disabled:opacity-50 ${V.toneClass}`,children:h===V.key?V.pending:V.label})]}),r.jsx("button",{type:"button",disabled:!oe||!!h,onClick:()=>H("locate",o),className:"rounded-md bg-fuchsia-600 px-1 py-0.75 text-sm font-semibold text-white transition hover:bg-fuchsia-500 disabled:opacity-50",children:h==="locate"?"Playing...":"Play sound"}),r.jsx("button",{type:"button",disabled:!ue||!!h,onClick:()=>H("clearErrors",f),className:"rounded-md bg-amber-500 px-1 py-0.75 text-sm font-semibold text-slate-900 transition hover:bg-amber-400 disabled:opacity-50",children:h==="clearErrors"?"Clearing...":"Clear errors"})]})}),r.jsx("section",{className:"surface-muted px-0.5 py-0.5",children:r.jsxs("div",{className:"grid gap-0.5 grid-cols-2 sm:grid-cols-3 lg:grid-cols-7",children:[r.jsx(ls,{label:"Battery",value:M,tone:Z}),r.jsx(ls,{label:"Voltage",value:A,tone:w==null?"muted":"info"}),r.jsx(ls,{label:"Docked",value:y?"Yes":"No",tone:y?"info":"muted"}),r.jsx(ls,{label:"Charging",value:S?"Yes":"No",tone:S?"info":"muted"}),r.jsx(ls,{label:"UI state",value:E,tone:"muted",valueClass:"truncate"}),r.jsx(ls,{label:"Robot error",value:N,tone:k?"danger":"muted",valueClass:"truncate"}),r.jsx(ls,{label:"Robot alert",value:T,tone:O?"warn":"muted",valueClass:"truncate"})]})}),!g||!v||!V.canRun||!K||!F?r.jsx("p",{className:"text-xs text-slate-400 text-center",children:g?v?"Waiting for required Neato entities in Home Assistant.":"Home Assistant is offline.":"Set homeAssistant.neato.device in server config to enable Neato controls."}):null]})})}function Jg(){const{session:n,identifySession:s,requestVerification:l,requestPrivateRoverAccess:o,playUploadedAudio:f,stopUploadedAudio:d,startMicWhip:h,readyMicWhip:p,stopMicWhip:x,neatoStart:g,neatoSendHome:v,neatoLocate:y,neatoClearErrors:S}=nn(),{value:E,save:C}=Ge("identity",{cookieUserId:""}),{value:M}=Ge("profile",{nickname:""}),w=m.useMemo(()=>(E?.cookieUserId||"").trim(),[E?.cookieUserId]),A=(M?.nickname||"").trim(),N=!!n?.isVerified,T=n?.verification?.pendingRequestId||null,k=n?.privateRoverAccess?.requestableRovers||[],O=n?.privateRoverAccess?.pendingRequests||[],Q=String(n?.assignment?.roverId||"").trim(),[D,U]=m.useState(""),K=async F=>{const V=String(F||"").trim().toLowerCase();if(!V)throw new Error("Identity key required.");if(!fw.test(V))throw new Error("Identity key must match format: cu_ + 32 lowercase hex chars.");return C(ee=>({...ee||{},cookieUserId:V})),await s({cookieUserId:V,nickname:A}),V};return r.jsxs("section",{className:"space-y-2 text-base",children:[r.jsxs("div",{className:"grid gap-2 grid-cols-1 lg:grid-cols-[minmax(0,1.25fr)_minmax(0,0.9fr)]",children:[N?null:r.jsx("div",{className:"lg:col-span-2",children:r.jsx(Nw,{pendingRequestId:T,currentStoredKey:w,nickname:A,requestVerification:l,applyIdentityKey:K,onMessage:U,fullWidth:!0})}),r.jsx("div",{className:"lg:col-span-2",children:N?r.jsxs("div",{className:"space-y-2",children:[r.jsx(kw,{neato:n?.neato||null,onStart:g,onSendHome:v,onLocate:y,onClearErrors:S,fullWidth:!0}),r.jsx(Aw,{ownRoverId:Q,audioForwardByRover:n?.audioForward||{},playUploadedAudio:f,stopUploadedAudio:d,startMicWhip:h,readyMicWhip:p,stopMicWhip:x})]}):r.jsx("section",{className:"surface h-full",children:r.jsx("div",{className:"flex h-full flex-col items-center justify-center text-center text-xs text-slate-400",children:"Verify your account to unlock VIP controls."})})}),r.jsx("div",{className:"lg:col-span-2",children:r.jsx(Cw,{requestableRovers:k,pendingRequests:O,requestPrivateRoverAccess:o,onMessage:U,fullWidth:!0})}),r.jsx("div",{className:"lg:col-span-2",children:r.jsx(ww,{currentStoredKey:w,applyIdentityKey:K,onMessage:U,fullWidth:!0})})]}),D?r.jsx("div",{className:_o,children:r.jsx("p",{className:"text-xs text-slate-400 text-center",children:D})}):null]})}const Mw=420,Rw=1200,qw={1:262,2:330,3:392,4:523};function jo(){const{session:n}=nn(),s=tn(),{value:l}=Ge("audio",st),o=Number.isFinite(l?.masterVolume)?l.masterVolume:st.masterVolume,f=Number.isFinite(l?.alertVolume)?l.alertVolume:st.alertVolume,d=Math.max(0,Math.min(1,o*f)),h=m.useMemo(()=>{const M=Array.isArray(n?.buttonBox?.buttons)?n.buttonBox.buttons:[];return M.length===4?M:[1,2,3,4].map(w=>M.find(A=>Number(A?.id)===w)||{id:w,count:0,goal:0,rewardName:null,rewardId:null,rewardNumber:null,lastRewardAt:null})},[n?.buttonBox?.buttons]),[p,x]=m.useState({}),[g,v]=m.useState({}),y=m.useRef(new Map),S=m.useRef(new Map),E=m.useRef({}),C=m.useRef(null);return m.useEffect(()=>{const M={};h.forEach(A=>{M[A.id]=Number(A.lastRewardAt||0)});const w=E.current||{};h.forEach(A=>{const N=Number(A.id),T=M[N]||0,k=Number(w[N]||0);if(T>0&&T!==k){v(D=>({...D,[N]:!0}));const O=S.current.get(N);O&&clearTimeout(O);const Q=setTimeout(()=>{v(D=>({...D,[N]:!1})),S.current.delete(N)},Rw);S.current.set(N,Q)}}),E.current=M},[h]),m.useEffect(()=>{function M(A){const N=qw[A];if(!N||d<=0)return;let T=C.current;if(!T){const D=window.AudioContext||window.webkitAudioContext;if(!D)return;T=new D,C.current=T}const k=T.createOscillator(),O=T.createGain();k.type="square",k.frequency.value=N;const Q=.14*d;O.gain.setValueAtTime(1e-4,T.currentTime),O.gain.exponentialRampToValueAtTime(Math.max(1e-4,Q),T.currentTime+.01),O.gain.exponentialRampToValueAtTime(1e-4,T.currentTime+.2),k.connect(O),O.connect(T.destination),k.start(),k.stop(T.currentTime+.21)}function w(A={}){const N=Number(A.buttonId);if(!Number.isFinite(N)||N<1||N>4)return;x(O=>({...O,[N]:!0}));const T=y.current.get(N);T&&clearTimeout(T);const k=setTimeout(()=>{x(O=>({...O,[N]:!1})),y.current.delete(N)},Mw);y.current.set(N,k),M(N)}return s.on("buttonBox:increment",w),()=>{s.off("buttonBox:increment",w)}},[d,s]),r.jsxs("section",{className:"panel-section space-y-0.5 text-base",children:[r.jsx("header",{className:"panel-muted text-center text-sm",children:r.jsx("p",{children:"Button Box"})}),r.jsx("div",{className:"grid grid-cols-4 gap-0.5",children:h.map(M=>{const w=Number(M.id),A=Number.isFinite(M.count)?M.count:0,N=Number.isFinite(M.goal)?M.goal:0,T=typeof M.rewardName=="string"&&M.rewardName.trim()?M.rewardName.trim():"Unassigned",k=Number.isFinite(M.rewardNumber)?M.rewardNumber:"?",O=!!p[w],Q=!!g[w];return r.jsx(xg,{buttonId:w,count:A,goal:N,rewardNumber:k,rewardName:T,className:[O?"bg-cyan-900/45":"",Q?"bg-fuchsia-900/45":""].filter(Boolean).join(" ")},w)})})]})}function Tw(){const{state:{roverId:n}}=_t(),l=ul(n)?.sensors||{};return r.jsx("section",{className:"panel-section",children:r.jsx("div",{className:"aspect-square w-full",children:r.jsx(Hf,{sensors:l})})})}function Dw(){const{state:{roverId:n,keymap:s,camera:l,horn:o},pipeline:f,actions:{setServoAngle:d,setNightVision:h,startHorn:p,stopHorn:x}}=_t(),g=qg(n),v=g.docked&&!g.driving,y=l?.config,S=!!(n&&l?.enabled&&y),E=!!(n&&f?.nightVision),C=f?.nightVisionState,M=!!(n&&f?.horn),w=o?.overheated,A=typeof y?.minAngle=="number"?y.minAngle:-30,N=typeof y?.maxAngle=="number"?y.maxAngle:30,T=typeof l?.angle=="number"?l.angle:typeof y?.homeAngle=="number"?y.homeAngle:(A+N)/2,k=Cn(s?.nightVisionToggle?.[0]),O=Cn(s?.hornHonk?.[0]),Q=Cn(s?.cameraUp?.[0]),D=Cn(s?.cameraDown?.[0]);return r.jsxs("section",{className:"panel-section grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto] gap-0.5",children:[r.jsx(Tg,{layout:"desktop",expand:!0,driveDockState:g}),v?null:r.jsxs("div",{className:"surface space-y-0.5 p-0 text-sm text-slate-200",children:[E&&r.jsx(Dg,{nightVisionOn:C?.nightVisionOn,disabled:!n,onToggle:h,keyLabel:k}),M&&r.jsx(_g,{disabled:!n||w,onStart:p,onStop:x,keyLabel:O,active:o?.active,heat:o?.heat}),S&&r.jsx(Bg,{value:T,min:A,max:N,onChange:d,keyDownLabel:D,keyUpLabel:Q,className:"space-y-0.5 px-1 py-1",labelRowClass:"text-xs text-slate-300",labelClass:"",valueClass:"font-mono text-slate-100",sliderClass:"w-full",accentClass:"accent-emerald-400",endpointClass:"text-[0.7rem] text-slate-400"})]})]})}function _w({layout:n,onOpenHelpOverlay:s}){const l=it(E=>E.session),{state:o}=_t(),{value:f}=Ge("vipAudio",{openMicEnabled:!1,pttMode:"live"}),d=l?.isVerified?"bg-emerald-400":"bg-red-600",h=String(l?.assignment?.roverId||"").trim(),p=h?l?.audioForward?.[h]:null,x=!!o?.mic?.pttActive,g=!!f?.openMicEnabled,v=f?.pttMode==="clip"?"clip":"live",y=!!(h&&l?.isVerified&&(v==="clip"?x:g||x)),S=!!(h&&l?.isVerified&&v==="clip"&&p?.source==="upload"&&p?.state==="playing");return r.jsx("section",{className:"panel text-base",children:r.jsxs(yd,{defaultTab:"telemetry",children:[r.jsxs(Sd,{children:[r.jsx(Wt,{id:"telemetry",children:"Controls"}),r.jsx(Wt,{id:"vip",highlight:S?"green":y?"pink":"none",children:r.jsxs("span",{className:"inline-flex items-center gap-2",children:[r.jsx("span",{children:"VIP"}),r.jsx("span",{className:`inline-block h-3 w-3 rounded-full ${d}`,"aria-hidden":"true",title:l?.isVerified?"Verified":"Not verified"})]})}),r.jsx(Wt,{id:"help",children:"Help"}),r.jsx(Wt,{id:"settings",children:"Settings"})]}),r.jsxs(jd,{children:[r.jsx(en,{id:"telemetry",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"grid items-stretch gap-0.5 grid-cols-[minmax(0,1.35fr)_minmax(0,0.95fr)]",children:[r.jsx(Tw,{}),r.jsx(Dw,{})]}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,0.9fr)_minmax(0,0.75fr)]",children:[r.jsx(Do,{}),r.jsx(ad,{panelId:"replay-sources-desktop",fillHeight:!0}),r.jsx(sw,{})]}),r.jsxs("div",{className:"grid items-stretch gap-0.5 grid-cols-[minmax(0,1.3fr)_minmax(0,0.22fr)] h-[14rem]",children:[r.jsx(qo,{fillHeight:!0}),r.jsxs("div",{className:"grid min-h-0 gap-0.5 grid-rows-[minmax(0,1fr)_auto]",children:[r.jsx(So,{compact:!0,hideNicknameForm:!0,fillHeight:!0}),r.jsx(aw,{compact:!0})]})]}),r.jsx(Hg,{}),r.jsx(jo,{}),r.jsx(fd,{defaultOrientation:"horizontal",panelId:"rightpane-telemetry"})]})}),r.jsx(en,{id:"vip",keepMounted:!0,children:r.jsx(Jg,{})}),r.jsx(en,{id:"help",children:r.jsx(Kg,{layout:n,onOpenOverlay:s})}),r.jsx(en,{id:"settings",children:r.jsx(Fg,{})})]})]})})}const Bw=new Set(["admin","lockdown","lockdown-admin"]),Ow=new Set(["lockdown","lockdown-admin"]),Lw=new Set(["admin","lockdown"]);function zw(n="admin"){return n==="lockdown"?{title:"Lockdown mode active",description:"Only the server owners can access the interface at this time."}:{title:"Admin mode active"}}function Uw(){const n=it(S=>S.session),s=n?.mode,l=n?.role,o=n?.adminReason?.text||"";n?.adminReason?.updatedAt;const f=n?.timezone||"UTC",d=n?.socials?.find(S=>String(S?.id||S?.label||"").toLowerCase()==="discord")?.url||n?.discord?.invite||null,h=Lw.has(s),p=s==="lockdown"?Ow.has(l):Bw.has(l),[x,g]=m.useState(()=>new Date),v=m.useMemo(()=>{try{return new Intl.DateTimeFormat("en-US",{timeZone:f,hour:"numeric",minute:"2-digit",second:"2-digit"}).format(x)}catch{return x.toLocaleTimeString()}},[x,f]);if(m.useEffect(()=>{const S=setInterval(()=>g(new Date),1e3);return()=>clearInterval(S)},[]),!h||p)return null;const y=zw(s);return r.jsx("div",{className:"pointer-events-auto fixed inset-0 z-50 flex items-center justify-center bg-black/85 px-0.5 py-0.5",children:r.jsxs("div",{className:"surface w-full max-w-md space-y-0.5 text-slate-100 shadow-2xl",children:[r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-lg font-semibold",children:y.title}),r.jsx("p",{className:"text-sm text-slate-300",children:y.description})]}),r.jsxs("div",{className:"surface-muted space-y-0.5",children:[r.jsx("p",{className:"text-[0.7rem] tracking-wide text-slate-400",children:"Reason for locking:"}),r.jsx("p",{className:"text-lg font-semibold text-slate-100",children:o||"No reason set."}),r.jsxs("p",{className:"text-center text-sm text-slate-300",children:["Server time: ",v]})]}),r.jsx("div",{className:"surface-muted",children:r.jsx(Vg,{})}),r.jsx("div",{className:"w-full justify-center items-center",children:r.jsx(hd,{id:"discord",label:"Join our Discord server for updates!",url:d})}),"You can still use the chat while the server is locked:",r.jsxs("div",{className:"max-h-80 overflow-y-auto",children:[r.jsx(qo,{}),r.jsx(To,{})]})]})})}const Hw="/assets/turn_alert-DVO8EZJa.mp3";function Vw(n,s=1){const l=m.useRef(null);return m.useEffect(()=>{l.current=new Audio(n),l.current.volume=s,l.current.load()},[n,s]),()=>{l.current&&(l.current.volume=s,l.current.currentTime=0,l.current.play().catch(()=>{}))}}function $w(){const n=it(v=>v.session),{pushAlert:s}=kn(),{value:l}=Ge("audio",st),o=Number.isFinite(l?.masterVolume)?l.masterVolume:st.masterVolume,f=Number.isFinite(l?.alertVolume)?l.alertVolume:st.alertVolume,d=Math.max(0,Math.min(1,o*f)),h=Vw(Hw,d),p=m.useRef(new Set),x=m.useMemo(()=>n?.turnQueues||{},[n?.turnQueues]),g=n?.socketId||null;return m.useEffect(()=>{if(!g)return;const v=[];Object.entries(x).forEach(([y,S])=>{!S||!S.current||S.current!==g||p.current.has(y)||(v.push(y),p.current.add(y))}),Object.keys(x).forEach(y=>{(!x[y]||x[y].current!==g)&&p.current.delete(y)}),v.length!==0&&(v.forEach(y=>{const S=n?.roster?.find(E=>String(E.id)===String(y))?.name||y;s({title:"Your turn!",message:`You now control ${S}.`,color:"#ff5722"})}),h())},[x,h,s,n?.roster,g]),null}function Gw({visible:n,mode:s,onEnterFullscreen:l,onDismiss:o}){if(!n)return null;const f=s==="pwa-hint";return r.jsx("div",{className:"fixed inset-0 z-40 flex items-end justify-center p-2 pointer-events-none sm:items-center",children:r.jsx("div",{className:"pointer-events-auto w-full max-w-sm rounded-lg border border-cyan-500/40 bg-zinc-950/95 shadow-xl",children:r.jsxs("div",{className:"space-y-0.5 p-4 text-sm text-slate-100",children:[r.jsx("h2",{className:"text-base font-semibold text-white",children:"Better in fullscreen"}),f?r.jsxs("p",{className:"text-slate-300",children:["For fullscreen on iOS, open Safari's share menu and pick ",r.jsx("strong",{children:"Add to Home Screen"}),". Launching from the home screen removes the browser chrome."]}):r.jsx("p",{className:"text-slate-300",children:"Enable fullscreen to free up more space for the video feed and controls. You can exit fullscreen at any time via the system back or home gesture."}),r.jsxs("div",{className:"flex justify-end gap-0.5 pt-1 text-sm",children:[r.jsx("button",{type:"button",className:"rounded border border-slate-600 px-3 py-1 text-slate-200",onClick:o,children:f?"Got it":"Not now"}),!f&&r.jsx("button",{type:"button",className:"rounded bg-cyan-500 px-3 py-1 font-semibold text-black hover:bg-cyan-400",onClick:l,children:"Enter fullscreen"})]})]})})})}function Yw({side:n="right",onClick:s}){const l=n==="left"?"left-2":"right-2";return r.jsx("button",{type:"button",onClick:s,className:`fixed bottom-2 ${l} z-30 flex h-10 w-10 items-center justify-center rounded-full border border-cyan-200/50 bg-slate-900/70 text-cyan-100 shadow-lg backdrop-blur-sm transition hover:bg-slate-800/80 active:scale-95`,"aria-label":"Enter fullscreen",title:"Enter fullscreen",children:r.jsx("svg",{viewBox:"0 0 24 24",width:"18",height:"18","aria-hidden":"true",className:"pointer-events-none",children:r.jsx("path",{d:"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})})}const Pg="fullscreenPromptDismissed",Fw=new Set(["mobile-portrait","mobile-landscape"]),Wg=()=>{if(typeof window>"u")return null;try{return window.sessionStorage}catch{return null}},Xw=()=>{const n=Wg();if(!n)return!1;try{return n.getItem(Pg)==="1"}catch{return!1}},wx=()=>{const n=Wg();if(n)try{n.setItem(Pg,"1")}catch{}},Qw=()=>{if(typeof navigator>"u")return!1;const n=navigator.userAgent||navigator.platform||"";return/iphone|ipad|ipod/i.test(n)},Cx=()=>{if(typeof document>"u")return!1;const n=document;return!!(n.fullscreenEnabled||n.webkitFullscreenEnabled||n.mozFullScreenEnabled||n.msFullscreenEnabled)};function Kw(n){const s=Fw.has(n),[l,o]=m.useState(!1),[f,d]=m.useState("native"),[h,p]=m.useState(!1),x=m.useMemo(()=>Qw(),[]),g=m.useMemo(()=>Cx(),[]),v=m.useCallback(()=>{if(!s){o(!1),p(!1);return}if(!h&&Xw()){o(!1);return}if(x){d("pwa-hint"),o(!0);return}if(!Cx()){o(!1);return}if(typeof document<"u"&&document.fullscreenElement){o(!1);return}d("native"),o(!0)},[h,x,s]);m.useEffect(()=>{v()},[v]),m.useEffect(()=>{if(typeof document>"u")return;const C=()=>{if(document.fullscreenElement){p(!1),o(!1);return}v()};return document.addEventListener("fullscreenchange",C),document.addEventListener("webkitfullscreenchange",C),()=>{document.removeEventListener("fullscreenchange",C),document.removeEventListener("webkitfullscreenchange",C)}},[v]);const y=m.useCallback(()=>{wx(),p(!1),o(!1)},[]),S=m.useCallback(async()=>{if(!s||x||typeof document>"u")return!1;if(document.fullscreenElement)return!0;const C=document.documentElement;if(!C)return!1;const M=C.requestFullscreen||C.webkitRequestFullscreen||C.mozRequestFullScreen||C.msRequestFullscreen;if(!M)return!1;try{const w=M.call(C);return w&&typeof w.then=="function"&&await w,wx(),p(!1),o(!1),!0}catch(w){return console.warn("Failed to enter fullscreen",w),!1}},[x,s]),E=m.useCallback(()=>{s&&(p(!0),d(x?"pwa-hint":"native"),o(!0))},[x,s]);return m.useEffect(()=>{s||(p(!1),o(!1))},[s]),{visible:l,mode:f,isIOS:x,nativeSupported:g,enterFullscreen:S,dismiss:y,showPrompt:E}}function Zw({visible:n,layout:s,onClose:l,showOnLoad:o,onToggleShowOnLoad:f}){if(!n)return null;const{state:d}=_t(),h=p=>{const x=!p.target.checked;f?.(x)};return r.jsx("div",{className:"fixed inset-0 z-30 flex items-center justify-center bg-black/80 px-0.5 py-0.5",children:r.jsxs("div",{className:"pointer-events-auto surface w-full max-w-4xl max-h-[90vh] overflow-hidden shadow-2xl",children:[r.jsxs("div",{className:"flex items-center justify-between border-b border-slate-700 px-0.5 py-0.25 text-sm text-slate-200",children:[r.jsx("span",{className:"font-semibold",children:"Help & controls"}),r.jsxs("div",{className:"flex items-center gap-0.5 text-[0.8rem] text-slate-300",children:[r.jsxs("label",{className:"flex items-center gap-0.5",children:[r.jsx("input",{type:"checkbox",checked:!o,onChange:h,className:"accent-cyan-500"}),r.jsx("span",{children:"Don't show again"})]}),r.jsx("button",{type:"button",onClick:l,className:"button-dark px-2 py-0.25 text-[0.8rem]",children:"Close"})]})]}),r.jsx("div",{className:"max-h-[85vh] overflow-y-auto p-0.5",children:r.jsx(Qg,{layout:s,keymap:d?.keymap||{}})})]})})}const Iw=`aroma +`).trim();if(ne){N(!0);try{await p(ne,Z),w(""),y(),S(!1)}catch(se){alert(se.message)}finally{N(!1)}}}const ce=l?"flex-1 min-h-0 overflow-y-auto":"h-48 overflow-y-auto";return r.jsxs("section",{className:`panel-section space-y-0.5 text-base ${l?"flex h-full flex-col overflow-hidden":""}`,children:[r.jsxs("div",{className:`surface overflow-y-auto space-y-0.5 px-0 ${ce}`,ref:ee,children:[R.length===0&&L.length===0?r.jsx("p",{className:"text-sm text-slate-500",children:"No messages yet."}):R.map(q=>r.jsx(ld,{message:q},q.id)),L.map(q=>r.jsx(pg,{message:q},`typing-${q.typingId||q.id}`))]}),!n&&r.jsxs("form",{className:"flex flex-wrap items-stretch gap-0.5",onSubmit:ae,children:[r.jsx("input",{className:"field-input flex-1 min-w-[10rem]",value:M,onChange:q=>{const $=q.target.value;w($),S(!!$.trim())},onFocus:q=>{g(q),S(!!M.trim())},onBlur:q=>{v(q),S(!1)},onKeyDown:q=>{q.key==="Enter"&&!M.trim()&&(q.preventDefault(),y(),S(!1))},ref:q=>x(q,{target:"panel"}),placeholder:V?"Type a message…":s?"":"Spectators cannot chat",disabled:!V}),H&&r.jsxs("div",{className:"flex flex-wrap items-center gap-0.5 basis-full sm:basis-auto",children:[r.jsxs("label",{className:"flex items-center gap-0.5 text-xs text-slate-300",children:[r.jsx("input",{type:"checkbox",checked:T,onChange:q=>k(q.target.checked),className:"accent-cyan-500"}),r.jsx("span",{children:"Speak"})]}),r.jsxs("select",{value:O,onChange:q=>{const $=q.target.value;Q($),C(ne=>({...ne||{},engine:$}))},className:"field-input text-xs",children:[r.jsx("option",{value:"flite",children:"flite"}),r.jsx("option",{value:"espeak",children:"espeak"})]}),O==="flite"?r.jsx("select",{value:D,onChange:q=>{const $=q.target.value;U($),C(ne=>({...ne||{},voice:$}))},className:"field-input text-xs",children:WN.map(q=>r.jsx("option",{value:q,children:q},q))}):r.jsx("select",{value:K,onChange:q=>{const $=Number(q.target.value);F($),C(ne=>({...ne||{},pitch:$}))},className:"field-input text-xs",children:ew.map(q=>r.jsxs("option",{value:q,children:["pitch ",q]},q))})]}),r.jsx("button",{type:"submit",disabled:!V||A,className:"button-dark h-full disabled:opacity-50 self-stretch",children:A?"...":"Send"})]})]})}function To({compact:n=!1}){const{session:s,setNickname:l}=nn(),{value:o,save:f}=Ge("profile",{nickname:""}),[d,h]=m.useState(o.nickname||""),[p,x]=m.useState(!1),g=s?.role!=="spectator";m.useEffect(()=>{h(o.nickname||"")},[o.nickname]);async function v(y){if(y.preventDefault(),!g)return;const S=(d||"").trim().slice(0,32);if(S){x(!0);try{await l(S),f({nickname:S})}catch(E){alert(E.message)}finally{x(!1)}}}return r.jsxs("form",{className:"grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-stretch gap-0.5",onSubmit:v,children:[r.jsx("input",{className:"field-input flex-1 min-w-0",value:d,onChange:y=>h(y.target.value),maxLength:32,placeholder:"Enter a nickname",disabled:!g}),r.jsx("button",{type:"submit",disabled:!g||p,className:"button-dark h-full shrink-0 whitespace-nowrap px-0.5 py-0 disabled:opacity-50",children:p?"Saving…":n?"Set":"Save"})]})}function tw(n){const s=Array.isArray(n?.socials)?n.socials:null;if(s&&s.length)return s;const l=[];return n?.discord?.invite&&l.push({id:"discord",label:"Discord",url:n.discord.invite}),n?.kofi?.link&&l.push({id:"kofi",label:"Ko-fi",url:n.kofi.link}),l}function nw(n){if(!n||typeof n!="object")return null;const s=n.id||n.service||n.key||n.name||null,l=n.label||n.title||n.name||s||"Link",o=n.url||n.link||n.href||null;return o?{id:s,label:l,url:o}:null}function Zg({className:n=""}){const{session:s}=nn(),l=tw(s).map(nw).filter(Boolean).slice(0,4);return l.length?r.jsx("div",{className:`grid grid-cols-2 grid-rows-2 gap-0.5 ${n}`,children:l.map(o=>r.jsx(hd,{...o},`${o.id||o.label}-${o.url}`))}):null}function aw({compact:n=!1}){return r.jsx("section",{className:"panel-section flex h-full min-h-0 flex-col gap-0.5 text-base",children:r.jsx("div",{className:"surface flex w-full items-center px-0 py-0",children:r.jsx(To,{compact:n})})})}function sw(){return r.jsx("section",{className:"panel-section flex h-full min-h-0 flex-col gap-0.5 text-base",children:r.jsx(Zg,{className:"flex-1 min-h-0"})})}function yf(...n){return n.filter(Boolean).join(" ")}function lw(n){const s=n?.batteryState?.percentDisplay;return s==null?"--":`${s}%`}function iw(n){return n?.batteryState?n.batteryState.urgentActive?"text-red-400":n.batteryState.warnActive?"text-amber-300":"text-emerald-300":"text-slate-400"}function rw(n){switch(n){case"admin":case"lockdown":case"lockdown-admin":return"text-amber-300";case"spectator":return"text-slate-400";default:return"text-sky-300"}}function ow(n,s){if(!n)return"";const l=n.nickname||n.socketId?.slice(0,6)||"unknown";return n.socketId&&n.socketId===s?`${l} (you)`:l}function Do({title:n="Rovers"}){const s=it(A=>A.session),{requestControl:l}=kn(),[o,f]=m.useState({}),[d,h]=m.useState(()=>Date.now()),p=m.useMemo(()=>s?.role&&s.role!=="spectator",[s?.role]),x=m.useMemo(()=>s?.role==="admin"||s?.role==="lockdown"||s?.role==="lockdown-admin",[s?.role]),g=s?.roster??[],v=s?.turnQueues??{},y=s?.users??[],S=s?.socketId||null,E=m.useMemo(()=>Object.values(v||{}).some(A=>A?.deadline||A?.idleDeadline),[v]);m.useEffect(()=>{if(!E)return;const A=setInterval(()=>{h(Date.now())},1e3);return()=>clearInterval(A)},[E]);const C=m.useMemo(()=>{const A=new Set(g.map(T=>String(T.id))),N=Object.keys(v||{}).filter(T=>!A.has(String(T))).map(T=>({id:T,name:T,locked:!1,batteryState:null}));return[...g,...N]},[g,v]);async function M(A){if(A){f(N=>({...N,[A]:!0}));try{await l(A)}catch(N){alert(N.message)}finally{f(N=>({...N,[A]:!1}))}}}const w=A=>y.find(N=>N.socketId===A)||{socketId:A,nickname:null,role:null};return r.jsxs("div",{className:"space-y-0.5 text-sm",children:[n&&r.jsx("p",{className:"text-sm text-slate-400",children:n}),C.length===0?r.jsx("p",{className:"text-sm text-slate-500",children:"No rovers registered."}):r.jsx("ul",{className:"space-y-0.5 text-sm",children:C.map(A=>{const N=String(A.id),T=v?.[N]||null,k=T?.queue||[],O=T?.idleDeadline||T?.deadline||null,Q=O&&O>d?Math.ceil((O-d)/1e3):O?0:null,D=T?.current||null,U=D?k.findIndex(ae=>ae===D):-1,K=k.length>1?U>=0?k[(U+1)%k.length]:k[0]:null,F=!!(S&&D&&D===S),V=!!(S&&K&&K===S),ee=Q!=null&&(F||V),oe=!!A.locked,ue=oe&&!x,H=!!(A?.private?.enabled&&A?.private?.open),R=A.lockReason?`locked: ${A.lockReason}`:"locked",L=oe?R:o[N]?"...":"request",Z=p&&!ue&&!o[N];return r.jsxs("li",{className:yf("surface flex flex-wrap items-start justify-between gap-0.5",Z&&"cursor-pointer",oe?"bg-red-900/40":H?"bg-amber-700/35 border border-amber-200/30":null),onClick:()=>{Z&&M(A.id)},children:[r.jsxs("div",{className:"flex min-w-0 flex-1 flex-col gap-0.5",children:[r.jsxs("div",{className:"flex items-center justify-between gap-0.5",children:[r.jsxs("div",{className:"flex items-center gap-0.5",children:[r.jsx("p",{className:"text-slate-200",children:r.jsx("span",{className:"rounded px-1 py-[1px] border border-transparent",style:oa(A.color,.16),children:A.name})}),ee?r.jsx("span",{className:"rounded bg-slate-800 px-1 text-[0.7rem] text-slate-200",children:F?`${Q}s left`:`Your turn in ${Q}s`}):null]}),r.jsx("span",{className:yf("text-[0.75rem] font-semibold",iw(A)),children:lw(A)})]}),k.length===0?r.jsx("p",{className:"text-[0.7rem] text-slate-500",children:"No queue."}):r.jsx("div",{className:"flex flex-wrap items-center gap-0.5",children:k.map((ae,ce)=>{const q=w(ae),$=ae===D,ne=!!(K&&ae===K&&!$),se=$?"bg-sky-600 text-white ring-2 ring-amber-300":ne?"bg-emerald-700/60 text-emerald-100 ring-1 ring-emerald-300/70":"bg-slate-800 text-slate-200";return r.jsxs("span",{className:`flex items-center gap-0.5 rounded px-1 text-[0.7rem] ${se}`,children:[r.jsx("span",{className:`${rw(q.role)} font-semibold`,children:ow(q,S)}),$&&r.jsx("span",{className:"text-[0.65rem] text-slate-200",children:"now"}),ne&&r.jsx("span",{className:"text-[0.65rem] text-emerald-100",children:"next"})]},`${N}-${ae}-${ce}`)})})]}),p?r.jsx("button",{type:"button",onClick:ae=>{ae.stopPropagation(),M(A.id)},disabled:o[N]||ue,className:yf("button-dark disabled:opacity-40",oe&&"bg-red-600/70 text-white hover:bg-red-600"),children:L}):null]},A.id)})})]})}function cw(n){switch(n){case"admin":case"lockdown":case"lockdown-admin":return"text-amber-300";case"spectator":return"text-slate-400";default:return"text-sky-300"}}function uw(n,s){if(!n)return"";const l=n.nickname||n.socketId?.slice(0,6)||"unknown";return n.socketId&&n.socketId===s?`${l} (you)`:l}function So({hideNicknameForm:n=!1,hideHeader:s=!1,className:l="",fillHeight:o=!1,compact:f=!1}){const{session:d}=nn(),h=d?.role!=="spectator",p=d?.users??[],x=d?.socketId||null,g=m.useMemo(()=>[...p].sort((y,S)=>y.socketId===x?-1:S.socketId===x?1:(y.nickname||"").localeCompare(S.nickname||"")),[x,p]),v=o?"flex-1 min-h-0 overflow-y-auto":f?"h-28 overflow-y-auto":"h-48 overflow-y-auto";return r.jsxs("section",{className:`panel-section space-y-0.5 text-base ${o?"flex h-full min-h-0 flex-col overflow-hidden":""} ${l}`,children:[!n&&r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]",children:[r.jsx("div",{className:"min-w-0",children:r.jsx("div",{className:"surface flex w-full items-center px-0 py-0",children:r.jsx(To,{compact:f})})}),r.jsx(Zg,{})]}),!h&&r.jsx("p",{className:"text-xs text-slate-500",children:"Spectators cannot set nicknames."})]}),r.jsxs("div",{className:`space-y-0.5 ${o?"flex flex-1 min-h-0 flex-col":""}`,children:[!s&&r.jsxs("div",{className:`flex items-center justify-between text-sm text-slate-400 ${f?"text-xs":""}`,children:[r.jsx("span",{children:"Users"}),r.jsx("span",{className:"text-xs text-slate-500",children:g.length})]}),r.jsx("div",{className:`surface flex flex-wrap content-start items-start gap-0.5 px-0 pb-0 ${v} ${f?"text-[0.8rem]":""}`,children:g.length===0?r.jsx("p",{className:"text-sm text-slate-500",children:"Waiting for users…"}):g.map(y=>r.jsx("span",{className:`rounded bg-slate-800/80 px-1 py-0.25 text-[0.7rem] ${cw(y.role)}`,children:uw(y,x)},y.socketId))})]})]})}const fw=/^cu_[a-f0-9]{32}$/,wi="field-input w-full max-w-sm text-left focus:ring-emerald-500",_o="mx-auto w-full max-w-xl flex justify-center",xi="mx-auto flex w-full max-w-md flex-col items-center space-y-0.5 text-center";function dw(n){const s=String(n||"").trim();return s?s.length<=10?`${s.slice(0,2)}***${s.slice(-2)}`:`${s.slice(0,6)}...${s.slice(-6)}`:""}const Sx=8*1024*1024,ss=16e3,mw={iceServers:[{urls:"stun:stun.l.google.com:19302"}],bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"};function jx(n){let s="";for(let o=0;o{if(!n||n.iceGatheringState==="complete"){l();return}const o=setTimeout(()=>{n.removeEventListener("icegatheringstatechange",f),l()},s);function f(){n.iceGatheringState==="complete"&&(clearTimeout(o),n.removeEventListener("icegatheringstatechange",f),l())}n.addEventListener("icegatheringstatechange",f)})}function Ax(n){if(!n)return!1;const s=n.connectionState,l=n.iceConnectionState;return s==="connected"||l==="connected"||l==="completed"}function gw(n,s=1e4){return new Promise((l,o)=>{if(!n){o(new Error("Peer connection missing"));return}if(Ax(n)){l();return}const f=setTimeout(()=>{h(),o(new Error("Peer connection timeout"))},s),d=()=>{Ax(n)?(h(),l()):(n.connectionState==="failed"||n.connectionState==="closed"||n.iceConnectionState==="failed")&&(h(),o(new Error(`Peer connection ${n.connectionState||n.iceConnectionState}`)))};function h(){clearTimeout(f),n.removeEventListener("connectionstatechange",d),n.removeEventListener("iceconnectionstatechange",d)}n.addEventListener("connectionstatechange",d),n.addEventListener("iceconnectionstatechange",d)})}async function vw(n){if(!n?.getParameters||!n?.setParameters)return;const s=n.getParameters()||{},l=s.encodings&&s.encodings[0]||{};s.encodings=[{...l,maxBitrate:64e3,dtx:"disabled"}];try{await n.setParameters(s)}catch{}}function bw(n,s=6e3){return new Promise((l,o)=>{if(!n){o(new Error("Peer connection missing"));return}const f=Date.now();let d=-1;const h=setInterval(async()=>{if(Date.now()-f>s){clearInterval(h),o(new Error("WHIP connected but no outbound audio flow"));return}try{const p=n.getSenders().filter(x=>x.track?.kind==="audio");for(const x of p){const g=await x.getStats();for(const v of g.values()){if(v.type!=="outbound-rtp"||v.kind!=="audio")continue;const y=Number(v.bytesSent||0),S=Number(v.packetsSent||0);if(d<0)d=y;else if(y>d+200||S>5){clearInterval(h),l();return}}}}catch{}},250)})}function yw(n){const s=n.reduce((f,d)=>f+d.length,0),l=new Float32Array(s);let o=0;for(const f of n)l.set(f,o),o+=f.length;return l}function Sw(n){const s=new Int16Array(n.length);for(let l=0;l{for(let v=0;vv&&s?.[v]||null,[s,v]),ne=!!($&&$.state!=="offline"&&!$.error),se=$?.source==="upload"&&$?.state==="playing",pe=$?.source==="mic-whip"&&($?.state==="starting"||$?.state==="playing"),re=!!(!A&&K.current&&(M||q)),fe=!A&&(N==="live"||N==="starting"),be=A&&k==="recording",ve=A&&k==="sending",xe=Cn(p?.keymap?.micPtt?.[0])||"M",Le=m.useCallback(J=>{const he=J==="clip"?"clip":"live";g(Ae=>({...Ae||{},pttMode:he}))},[g]),De=async()=>{if(!v){D("Take control of your rover first.");return}if(!y){D("Select an audio file first.");return}if(y.size>Sx){D(`File too large (max ${Sx} bytes).`);return}C(!0),D("");try{const J=await y.arrayBuffer(),he=jx(new Uint8Array(J));await l?.({roverId:v,name:y.name,mime:y.type||"",dataBase64:he}),D("Upload playback started.")}catch(J){D(J?.message||"Failed to play upload.")}finally{C(!1)}},Ee=async()=>{if(!v){D("Take control of your rover first.");return}C(!0),D("");try{await o?.(v),D("Upload playback stopped.")}catch(J){D(J?.message||"Failed to stop upload.")}finally{C(!1)}},nt=m.useCallback(async J=>{const he=String(J||ee.current||"").trim();if(V.current=!1,T("idle"),F.current){try{F.current.getSenders().forEach(Ae=>Ae.track?.stop())}catch{}try{F.current.close()}catch{}}if(F.current=null,U.current)try{U.current.getTracks().forEach(Ae=>Ae.stop())}catch{}if(U.current=null,K.current=null,he)try{await h?.(he)}catch{}ee.current=""},[h]),Ve=m.useCallback(async J=>{const he=await f?.(J),Ae=String(he?.whipUrl||"").trim(),ht=String(he?.token||"").trim();if(!Ae||!ht)throw new Error("WHIP endpoint unavailable");const Oe=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:ss,echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1}});U.current=Oe;const rt=Oe.getAudioTracks()?.[0];if(K.current=rt||null,rt&&(rt.enabled=!!(M||q)),rt?.applyConstraints)try{await rt.applyConstraints({channelCount:1,sampleRate:ss,echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1})}catch{}const ot=new RTCPeerConnection(mw);F.current=ot,ot.onconnectionstatechange=()=>{if(!V.current)return;const Lt=ot.connectionState;if(Lt==="connected"){T("live");return}(Lt==="failed"||Lt==="disconnected"||Lt==="closed")&&(T("error"),D(`WHIP transport ${Lt}.`))},Oe.getAudioTracks().forEach(Lt=>{const fl=ot.addTrack(Lt,Oe);vw(fl)});const gt=await ot.createOffer({offerToReceiveAudio:!1,offerToReceiveVideo:!1});await ot.setLocalDescription(gt),await xw(ot,1800);const kt=await fetch(Ae,{method:"POST",headers:{"Content-Type":"application/sdp",...pw(ht)},body:ot.localDescription?.sdp||gt.sdp});if(!kt.ok)throw new Error(`WHIP request failed: ${kt.status}`);const fs=await kt.text();await ot.setRemoteDescription({type:"answer",sdp:fs}),await gw(ot,1e4),await bw(ot,6e3),await d?.(J)},[M,q,d,f]),mt=m.useCallback(async()=>{if(ae.current=!1,R.current){try{R.current.disconnect()}catch{}R.current.onaudioprocess=null}if(H.current)try{H.current.disconnect()}catch{}if(L.current)try{L.current.disconnect()}catch{}if(oe.current)try{oe.current.getTracks().forEach(J=>J.stop())}catch{}if(ue.current)try{await ue.current.close()}catch{}Z.current=[],R.current=null,H.current=null,L.current=null,oe.current=null,ue.current=null,ce.current=ss},[]),ie=m.useCallback(async()=>{if(oe.current&&ue.current&&R.current)return;O(rt=>rt==="recording"?rt:"arming");const J=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,sampleRate:ss,echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1}}),he=new AudioContext({latencyHint:"interactive"}),Ae=he.createMediaStreamSource(J),ht=he.createScriptProcessor(1024,1,1),Oe=he.createGain();Oe.gain.value=0,ht.onaudioprocess=rt=>{if(!ae.current)return;const ot=rt.inputBuffer.getChannelData(0);Z.current.push(new Float32Array(ot))},Ae.connect(ht),ht.connect(Oe),Oe.connect(he.destination),oe.current=J,ue.current=he,H.current=Ae,R.current=ht,L.current=Oe,ce.current=he.sampleRate||ss},[]),me=m.useCallback(async({send:J})=>{const he=ae.current;if(ae.current=!1,!he){J||O("idle");return}const Ae=Z.current;if(Z.current=[],!J){O("idle");return}if(!v){O("idle");return}if(!Ae.length){O("idle");return}O("sending");try{const ht=yw(Ae),Oe=jw(ht,ce.current||ss),rt=jx(Oe);await l?.({roverId:v,name:`ptt-${Date.now()}.wav`,mime:"audio/wav",dataBase64:rt}),O("idle")}catch(ht){O("error"),D(ht?.message||"Failed to send PTT clip.")}},[l,v]),le=m.useCallback(async()=>{if(!v){D("Take control of your rover first.");return}await ie(),Z.current=[],ae.current=!0,O("recording")},[ie,v]);return m.useEffect(()=>{let J=!1;async function he(){if(A){await nt(ee.current||v);return}if(!v){await nt(ee.current);return}if(!(V.current&&ee.current===v)&&!(!M&&!q))try{T("starting"),D(""),V.current=!0,ee.current=v,await Ve(v),J||T("live")}catch(Ae){J||(T("error"),D(Ae?.message||"Failed to start mic forwarding.")),await nt(v)}}return he(),()=>{J=!0}},[A,M,q,v,Ve,nt]),m.useEffect(()=>{const J=K.current;J&&(J.enabled=!!(!A&&(M||q)))},[A,M,q]),m.useEffect(()=>{let J=!1;async function he(){if(!A||!v){ae.current&&await me({send:!1}),O("idle");return}try{await ie(),!J&&!ae.current&&O("idle")}catch(Ae){J||(O("error"),D(Ae?.message||"Microphone access failed for clip mode."))}}return he(),()=>{J=!0}},[A,ie,me,v]),m.useEffect(()=>{let J=!1;async function he(){if(A){if(!v){ae.current&&await me({send:!1});return}if(q){if(!ae.current)try{D(""),await le()}catch(Ae){J||(O("error"),D(Ae?.message||"Failed to start clip recording."))}}else ae.current&&await me({send:!0})}}return he(),()=>{J=!0}},[A,me,q,v,le]),m.useEffect(()=>()=>{nt(ee.current||v),mt()},[v,nt,mt]),r.jsx("section",{className:"surface",children:r.jsxs("div",{className:"grid gap-1",children:[r.jsx("p",{className:"text-sm text-slate-100 text-center",children:"Audio Controls"}),r.jsx("section",{className:"surface",children:r.jsxs("div",{className:"flex items-center justify-center gap-0.5 py-0.25 text-xs text-slate-300",children:[r.jsx("span",{children:"Push-to-Talk Key"}),r.jsx(Nx,{label:xe})]})}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-1 lg:grid-cols-2",children:[r.jsx("section",{className:"surface h-full",children:r.jsxs("div",{className:"grid h-full gap-0.5 grid-rows-[auto_auto_1fr]",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"PTT Mode"}),r.jsx("p",{className:"text-xs text-slate-400 text-center",children:"Choose how holding your PTT key behaves: live mic stream, or record-and-send clip. If the live stream doesn't work, switch to clip mode."}),r.jsxs("div",{className:"grid grid-cols-2 gap-0.5",children:[r.jsx("button",{type:"button",className:`button-dark w-full text-sm ${A?"":"bg-emerald-500 text-white hover:bg-emerald-500"}`,onClick:()=>Le("live"),children:"Live Mic (WHIP)"}),r.jsx("button",{type:"button",className:`button-dark w-full text-sm ${A?"bg-emerald-500 text-white hover:bg-emerald-500":""}`,onClick:()=>Le("clip"),children:"Record Clip"})]})]})}),r.jsx("section",{className:"surface h-full",children:r.jsxs("div",{className:"grid h-full gap-0.5 grid-rows-[auto_1fr]",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"PTT Clip Status"}),r.jsxs("div",{className:"grid gap-0.5 content-start",children:[r.jsx(vn,{label:"Mode",active:A,detail:A?"record clip":"live mic"}),r.jsx(vn,{label:"PTT hold",active:q,detail:q?"held":"released"}),r.jsx(vn,{label:"Recording",active:be,detail:be?"capturing":"idle"}),r.jsx(vn,{label:"Playback",active:A&&se,detail:A&&se?"playing":"idle"})]})]})})]}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-1 lg:grid-cols-2",children:[r.jsx("section",{className:"surface h-full",children:r.jsxs("div",{className:"grid h-full gap-0.5 grid-rows-[auto_auto_1fr_auto]",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"Audio Upload"}),r.jsxs("label",{className:"mx-auto grid w-full max-w-sm gap-0.5 text-xs text-slate-300 text-center",children:[r.jsx("span",{children:"Audio file (mp3 / wav / ogg)"}),r.jsx("input",{className:`${wi} text-center`,type:"file",accept:".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg",disabled:E||!v,onChange:J=>S(J.target.files?.[0]||null)})]}),r.jsx("div",{className:"mx-auto w-full max-w-sm text-center",children:y?r.jsxs("div",{className:"surface-muted text-xs text-slate-300",children:[y.name," (",y.size," bytes)"]}):r.jsx("div",{className:"surface-muted text-xs text-slate-500",children:"No file selected"})}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:E||!v,onClick:De,children:E?"Working...":"Play Upload"}),r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:E||!v,onClick:Ee,children:"Stop"})]})]})}),r.jsx("section",{className:"surface h-full",children:r.jsxs("div",{className:"grid h-full gap-0.5 grid-rows-[auto_auto_1fr]",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"Live Microphone"}),r.jsxs("label",{className:"surface-muted mx-auto flex w-full max-w-sm items-center justify-center gap-0.5 px-0.5 py-0.5 text-xs text-slate-300",children:[r.jsx("input",{type:"checkbox",checked:M,disabled:!v||A,onChange:J=>g(he=>({...he||{},openMicEnabled:!!J.target.checked}))}),r.jsx("span",{children:A?"Open mic (live mode only)":"Open mic"})]}),r.jsxs("div",{className:"grid gap-0.5 content-start",children:[r.jsx(vn,{label:"WHIP link",active:fe,detail:N}),r.jsx(vn,{label:"Mic hot",active:re,detail:re?"transmitting":"muted"}),r.jsxs("div",{className:"surface-muted text-center text-xs text-slate-400",children:["Hold ",r.jsx(Nx,{label:xe})," to talk"]})]})]})})]}),r.jsx("section",{className:"surface",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-sm text-slate-200 text-center",children:"Audio Status"}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-6",children:[r.jsx(vn,{label:"Forward pipe",active:ne,detail:ne?"connected":"offline"}),r.jsx(vn,{label:"Upload playback",active:se,detail:se?"playing":"idle"}),r.jsx(vn,{label:"Mic relay",active:pe,detail:pe?"active":"idle"}),r.jsx(vn,{label:"WHIP transport",active:fe,detail:N}),r.jsx(vn,{label:"Clip recording",active:be,detail:be?"recording":"idle"}),r.jsx(vn,{label:"Clip sending",active:ve,detail:ve?"sending":"idle"})]}),Q?r.jsx("div",{className:"text-xs text-slate-400 text-center",children:Q}):null]})})]})})}function Nw({pendingRequestId:n,currentStoredKey:s,nickname:l,requestVerification:o,applyIdentityKey:f,onMessage:d,fullWidth:h=!1}){const[p,x]=m.useState(0),[g,v]=m.useState(""),[y,S]=m.useState(!1),[E,C]=m.useState(!1),M=()=>{x(1),v(s),S(!1),d?.("")},w=()=>{x(0),S(!1)},A=async T=>{if(T.preventDefault(),!y){d?.("Please confirm your nickname agreement before sending.");return}if(p<3){d?.("Complete all request steps before sending.");return}C(!0),d?.("");try{const k=await f(g);await o?.(),v(k),x(0),S(!1),d?.("Verification request sent to lockdown admins.")}catch(k){d?.(k.message||"Failed to submit request.")}finally{C(!1)}},N=h?"w-full":_o;return n?r.jsx("section",{className:`surface text-sm text-slate-300 ${N}`,children:r.jsxs("div",{className:xi,children:["Verification request pending: ",n]})}):p===0?r.jsx("section",{className:`surface ${N}`,children:r.jsxs("div",{className:xi,children:[r.jsx("p",{className:"text-sm text-slate-300",children:"Verification"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:M,disabled:E,children:"Request Verification"})]})}):r.jsx("form",{className:`surface ${N}`,onSubmit:A,children:r.jsxs("div",{className:xi,children:[r.jsx("p",{className:"text-sm text-slate-300",children:"Request verification"}),r.jsxs("p",{className:"text-xs text-slate-500",children:["Step ",p," of 3"]}),p===1?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Confirm your nickname, which is used as part of the verification process. You can edit it here before requesting."}),r.jsx("div",{className:"mx-auto w-full max-w-sm",children:r.jsx(To,{compact:!0})}),r.jsxs("div",{className:"surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center",children:["Current nickname: ",r.jsx("span",{className:"font-semibold",children:l||"(not set)"})]}),r.jsxs("label",{className:"flex items-center justify-center gap-0.5 text-xs text-slate-300 text-center",children:[r.jsx("input",{type:"checkbox",className:"accent-emerald-500",checked:y,onChange:T=>S(T.target.checked)}),r.jsx("span",{children:"I understand this nickname is tied to my verification."})]}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:!l||!y,onClick:()=>x(2),children:"Continue"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:w,children:"Cancel"})]})]}):null,p===2?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Save your identity key in a safe place. You can use it to restore your identity in another browser."}),r.jsx("input",{className:wi,type:"password",name:"identity_key_request",autoComplete:"current-password",maxLength:35,value:g,onChange:T=>v(T.target.value.toLowerCase()),placeholder:"Identity key for this request"}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:!g,onClick:()=>navigator.clipboard?.writeText(g),children:"Copy Key"}),r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:!g,onClick:()=>x(3),children:"Continue"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:()=>x(1),children:"Back"})]})]}):null,p===3?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Final step: confirm and send your verification request."}),r.jsx("input",{className:wi,type:"password",name:"identity_key_request_final",autoComplete:"current-password",maxLength:35,value:g,onChange:T=>v(T.target.value.toLowerCase()),placeholder:"Identity key for this request"}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"submit",className:"button-dark text-sm",disabled:E||!g,children:"Confirm Request"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:()=>x(2),children:"Back"})]})]}):null]})})}function ww({currentStoredKey:n,applyIdentityKey:s,onMessage:l,fullWidth:o=!1}){const[f,d]=m.useState(0),[h,p]=m.useState(""),[x,g]=m.useState(!1),v=()=>{d(1),p(""),l?.("")},y=()=>{d(0),p("")},S=async C=>{if(C.preventDefault(),!(f<2)){g(!0),l?.("");try{await s(h),d(0),p(""),l?.("Identity key restored.")}catch(M){l?.(M.message||"Failed to restore key.")}finally{g(!1)}}},E=o?"w-full":_o;return r.jsx("section",{className:`surface ${E}`,children:r.jsxs("div",{className:xi,children:[r.jsx("p",{className:"text-sm text-slate-300",children:"Identity key"}),r.jsxs("p",{className:"text-xs text-slate-500",children:["Current: ",dw(n)||"not set yet"]}),r.jsx("input",{className:wi,type:"password",name:"identity_key_current",autoComplete:"current-password",maxLength:35,value:n,readOnly:!0,placeholder:"Identity key"}),f===0?r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",disabled:!n,onClick:()=>navigator.clipboard?.writeText(n),children:"Copy Key"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:v,disabled:x,children:"Restore Key"})]}):null,f===1?r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-xs text-slate-400",children:"Restoring your key should only be done when needed. Use this to move your identity to another browser."}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:()=>d(2),children:"Continue"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:y,children:"Cancel"})]})]}):null,f===2?r.jsxs("form",{className:"space-y-0.5",onSubmit:S,children:[r.jsx("input",{className:wi,type:"password",name:"identity_key_restore",autoComplete:"current-password",maxLength:35,value:h,onChange:C=>p(C.target.value.toLowerCase()),placeholder:"Paste key to restore"}),r.jsxs("div",{className:"flex justify-center gap-0.5",children:[r.jsx("button",{type:"submit",className:"button-dark text-sm",disabled:x||!h,children:"Confirm Restore"}),r.jsx("button",{type:"button",className:"button-dark text-sm",onClick:()=>d(1),children:"Back"})]})]}):null]})})}function Cw({requestableRovers:n=[],pendingRequests:s=[],requestPrivateRoverAccess:l,onMessage:o,fullWidth:f=!1}){const[d,h]=m.useState({}),p=m.useMemo(()=>{const v=new Map;return(s||[]).forEach(y=>{y?.roverId&&v.set(String(y.roverId),y)}),v},[s]),x=f?"w-full":_o,g=async v=>{if(v){h(y=>({...y,[v]:!0})),o?.("");try{(await l?.(v))?.existing?o?.("You already have a pending request for that rover."):o?.("Private rover access request sent to lockdown admins.")}catch(y){o?.(y.message||"Failed to send request.")}finally{h(y=>({...y,[v]:!1}))}}};return r.jsx("section",{className:`surface text-sm text-slate-300 ${x}`,children:r.jsxs("div",{className:xi,children:[r.jsx("p",{className:"text-sm text-slate-300",children:"Private rover access requests"}),n.length===0?r.jsx("p",{className:"text-xs text-slate-500",children:"No closed private rovers are available to request right now."}):r.jsx("div",{className:"w-full space-y-0.5",children:n.map(v=>{const y=String(v.id),S=!!d[y],E=p.get(y);return r.jsxs("div",{className:"surface-muted flex items-center justify-between gap-0.5 px-1 py-0.5",children:[r.jsxs("div",{className:"min-w-0 text-left",children:[r.jsx("p",{className:"truncate text-xs font-semibold text-slate-100",children:v.name||y}),r.jsx("p",{className:"truncate text-[0.7rem] text-slate-500",children:y})]}),E?r.jsx("span",{className:"rounded bg-amber-700/30 px-1 py-0.5 text-[0.7rem] text-amber-200",children:"Pending"}):r.jsx("button",{type:"button",onClick:()=>g(y),disabled:S,className:"button-dark text-xs disabled:opacity-50",children:S?"Sending...":"Request"})]},y)})})]})})}function Vf(n){return String(n||"").trim()}function Ew(n){const s=Vf(n);return s?s.includes("DOCKINGRUNNING")?"Returning":s.includes("PAUSED")?"Paused":s.includes("HOUSECLEANINGRUNNING")?"Cleaning":s.includes("SPOTCLEANINGRUNNING")?"Spot cleaning":s.includes("STATE_START")?"Starting":s.includes("STATE_IDLE")||s.includes("STATE_STANDBY")?"Idle":s:"--"}function Ig(n){return n==="good"?"bg-emerald-600 text-white":n==="warn"?"bg-amber-500 text-slate-900":n==="danger"?"bg-rose-600 text-white":n==="info"?"bg-sky-600 text-white":"bg-slate-700 text-slate-100"}function ls({label:n,value:s,tone:l="muted",valueClass:o=""}){return r.jsxs("div",{className:`rounded-md px-1 py-0.75 text-center ${Ig(l)}`,children:[r.jsx("div",{className:"text-[0.72rem] opacity-90",children:n}),r.jsx("div",{className:`text-sm font-semibold ${o}`,title:String(s||""),children:s})]})}function kw({neato:n,onStart:s,onSendHome:l,onLocate:o,onClearErrors:f,fullWidth:d=!1}){const[h,p]=m.useState(""),x=d?"w-full":"w-full max-w-xl",g=!!n?.configured,v=!!(n?.enabled&&n?.connected),y=!!n?.telemetry?.extPowerPresent,S=!!n?.telemetry?.chargingActive,E=Ew(n?.telemetry?.uiState),C=n?.telemetry?.batteryPercent,M=Number.isFinite(C)?`${C}%`:"--",w=n?.telemetry?.batteryVoltage,A=Number.isFinite(w)?`${w.toFixed(2)} V`:"--",N=Vf(n?.telemetry?.robotError)||"--",T=Vf(n?.telemetry?.robotAlert)||"--",k=N!=="--"&&!/^no errors$/i.test(N)&&!/^200/.test(N),O=T!=="--"&&!/^200/.test(T),Q=n?.controls||{},D=!!Q?.start?.available,U=!!Q?.sendHome?.available,K=!!Q?.locate?.available,F=!!Q?.clearErrors?.available,V=m.useMemo(()=>y?{key:"start",label:"Start cleaning",pending:"Starting...",toneClass:"bg-emerald-600 hover:bg-emerald-500 text-white",canRun:D,fn:s}:{key:"home",label:"Send to dock",pending:"Sending...",toneClass:"bg-sky-600 hover:bg-sky-500 text-white",canRun:U,fn:l},[y,D,s,U,l]),ee=g&&v&&V.canRun,oe=g&&v&&K,ue=g&&v&&F,H=async(ce,q)=>{if(q){p(ce);try{await q()}catch{}finally{p("")}}},R=g?v?"Online":"Offline":"Not configured",L=g?v?"good":"danger":"warn",Z=C==null?"muted":C>=60?"good":C>=25?"warn":"danger",ae=y?"Docked":E!=="--"?E:"Away from dock";return r.jsx("section",{className:`surface text-sm text-slate-200 ${x}`,children:r.jsxs("div",{className:"grid gap-0.5",children:[r.jsxs("div",{className:"relative flex items-center justify-center min-h-[1.5rem]",children:[r.jsxs("div",{className:"text-center",children:[r.jsx("p",{className:"text-sm text-slate-100",children:"Neato Controls"}),r.jsx("p",{className:"text-xs text-slate-400",children:"Control the autonomous Neato robovac"})]}),r.jsx("span",{className:`absolute right-0 inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${Ig(L)}`,children:R})]}),r.jsx("section",{className:"surface-muted px-0.5 py-0.5",children:r.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-0.5",children:[r.jsxs("div",{className:"grid gap-0.5",children:[r.jsxs("div",{className:"rounded-md bg-slate-800 px-1 py-0.75 text-center",children:[r.jsx("div",{className:"text-xs text-slate-300",children:"State"}),r.jsx("div",{className:"text-base font-semibold text-slate-100",children:ae})]}),r.jsx("button",{type:"button",disabled:!ee||!!h,onClick:()=>H(V.key,V.fn),className:`rounded-md px-1 py-1 text-base font-semibold transition disabled:opacity-50 ${V.toneClass}`,children:h===V.key?V.pending:V.label})]}),r.jsx("button",{type:"button",disabled:!oe||!!h,onClick:()=>H("locate",o),className:"rounded-md bg-fuchsia-600 px-1 py-0.75 text-sm font-semibold text-white transition hover:bg-fuchsia-500 disabled:opacity-50",children:h==="locate"?"Playing...":"Play sound"}),r.jsx("button",{type:"button",disabled:!ue||!!h,onClick:()=>H("clearErrors",f),className:"rounded-md bg-amber-500 px-1 py-0.75 text-sm font-semibold text-slate-900 transition hover:bg-amber-400 disabled:opacity-50",children:h==="clearErrors"?"Clearing...":"Clear errors"})]})}),r.jsx("section",{className:"surface-muted px-0.5 py-0.5",children:r.jsxs("div",{className:"grid gap-0.5 grid-cols-2 sm:grid-cols-3 lg:grid-cols-7",children:[r.jsx(ls,{label:"Battery",value:M,tone:Z}),r.jsx(ls,{label:"Voltage",value:A,tone:w==null?"muted":"info"}),r.jsx(ls,{label:"Docked",value:y?"Yes":"No",tone:y?"info":"muted"}),r.jsx(ls,{label:"Charging",value:S?"Yes":"No",tone:S?"info":"muted"}),r.jsx(ls,{label:"UI state",value:E,tone:"muted",valueClass:"truncate"}),r.jsx(ls,{label:"Robot error",value:N,tone:k?"danger":"muted",valueClass:"truncate"}),r.jsx(ls,{label:"Robot alert",value:T,tone:O?"warn":"muted",valueClass:"truncate"})]})}),!g||!v||!V.canRun||!K||!F?r.jsx("p",{className:"text-xs text-slate-400 text-center",children:g?v?"Waiting for required Neato entities in Home Assistant.":"Home Assistant is offline.":"Set homeAssistant.neato.device in server config to enable Neato controls."}):null]})})}function Jg(){const{session:n,identifySession:s,requestVerification:l,requestPrivateRoverAccess:o,playUploadedAudio:f,stopUploadedAudio:d,startMicWhip:h,readyMicWhip:p,stopMicWhip:x,neatoStart:g,neatoSendHome:v,neatoLocate:y,neatoClearErrors:S}=nn(),{value:E,save:C}=Ge("identity",{cookieUserId:""}),{value:M}=Ge("profile",{nickname:""}),w=m.useMemo(()=>(E?.cookieUserId||"").trim(),[E?.cookieUserId]),A=(M?.nickname||"").trim(),N=!!n?.isVerified,T=n?.verification?.pendingRequestId||null,k=n?.privateRoverAccess?.requestableRovers||[],O=n?.privateRoverAccess?.pendingRequests||[],Q=String(n?.assignment?.roverId||"").trim(),[D,U]=m.useState(""),K=async F=>{const V=String(F||"").trim().toLowerCase();if(!V)throw new Error("Identity key required.");if(!fw.test(V))throw new Error("Identity key must match format: cu_ + 32 lowercase hex chars.");return C(ee=>({...ee||{},cookieUserId:V})),await s({cookieUserId:V,nickname:A}),V};return r.jsxs("section",{className:"space-y-2 text-base",children:[r.jsxs("div",{className:"grid gap-2 grid-cols-1 lg:grid-cols-[minmax(0,1.25fr)_minmax(0,0.9fr)]",children:[N?null:r.jsx("div",{className:"lg:col-span-2",children:r.jsx(Nw,{pendingRequestId:T,currentStoredKey:w,nickname:A,requestVerification:l,applyIdentityKey:K,onMessage:U,fullWidth:!0})}),r.jsx("div",{className:"lg:col-span-2",children:N?r.jsxs("div",{className:"space-y-2",children:[r.jsx(kw,{neato:n?.neato||null,onStart:g,onSendHome:v,onLocate:y,onClearErrors:S,fullWidth:!0}),r.jsx(Aw,{ownRoverId:Q,audioForwardByRover:n?.audioForward||{},playUploadedAudio:f,stopUploadedAudio:d,startMicWhip:h,readyMicWhip:p,stopMicWhip:x})]}):r.jsx("section",{className:"surface h-full",children:r.jsx("div",{className:"flex h-full flex-col items-center justify-center text-center text-xs text-slate-400",children:"Verify your account to unlock VIP controls."})})}),r.jsx("div",{className:"lg:col-span-2",children:r.jsx(Cw,{requestableRovers:k,pendingRequests:O,requestPrivateRoverAccess:o,onMessage:U,fullWidth:!0})}),r.jsx("div",{className:"lg:col-span-2",children:r.jsx(ww,{currentStoredKey:w,applyIdentityKey:K,onMessage:U,fullWidth:!0})})]}),D?r.jsx("div",{className:_o,children:r.jsx("p",{className:"text-xs text-slate-400 text-center",children:D})}):null]})}const Mw=420,Rw=1200,qw={1:262,2:330,3:392,4:523};function jo(){const{session:n}=nn(),s=tn(),{value:l}=Ge("audio",st),o=Number.isFinite(l?.masterVolume)?l.masterVolume:st.masterVolume,f=Number.isFinite(l?.alertVolume)?l.alertVolume:st.alertVolume,d=Math.max(0,Math.min(1,o*f)),h=m.useMemo(()=>{const M=Array.isArray(n?.buttonBox?.buttons)?n.buttonBox.buttons:[];return M.length===4?M:[1,2,3,4].map(w=>M.find(A=>Number(A?.id)===w)||{id:w,count:0,goal:0,rewardName:null,rewardId:null,rewardNumber:null,lastRewardAt:null})},[n?.buttonBox?.buttons]),[p,x]=m.useState({}),[g,v]=m.useState({}),y=m.useRef(new Map),S=m.useRef(new Map),E=m.useRef({}),C=m.useRef(null);return m.useEffect(()=>{const M={};h.forEach(A=>{M[A.id]=Number(A.lastRewardAt||0)});const w=E.current||{};h.forEach(A=>{const N=Number(A.id),T=M[N]||0,k=Number(w[N]||0);if(T>0&&T!==k){v(D=>({...D,[N]:!0}));const O=S.current.get(N);O&&clearTimeout(O);const Q=setTimeout(()=>{v(D=>({...D,[N]:!1})),S.current.delete(N)},Rw);S.current.set(N,Q)}}),E.current=M},[h]),m.useEffect(()=>{function M(A){const N=qw[A];if(!N||d<=0)return;let T=C.current;if(!T){const D=window.AudioContext||window.webkitAudioContext;if(!D)return;T=new D,C.current=T}const k=T.createOscillator(),O=T.createGain();k.type="square",k.frequency.value=N;const Q=.14*d;O.gain.setValueAtTime(1e-4,T.currentTime),O.gain.exponentialRampToValueAtTime(Math.max(1e-4,Q),T.currentTime+.01),O.gain.exponentialRampToValueAtTime(1e-4,T.currentTime+.2),k.connect(O),O.connect(T.destination),k.start(),k.stop(T.currentTime+.21)}function w(A={}){const N=Number(A.buttonId);if(!Number.isFinite(N)||N<1||N>4)return;x(O=>({...O,[N]:!0}));const T=y.current.get(N);T&&clearTimeout(T);const k=setTimeout(()=>{x(O=>({...O,[N]:!1})),y.current.delete(N)},Mw);y.current.set(N,k),M(N)}return s.on("buttonBox:increment",w),()=>{s.off("buttonBox:increment",w)}},[d,s]),r.jsxs("section",{className:"panel-section space-y-0.5 text-base",children:[r.jsx("header",{className:"panel-muted text-center text-sm",children:r.jsx("p",{children:"Button Box"})}),r.jsx("div",{className:"grid grid-cols-4 gap-0.5",children:h.map(M=>{const w=Number(M.id),A=Number.isFinite(M.count)?M.count:0,N=Number.isFinite(M.goal)?M.goal:0,T=typeof M.rewardName=="string"&&M.rewardName.trim()?M.rewardName.trim():"Unassigned",k=Number.isFinite(M.rewardNumber)?M.rewardNumber:"?",O=!!p[w],Q=!!g[w];return r.jsx(xg,{buttonId:w,count:A,goal:N,rewardNumber:k,rewardName:T,className:[O?"bg-cyan-900/45":"",Q?"bg-fuchsia-900/45":""].filter(Boolean).join(" ")},w)})})]})}function Tw(){const{state:{roverId:n}}=_t(),l=ul(n)?.sensors||{};return r.jsx("section",{className:"panel-section",children:r.jsx("div",{className:"aspect-square w-full",children:r.jsx(Hf,{sensors:l})})})}function Dw(){const{state:{roverId:n,keymap:s,camera:l,horn:o},pipeline:f,actions:{setServoAngle:d,setNightVision:h,startHorn:p,stopHorn:x}}=_t(),g=qg(n),v=g.docked&&!g.driving,y=l?.config,S=!!(n&&l?.enabled&&y),E=!!(n&&f?.nightVision),C=f?.nightVisionState,M=!!(n&&f?.horn),w=o?.overheated,A=typeof y?.minAngle=="number"?y.minAngle:-30,N=typeof y?.maxAngle=="number"?y.maxAngle:30,T=typeof l?.angle=="number"?l.angle:typeof y?.homeAngle=="number"?y.homeAngle:(A+N)/2,k=Cn(s?.nightVisionToggle?.[0]),O=Cn(s?.hornHonk?.[0]),Q=Cn(s?.cameraUp?.[0]),D=Cn(s?.cameraDown?.[0]);return r.jsxs("section",{className:"panel-section grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto] gap-0.5",children:[r.jsx(Tg,{layout:"desktop",expand:!0,driveDockState:g}),v?null:r.jsxs("div",{className:"surface space-y-0.5 p-0 text-sm text-slate-200",children:[E&&r.jsx(Dg,{nightVisionOn:C?.nightVisionOn,disabled:!n,onToggle:h,keyLabel:k}),M&&r.jsx(_g,{disabled:!n||w,onStart:p,onStop:x,keyLabel:O,active:o?.active,heat:o?.heat}),S&&r.jsx(Bg,{value:T,min:A,max:N,onChange:d,keyDownLabel:D,keyUpLabel:Q,className:"space-y-0.5 px-1 py-1",labelRowClass:"text-xs text-slate-300",labelClass:"",valueClass:"font-mono text-slate-100",sliderClass:"w-full",accentClass:"accent-emerald-400",endpointClass:"text-[0.7rem] text-slate-400"})]})]})}function _w({layout:n,onOpenHelpOverlay:s}){const l=it(E=>E.session),{state:o}=_t(),{value:f}=Ge("vipAudio",{openMicEnabled:!1,pttMode:"live"}),d=l?.isVerified?"bg-emerald-400":"bg-red-600",h=String(l?.assignment?.roverId||"").trim(),p=h?l?.audioForward?.[h]:null,x=!!o?.mic?.pttActive,g=!!f?.openMicEnabled,v=f?.pttMode==="clip"?"clip":"live",y=!!(h&&l?.isVerified&&(v==="clip"?x:g||x)),S=!!(h&&l?.isVerified&&v==="clip"&&p?.source==="upload"&&p?.state==="playing");return r.jsx("section",{className:"panel text-base",children:r.jsxs(yd,{defaultTab:"telemetry",children:[r.jsxs(Sd,{children:[r.jsx(Wt,{id:"telemetry",children:"Controls"}),r.jsx(Wt,{id:"vip",highlight:S?"green":y?"pink":"none",children:r.jsxs("span",{className:"inline-flex items-center gap-2",children:[r.jsx("span",{children:"VIP"}),r.jsx("span",{className:`inline-block h-3 w-3 rounded-full ${d}`,"aria-hidden":"true",title:l?.isVerified?"Verified":"Not verified"})]})}),r.jsx(Wt,{id:"help",children:"Help"}),r.jsx(Wt,{id:"settings",children:"Settings"})]}),r.jsxs(jd,{children:[r.jsx(en,{id:"telemetry",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsxs("div",{className:"grid items-stretch gap-0.5 grid-cols-[minmax(0,1.35fr)_minmax(0,0.95fr)]",children:[r.jsx(Tw,{}),r.jsx(Dw,{})]}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,0.9fr)_minmax(0,0.75fr)]",children:[r.jsx(Do,{}),r.jsx(ad,{panelId:"replay-sources-desktop",fillHeight:!0}),r.jsx(sw,{})]}),r.jsxs("div",{className:"grid items-stretch gap-0.5 grid-cols-[minmax(0,1.3fr)_minmax(0,0.22fr)] h-[14rem]",children:[r.jsx(qo,{fillHeight:!0}),r.jsxs("div",{className:"grid min-h-0 gap-0.5 grid-rows-[minmax(0,1fr)_auto]",children:[r.jsx(So,{compact:!0,hideNicknameForm:!0,fillHeight:!0}),r.jsx(aw,{compact:!0})]})]}),r.jsx(Hg,{}),r.jsx(jo,{}),r.jsx(fd,{defaultOrientation:"horizontal",panelId:"rightpane-telemetry"})]})}),r.jsx(en,{id:"vip",keepMounted:!0,children:r.jsx(Jg,{})}),r.jsx(en,{id:"help",children:r.jsx(Kg,{layout:n,onOpenOverlay:s})}),r.jsx(en,{id:"settings",children:r.jsx(Fg,{})})]})]})})}const Bw=new Set(["admin","lockdown","lockdown-admin"]),Ow=new Set(["lockdown","lockdown-admin"]),Lw=new Set(["admin","lockdown"]);function zw(n="admin"){return n==="lockdown"?{title:"Lockdown mode active",description:"Only the server owners can access the interface at this time."}:{title:"Admin mode active"}}function Uw(){const n=it(S=>S.session),s=n?.mode,l=n?.role,o=n?.adminReason?.text||"";n?.adminReason?.updatedAt;const f=n?.timezone||"UTC",d=n?.socials?.find(S=>String(S?.id||S?.label||"").toLowerCase()==="discord")?.url||n?.discord?.invite||null,h=Lw.has(s),p=s==="lockdown"?Ow.has(l):Bw.has(l),[x,g]=m.useState(()=>new Date),v=m.useMemo(()=>{try{return new Intl.DateTimeFormat("en-US",{timeZone:f,hour:"numeric",minute:"2-digit",second:"2-digit"}).format(x)}catch{return x.toLocaleTimeString()}},[x,f]);if(m.useEffect(()=>{const S=setInterval(()=>g(new Date),1e3);return()=>clearInterval(S)},[]),!h||p)return null;const y=zw(s);return r.jsx("div",{className:"pointer-events-auto fixed inset-0 z-50 flex items-center justify-center bg-black/85 px-0.5 py-0.5",children:r.jsxs("div",{className:"surface w-full max-w-md space-y-0.5 text-slate-100 shadow-2xl",children:[r.jsxs("div",{className:"space-y-0.5",children:[r.jsx("p",{className:"text-lg font-semibold",children:y.title}),r.jsx("p",{className:"text-sm text-slate-300",children:y.description})]}),r.jsxs("div",{className:"surface-muted space-y-0.5",children:[r.jsx("p",{className:"text-[0.7rem] tracking-wide text-slate-400",children:"Reason for locking:"}),r.jsx("p",{className:"text-lg font-semibold text-slate-100",children:o||"No reason set."}),r.jsxs("p",{className:"text-center text-sm text-slate-300",children:["Server time: ",v]})]}),r.jsx("div",{className:"surface-muted",children:r.jsx(Vg,{})}),r.jsx("div",{className:"w-full justify-center items-center",children:r.jsx(hd,{id:"discord",label:"Join our Discord server for updates!",url:d})}),"You can still use the chat while the server is locked:",r.jsxs("div",{className:"max-h-80 overflow-y-auto",children:[r.jsx(qo,{}),r.jsx(To,{})]})]})})}const Hw="/assets/turn_alert-DVO8EZJa.mp3";function Vw(n,s=1){const l=m.useRef(null);return m.useEffect(()=>{l.current=new Audio(n),l.current.volume=s,l.current.load()},[n,s]),()=>{l.current&&(l.current.volume=s,l.current.currentTime=0,l.current.play().catch(()=>{}))}}function $w(){const n=it(v=>v.session),{pushAlert:s}=kn(),{value:l}=Ge("audio",st),o=Number.isFinite(l?.masterVolume)?l.masterVolume:st.masterVolume,f=Number.isFinite(l?.alertVolume)?l.alertVolume:st.alertVolume,d=Math.max(0,Math.min(1,o*f)),h=Vw(Hw,d),p=m.useRef(new Set),x=m.useMemo(()=>n?.turnQueues||{},[n?.turnQueues]),g=n?.socketId||null;return m.useEffect(()=>{if(!g)return;const v=[];Object.entries(x).forEach(([y,S])=>{!S||!S.current||S.current!==g||p.current.has(y)||(v.push(y),p.current.add(y))}),Object.keys(x).forEach(y=>{(!x[y]||x[y].current!==g)&&p.current.delete(y)}),v.length!==0&&(v.forEach(y=>{const S=n?.roster?.find(E=>String(E.id)===String(y))?.name||y;s({title:"Your turn!",message:`You now control ${S}.`,color:"#ff5722"})}),h())},[x,h,s,n?.roster,g]),null}function Gw({visible:n,mode:s,onEnterFullscreen:l,onDismiss:o}){if(!n)return null;const f=s==="pwa-hint";return r.jsx("div",{className:"fixed inset-0 z-40 flex items-end justify-center p-2 pointer-events-none sm:items-center",children:r.jsx("div",{className:"pointer-events-auto w-full max-w-sm rounded-lg border border-cyan-500/40 bg-zinc-950/95 shadow-xl",children:r.jsxs("div",{className:"space-y-0.5 p-4 text-sm text-slate-100",children:[r.jsx("h2",{className:"text-base font-semibold text-white",children:"Better in fullscreen"}),f?r.jsxs("p",{className:"text-slate-300",children:["For fullscreen on iOS, open Safari's share menu and pick ",r.jsx("strong",{children:"Add to Home Screen"}),". Launching from the home screen removes the browser chrome."]}):r.jsx("p",{className:"text-slate-300",children:"Enable fullscreen to free up more space for the video feed and controls. You can exit fullscreen at any time via the system back or home gesture."}),r.jsxs("div",{className:"flex justify-end gap-0.5 pt-1 text-sm",children:[r.jsx("button",{type:"button",className:"rounded border border-slate-600 px-3 py-1 text-slate-200",onClick:o,children:f?"Got it":"Not now"}),!f&&r.jsx("button",{type:"button",className:"rounded bg-cyan-500 px-3 py-1 font-semibold text-black hover:bg-cyan-400",onClick:l,children:"Enter fullscreen"})]})]})})})}function Yw({side:n="right",onClick:s}){const l=n==="left"?"left-2":"right-2";return r.jsx("button",{type:"button",onClick:s,className:`fixed bottom-2 ${l} z-30 flex h-10 w-10 items-center justify-center rounded-full border border-cyan-200/50 bg-slate-900/70 text-cyan-100 shadow-lg backdrop-blur-sm transition hover:bg-slate-800/80 active:scale-95`,"aria-label":"Enter fullscreen",title:"Enter fullscreen",children:r.jsx("svg",{viewBox:"0 0 24 24",width:"18",height:"18","aria-hidden":"true",className:"pointer-events-none",children:r.jsx("path",{d:"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})})}const Pg="fullscreenPromptDismissed",Fw=new Set(["mobile-portrait","mobile-landscape"]),Wg=()=>{if(typeof window>"u")return null;try{return window.sessionStorage}catch{return null}},Xw=()=>{const n=Wg();if(!n)return!1;try{return n.getItem(Pg)==="1"}catch{return!1}},wx=()=>{const n=Wg();if(n)try{n.setItem(Pg,"1")}catch{}},Qw=()=>{if(typeof navigator>"u")return!1;const n=navigator.userAgent||navigator.platform||"";return/iphone|ipad|ipod/i.test(n)},Cx=()=>{if(typeof document>"u")return!1;const n=document;return!!(n.fullscreenEnabled||n.webkitFullscreenEnabled||n.mozFullScreenEnabled||n.msFullscreenEnabled)};function Kw(n){const s=Fw.has(n),[l,o]=m.useState(!1),[f,d]=m.useState("native"),[h,p]=m.useState(!1),x=m.useMemo(()=>Qw(),[]),g=m.useMemo(()=>Cx(),[]),v=m.useCallback(()=>{if(!s){o(!1),p(!1);return}if(!h&&Xw()){o(!1);return}if(x){d("pwa-hint"),o(!0);return}if(!Cx()){o(!1);return}if(typeof document<"u"&&document.fullscreenElement){o(!1);return}d("native"),o(!0)},[h,x,s]);m.useEffect(()=>{v()},[v]),m.useEffect(()=>{if(typeof document>"u")return;const C=()=>{if(document.fullscreenElement){p(!1),o(!1);return}v()};return document.addEventListener("fullscreenchange",C),document.addEventListener("webkitfullscreenchange",C),()=>{document.removeEventListener("fullscreenchange",C),document.removeEventListener("webkitfullscreenchange",C)}},[v]);const y=m.useCallback(()=>{wx(),p(!1),o(!1)},[]),S=m.useCallback(async()=>{if(!s||x||typeof document>"u")return!1;if(document.fullscreenElement)return!0;const C=document.documentElement;if(!C)return!1;const M=C.requestFullscreen||C.webkitRequestFullscreen||C.mozRequestFullScreen||C.msRequestFullscreen;if(!M)return!1;try{const w=M.call(C);return w&&typeof w.then=="function"&&await w,wx(),p(!1),o(!1),!0}catch(w){return console.warn("Failed to enter fullscreen",w),!1}},[x,s]),E=m.useCallback(()=>{s&&(p(!0),d(x?"pwa-hint":"native"),o(!0))},[x,s]);return m.useEffect(()=>{s||(p(!1),o(!1))},[s]),{visible:l,mode:f,isIOS:x,nativeSupported:g,enterFullscreen:S,dismiss:y,showPrompt:E}}function Zw({visible:n,layout:s,onClose:l,showOnLoad:o,onToggleShowOnLoad:f}){if(!n)return null;const{state:d}=_t(),h=p=>{const x=!p.target.checked;f?.(x)};return r.jsx("div",{className:"fixed inset-0 z-30 flex items-center justify-center bg-black/80 px-0.5 py-0.5",children:r.jsxs("div",{className:"pointer-events-auto surface w-full max-w-4xl max-h-[90vh] overflow-hidden shadow-2xl",children:[r.jsxs("div",{className:"flex items-center justify-between border-b border-slate-700 px-0.5 py-0.25 text-sm text-slate-200",children:[r.jsx("span",{className:"font-semibold",children:"Help & controls"}),r.jsxs("div",{className:"flex items-center gap-0.5 text-[0.8rem] text-slate-300",children:[r.jsxs("label",{className:"flex items-center gap-0.5",children:[r.jsx("input",{type:"checkbox",checked:!o,onChange:h,className:"accent-cyan-500"}),r.jsx("span",{children:"Don't show again"})]}),r.jsx("button",{type:"button",onClick:l,className:"button-dark px-2 py-0.25 text-[0.8rem]",children:"Close"})]})]}),r.jsx("div",{className:"max-h-[85vh] overflow-y-auto p-0.5",children:r.jsx(Qg,{layout:s,keymap:d?.keymap||{}})})]})})}const Iw=`aroma bagel batter beans diff --git a/server/public/index.html b/server/public/index.html index b8874bde..d2ef165e 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -11,7 +11,7 @@ Multi Roomba Rover - + diff --git a/webui/src/components/VipPanel.jsx b/webui/src/components/VipPanel.jsx index 0c760cc4..bd21c360 100644 --- a/webui/src/components/VipPanel.jsx +++ b/webui/src/components/VipPanel.jsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { useSession } from '../context/SessionContext.jsx'; import { useSettingsNamespace } from '../settings/index.js'; import { COOKIE_KEY_REGEX, flowWrapClass } from './vip/constants.js'; -import VipAudioUploadCard from './vip/VipAudioUploadCard.jsx'; +import VipAudioUploadCard from './vip/VipAudioUploadCard/index.jsx'; import VipVerificationCard from './vip/VipVerificationCard.jsx'; import VipIdentityCard from './vip/VipIdentityCard.jsx'; import VipPrivateRoverAccessCard from './vip/VipPrivateRoverAccessCard.jsx'; diff --git a/webui/src/components/vip/VipAudioUploadCard/KeyPill.jsx b/webui/src/components/vip/VipAudioUploadCard/KeyPill.jsx new file mode 100644 index 00000000..21602e4f --- /dev/null +++ b/webui/src/components/vip/VipAudioUploadCard/KeyPill.jsx @@ -0,0 +1,5 @@ +// Keybinding label pill for PTT hints. +export default function KeyPill({ label }) { + if (!label) return null; + return {label}; +} diff --git a/webui/src/components/vip/VipAudioUploadCard/StatusIndicator.jsx b/webui/src/components/vip/VipAudioUploadCard/StatusIndicator.jsx new file mode 100644 index 00000000..ab0af8c0 --- /dev/null +++ b/webui/src/components/vip/VipAudioUploadCard/StatusIndicator.jsx @@ -0,0 +1,13 @@ +// Compact status tile used throughout VIP audio card. +export default function StatusIndicator({ label, active, detail = '' }) { + return ( +
+
{label}
+
{detail || (active ? 'active' : 'idle')}
+
+ ); +} diff --git a/webui/src/components/vip/VipAudioUploadCard.jsx b/webui/src/components/vip/VipAudioUploadCard/VipAudioUploadCardContent.jsx similarity index 76% rename from webui/src/components/vip/VipAudioUploadCard.jsx rename to webui/src/components/vip/VipAudioUploadCard/VipAudioUploadCardContent.jsx index bb12f2e5..fd58f85c 100644 --- a/webui/src/components/vip/VipAudioUploadCard.jsx +++ b/webui/src/components/vip/VipAudioUploadCard/VipAudioUploadCardContent.jsx @@ -1,232 +1,19 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { fieldClass } from './constants.js'; -import { useControlSystem } from '../../controls/index.js'; -import { formatKeyLabel } from '../../controls/keymapUtils.js'; -import { useSettingsNamespace } from '../../settings/index.js'; - -const MAX_UPLOAD_BYTES = 8 * 1024 * 1024; -const TARGET_SAMPLE_RATE = 16000; -const RTC_CONFIG = { - iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], - bundlePolicy: 'max-bundle', - rtcpMuxPolicy: 'require', -}; - -function bytesToBase64(bytes) { - let binary = ''; - const chunkSize = 0x8000; - for (let i = 0; i < bytes.length; i += chunkSize) { - const chunk = bytes.subarray(i, i + chunkSize); - binary += String.fromCharCode(...chunk); - } - return btoa(binary); -} - -function encodeBase64(value) { - if (typeof btoa === 'function') return btoa(value); - return ''; -} - -function buildAuthHeader(token) { - if (!token) return {}; - const encoded = encodeBase64(`${token}:${token}`); - return encoded ? { Authorization: `Basic ${encoded}` } : {}; -} - -function waitForIceGatheringComplete(pc, timeoutMs = 1500) { - return new Promise((resolve) => { - if (!pc || pc.iceGatheringState === 'complete') { - resolve(); - return; - } - const timer = setTimeout(() => { - pc.removeEventListener('icegatheringstatechange', onChange); - resolve(); - }, timeoutMs); - function onChange() { - if (pc.iceGatheringState === 'complete') { - clearTimeout(timer); - pc.removeEventListener('icegatheringstatechange', onChange); - resolve(); - } - } - pc.addEventListener('icegatheringstatechange', onChange); - }); -} - -function isPeerTransportReady(pc) { - if (!pc) return false; - const conn = pc.connectionState; - const ice = pc.iceConnectionState; - if (conn === 'connected') return true; - if (ice === 'connected' || ice === 'completed') return true; - return false; -} - -function waitForPeerConnected(pc, timeoutMs = 10000) { - return new Promise((resolve, reject) => { - if (!pc) { - reject(new Error('Peer connection missing')); - return; - } - if (isPeerTransportReady(pc)) { - resolve(); - return; - } - const timer = setTimeout(() => { - cleanup(); - reject(new Error('Peer connection timeout')); - }, timeoutMs); - const onState = () => { - if (isPeerTransportReady(pc)) { - cleanup(); - resolve(); - } else if ( - pc.connectionState === 'failed' || - pc.connectionState === 'closed' || - pc.iceConnectionState === 'failed' - ) { - cleanup(); - reject(new Error(`Peer connection ${pc.connectionState || pc.iceConnectionState}`)); - } - }; - function cleanup() { - clearTimeout(timer); - pc.removeEventListener('connectionstatechange', onState); - pc.removeEventListener('iceconnectionstatechange', onState); - } - pc.addEventListener('connectionstatechange', onState); - pc.addEventListener('iceconnectionstatechange', onState); - }); -} - -async function configureSenderForLowLatency(sender) { - if (!sender?.getParameters || !sender?.setParameters) return; - const params = sender.getParameters() || {}; - const first = (params.encodings && params.encodings[0]) || {}; - params.encodings = [ - { - ...first, - maxBitrate: 64000, - dtx: 'disabled', - }, - ]; - try { - await sender.setParameters(params); - } catch { - // Browser support varies; keep defaults if rejected. - } -} - -function waitForOutboundAudioFlow(pc, timeoutMs = 6000) { - return new Promise((resolve, reject) => { - if (!pc) { - reject(new Error('Peer connection missing')); - return; - } - const start = Date.now(); - let baseline = -1; - const timer = setInterval(async () => { - if (Date.now() - start > timeoutMs) { - clearInterval(timer); - reject(new Error('WHIP connected but no outbound audio flow')); - return; - } - try { - const senders = pc.getSenders().filter((s) => s.track?.kind === 'audio'); - for (const sender of senders) { - const stats = await sender.getStats(); - for (const report of stats.values()) { - if (report.type !== 'outbound-rtp' || report.kind !== 'audio') continue; - const sent = Number(report.bytesSent || 0); - const packets = Number(report.packetsSent || 0); - if (baseline < 0) { - baseline = sent; - } else if (sent > baseline + 200 || packets > 5) { - clearInterval(timer); - resolve(); - return; - } - } - } - } catch { - // Keep polling until timeout. - } - }, 250); - }); -} - -function StatusIndicator({ label, active, detail = '' }) { - return ( -
-
{label}
-
{detail || (active ? 'active' : 'idle')}
-
- ); -} - -function KeyPill({ label }) { - if (!label) return null; - return {label}; -} - -function mergeFloatChunks(chunks) { - const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0); - const merged = new Float32Array(total); - let offset = 0; - for (const chunk of chunks) { - merged.set(chunk, offset); - offset += chunk.length; - } - return merged; -} - -function pcm16FromFloat32(samples) { - const out = new Int16Array(samples.length); - for (let i = 0; i < samples.length; i += 1) { - const s = Math.max(-1, Math.min(1, samples[i])); - out[i] = s < 0 ? s * 0x8000 : s * 0x7fff; - } - return out; -} - -function encodeWavMono16(samples, sampleRate) { - const pcm = pcm16FromFloat32(samples); - const dataSize = pcm.length * 2; - const buffer = new ArrayBuffer(44 + dataSize); - const view = new DataView(buffer); - - const writeString = (offset, value) => { - for (let i = 0; i < value.length; i += 1) { - view.setUint8(offset + i, value.charCodeAt(i)); - } - }; - - writeString(0, 'RIFF'); - view.setUint32(4, 36 + dataSize, true); - writeString(8, 'WAVE'); - writeString(12, 'fmt '); - view.setUint32(16, 16, true); - view.setUint16(20, 1, true); - view.setUint16(22, 1, true); - view.setUint32(24, sampleRate, true); - view.setUint32(28, sampleRate * 2, true); - view.setUint16(32, 2, true); - view.setUint16(34, 16, true); - writeString(36, 'data'); - view.setUint32(40, dataSize, true); - - let offset = 44; - for (let i = 0; i < pcm.length; i += 1, offset += 2) { - view.setInt16(offset, pcm[i], true); - } - - return new Uint8Array(buffer); -} +import { fieldClass } from '../constants.js'; +import { useControlSystem } from '../../../controls/index.js'; +import { formatKeyLabel } from '../../../controls/keymapUtils.js'; +import { useSettingsNamespace } from '../../../settings/index.js'; +import { MAX_UPLOAD_BYTES, TARGET_SAMPLE_RATE, RTC_CONFIG } from './constants.js'; +import { bytesToBase64, buildAuthHeader } from './base64.js'; +import { + waitForIceGatheringComplete, + waitForPeerConnected, + configureSenderForLowLatency, + waitForOutboundAudioFlow, +} from './whipTransport.js'; +import { mergeFloatChunks, encodeWavMono16 } from './audioCodec.js'; +import StatusIndicator from './StatusIndicator.jsx'; +import KeyPill from './KeyPill.jsx'; export default function VipAudioUploadCard({ ownRoverId = '', diff --git a/webui/src/components/vip/VipAudioUploadCard/audioCodec.js b/webui/src/components/vip/VipAudioUploadCard/audioCodec.js new file mode 100644 index 00000000..72215d90 --- /dev/null +++ b/webui/src/components/vip/VipAudioUploadCard/audioCodec.js @@ -0,0 +1,54 @@ +// PCM/WAV helpers for clip-mode recording payloads. +export function mergeFloatChunks(chunks) { + const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const merged = new Float32Array(total); + let offset = 0; + for (const chunk of chunks) { + merged.set(chunk, offset); + offset += chunk.length; + } + return merged; +} + +function pcm16FromFloat32(samples) { + const out = new Int16Array(samples.length); + for (let i = 0; i < samples.length; i += 1) { + const s = Math.max(-1, Math.min(1, samples[i])); + out[i] = s < 0 ? s * 0x8000 : s * 0x7fff; + } + return out; +} + +export function encodeWavMono16(samples, sampleRate) { + const pcm = pcm16FromFloat32(samples); + const dataSize = pcm.length * 2; + const buffer = new ArrayBuffer(44 + dataSize); + const view = new DataView(buffer); + + const writeString = (offset, value) => { + for (let i = 0; i < value.length; i += 1) { + view.setUint8(offset + i, value.charCodeAt(i)); + } + }; + + writeString(0, 'RIFF'); + view.setUint32(4, 36 + dataSize, true); + writeString(8, 'WAVE'); + writeString(12, 'fmt '); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); + view.setUint16(22, 1, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * 2, true); + view.setUint16(32, 2, true); + view.setUint16(34, 16, true); + writeString(36, 'data'); + view.setUint32(40, dataSize, true); + + let offset = 44; + for (let i = 0; i < pcm.length; i += 1, offset += 2) { + view.setInt16(offset, pcm[i], true); + } + + return new Uint8Array(buffer); +} diff --git a/webui/src/components/vip/VipAudioUploadCard/base64.js b/webui/src/components/vip/VipAudioUploadCard/base64.js new file mode 100644 index 00000000..65ea881e --- /dev/null +++ b/webui/src/components/vip/VipAudioUploadCard/base64.js @@ -0,0 +1,21 @@ +// Base64 and auth helpers for upload and WHIP requests. +export function bytesToBase64(bytes) { + let binary = ''; + const chunkSize = 0x8000; + for (let i = 0; i < bytes.length; i += chunkSize) { + const chunk = bytes.subarray(i, i + chunkSize); + binary += String.fromCharCode(...chunk); + } + return btoa(binary); +} + +function encodeBase64(value) { + if (typeof btoa === 'function') return btoa(value); + return ''; +} + +export function buildAuthHeader(token) { + if (!token) return {}; + const encoded = encodeBase64(`${token}:${token}`); + return encoded ? { Authorization: `Basic ${encoded}` } : {}; +} diff --git a/webui/src/components/vip/VipAudioUploadCard/constants.js b/webui/src/components/vip/VipAudioUploadCard/constants.js new file mode 100644 index 00000000..8459c917 --- /dev/null +++ b/webui/src/components/vip/VipAudioUploadCard/constants.js @@ -0,0 +1,8 @@ +// VIP audio upload/mic forwarding constants. +export const MAX_UPLOAD_BYTES = 8 * 1024 * 1024; +export const TARGET_SAMPLE_RATE = 16000; +export const RTC_CONFIG = { + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', +}; diff --git a/webui/src/components/vip/VipAudioUploadCard/index.jsx b/webui/src/components/vip/VipAudioUploadCard/index.jsx new file mode 100644 index 00000000..54fe72e3 --- /dev/null +++ b/webui/src/components/vip/VipAudioUploadCard/index.jsx @@ -0,0 +1,3 @@ +import VipAudioUploadCardContent from './VipAudioUploadCardContent.jsx'; + +export default VipAudioUploadCardContent; diff --git a/webui/src/components/vip/VipAudioUploadCard/whipTransport.js b/webui/src/components/vip/VipAudioUploadCard/whipTransport.js new file mode 100644 index 00000000..200ee99e --- /dev/null +++ b/webui/src/components/vip/VipAudioUploadCard/whipTransport.js @@ -0,0 +1,123 @@ +// WHIP/WebRTC transport helpers for microphone forwarding. +export function waitForIceGatheringComplete(pc, timeoutMs = 1500) { + return new Promise((resolve) => { + if (!pc || pc.iceGatheringState === 'complete') { + resolve(); + return; + } + const timer = setTimeout(() => { + pc.removeEventListener('icegatheringstatechange', onChange); + resolve(); + }, timeoutMs); + function onChange() { + if (pc.iceGatheringState === 'complete') { + clearTimeout(timer); + pc.removeEventListener('icegatheringstatechange', onChange); + resolve(); + } + } + pc.addEventListener('icegatheringstatechange', onChange); + }); +} + +function isPeerTransportReady(pc) { + if (!pc) return false; + const conn = pc.connectionState; + const ice = pc.iceConnectionState; + if (conn === 'connected') return true; + if (ice === 'connected' || ice === 'completed') return true; + return false; +} + +export function waitForPeerConnected(pc, timeoutMs = 10000) { + return new Promise((resolve, reject) => { + if (!pc) { + reject(new Error('Peer connection missing')); + return; + } + if (isPeerTransportReady(pc)) { + resolve(); + return; + } + const timer = setTimeout(() => { + cleanup(); + reject(new Error('Peer connection timeout')); + }, timeoutMs); + const onState = () => { + if (isPeerTransportReady(pc)) { + cleanup(); + resolve(); + } else if ( + pc.connectionState === 'failed' || + pc.connectionState === 'closed' || + pc.iceConnectionState === 'failed' + ) { + cleanup(); + reject(new Error(`Peer connection ${pc.connectionState || pc.iceConnectionState}`)); + } + }; + function cleanup() { + clearTimeout(timer); + pc.removeEventListener('connectionstatechange', onState); + pc.removeEventListener('iceconnectionstatechange', onState); + } + pc.addEventListener('connectionstatechange', onState); + pc.addEventListener('iceconnectionstatechange', onState); + }); +} + +export async function configureSenderForLowLatency(sender) { + if (!sender?.getParameters || !sender?.setParameters) return; + const params = sender.getParameters() || {}; + const first = (params.encodings && params.encodings[0]) || {}; + params.encodings = [ + { + ...first, + maxBitrate: 64000, + dtx: 'disabled', + }, + ]; + try { + await sender.setParameters(params); + } catch { + // Browser support varies; keep defaults if rejected. + } +} + +export function waitForOutboundAudioFlow(pc, timeoutMs = 6000) { + return new Promise((resolve, reject) => { + if (!pc) { + reject(new Error('Peer connection missing')); + return; + } + const start = Date.now(); + let baseline = -1; + const timer = setInterval(async () => { + if (Date.now() - start > timeoutMs) { + clearInterval(timer); + reject(new Error('WHIP connected but no outbound audio flow')); + return; + } + try { + const senders = pc.getSenders().filter((s) => s.track?.kind === 'audio'); + for (const sender of senders) { + const stats = await sender.getStats(); + for (const report of stats.values()) { + if (report.type !== 'outbound-rtp' || report.kind !== 'audio') continue; + const sent = Number(report.bytesSent || 0); + const packets = Number(report.packetsSent || 0); + if (baseline < 0) { + baseline = sent; + } else if (sent > baseline + 200 || packets > 5) { + clearInterval(timer); + resolve(); + return; + } + } + } + } catch { + // Keep polling until timeout. + } + }, 250); + }); +}