From 13e9ebc90952c1fd0acbc8855bb113857859a4cb Mon Sep 17 00:00:00 2001 From: legop3 Date: Tue, 28 Apr 2026 14:02:14 -0400 Subject: [PATCH] it begins --- rulesdocs/refector_rules_and_tracking.md | 113 +++-- .../{index-bndWXjZU.js => index-BK4DZOjR.js} | 3 +- server/public/index.html | 2 +- webui/src/mini/MiniSummaryApp.jsx | 451 +----------------- .../MiniSummaryApp/MiniSummaryAppRoot.jsx | 24 + .../MiniSummaryApp/MiniSummaryContent.jsx | 224 +++++++++ .../MiniSummaryApp/components/AutoFitText.jsx | 63 +++ .../components/FitViewportFrame.jsx | 19 + .../MiniSummaryApp/components/InfoColumn.jsx | 118 +++++ webui/src/mini/MiniSummaryApp/constants.js | 3 + webui/src/mini/MiniSummaryApp/utils.js | 17 + 11 files changed, 536 insertions(+), 501 deletions(-) rename server/public/assets/{index-bndWXjZU.js => index-BK4DZOjR.js} (95%) create mode 100644 webui/src/mini/MiniSummaryApp/MiniSummaryAppRoot.jsx create mode 100644 webui/src/mini/MiniSummaryApp/MiniSummaryContent.jsx create mode 100644 webui/src/mini/MiniSummaryApp/components/AutoFitText.jsx create mode 100644 webui/src/mini/MiniSummaryApp/components/FitViewportFrame.jsx create mode 100644 webui/src/mini/MiniSummaryApp/components/InfoColumn.jsx create mode 100644 webui/src/mini/MiniSummaryApp/constants.js create mode 100644 webui/src/mini/MiniSummaryApp/utils.js diff --git a/rulesdocs/refector_rules_and_tracking.md b/rulesdocs/refector_rules_and_tracking.md index bdec200f..79329123 100644 --- a/rulesdocs/refector_rules_and_tracking.md +++ b/rulesdocs/refector_rules_and_tracking.md @@ -1,67 +1,80 @@ # REFACTOR RULES -- do NOT change ANY functionality!!! all changes should be purely internal!! -- this refactor is for simplification purposes, to make it easier to work on. remove any unused files and code. +- Do NOT change ANY functionality. All changes must be purely internal refactors. +- Refactor for simplification and maintainability. +- Remove unused files/code only when verified unused. +- No backwards compatability is needed anywhere. Clients and the server are both always up to date. +- Keep behavior/API contracts unchanged. -## server backend -- there are a lot of services and some have gotten huge, like way too large to manage manually -- all services should be converted into folders, with their files inside the folders -- large functions of the service should be split off into more, smaller files within the service's folder -- keep files relatively small and very clear and concise in their function. with titles commented at the top. +## Required safety checks for every change +- Preserve imports/exports and call signatures unless internal-only and non-observable. +- Validate no runtime behavior changes (manual flow checks + targeted tests when available). +- Make small, reviewable commits per service/component area. -## webui frontend -- similar situation to the server's service file bloat, but with large jsx component files -- split up large jsx components and backing js files into folders containing smaller files -- use same clear concise functional format, with title comments -- the web UI probably has a lot of leftovers inside still, old files that arent used, backwards compatability that doesnt need to exist, etc. simplify it all. +## Server backend +- Convert every service into a folder-based structure. +- Split very large service files into smaller focused modules. +- Keep files concise and single-purpose. +- Add clear title comments at top of split files. +## WebUI frontend +- Split large JSX/components and large backing JS files into folderized modules. +- Keep modules clear and focused, with title comments. +- Remove stale compatibility/leftover code only after usage verification. # REFACTOR TRACKING -## server backend +## Current phase +- [x] Phase 1: Inventory + usage mapping (server + webui) +- [ ] Phase 2: Refactor highest-impact offenders first +- [ ] Phase 3: Sweep remaining services/components +- [ ] Phase 4: Dead code/file removal pass +- [ ] Phase 5: Final regression validation + +## Server backend ### BIGGEST OFFENDERS -- audio forward service -- button box service -- chat service -- discord bot service -- home assistant service -- llm commentary service -- private rover access request service -- replay services, already split up kinda. still go through them and reformat into folders. -- room camera services, also split up already. go through and reformat -- rover manager service -- session service -- turn service -- verification service -- video auth service -- despite this list, ALL SERVICES should be reorganized into folders and split up where reasonable +- [ ] audio forward service +- [ ] button box service +- [ ] chat service +- [ ] discord bot service +- [ ] home assistant service +- [ ] llm commentary service +- [ ] private rover access request service +- [ ] replay services (already partly split; reformat consistently) +- [ ] room camera services (already partly split; reformat consistently) +- [ ] rover manager service +- [ ] session service +- [ ] turn service +- [ ] verification service +- [ ] video auth service +- [ ] All remaining services: reorganize to folder structure where needed ### COMPLETED SERVICES -- +- None yet. ### LARGE CHANGES -- +- None yet. - - - - - - - -## webui frontend +## WebUI frontend ### BIGGEST OFFENDERS -- mini summary app -- spectator app -- vip audio upload card -- admin panel -- drive dock action -- gamepad mapping settings -- mobile controls -- top down map -- videotile -- a lot of the code in the webui folder may be unused or unneeded. when going through the files, check if that function needs to exist, or if the file is used +- [x] mini summary app +- [ ] spectator app +- [ ] vip audio upload card +- [ ] admin panel +- [ ] drive dock action +- [ ] gamepad mapping settings +- [ ] mobile controls +- [ ] top down map +- [ ] video tile +- [ ] Sweep `webui` for unused or unneeded files/code with verification ### COMPLETED COMPONENTS -- +- mini summary app ### LARGE CHANGES -- \ No newline at end of file +- Split `webui/src/mini/MiniSummaryApp.jsx` into folderized modules under `webui/src/mini/MiniSummaryApp/` with a compatibility entrypoint preserved. + +## Done criteria (per item) +- [ ] Folderized structure created. +- [ ] Large functions extracted into focused files. +- [ ] Imports/exports updated with no external behavior change. +- [ ] Verified references/usages still resolve. +- [ ] Passed targeted checks/tests for touched area. diff --git a/server/public/assets/index-bndWXjZU.js b/server/public/assets/index-BK4DZOjR.js similarity index 95% rename from server/public/assets/index-bndWXjZU.js rename to server/public/assets/index-BK4DZOjR.js index 75018a4e..2e0761a6 100644 --- a/server/public/assets/index-bndWXjZU.js +++ b/server/public/assets/index-BK4DZOjR.js @@ -122,4 +122,5 @@ wine wok yeast yogurt -`;function Jw(n){return n?n.charAt(0).toUpperCase()+n.slice(1):""}function Pw(){const n=String(Iw).split(/\r?\n/).map(l=>l.trim()).filter(Boolean);return n.length?(n[Math.floor(Math.random()*n.length)]||"").trim():""}function Ad(){const{setNickname:n}=wn(),{value:s,status:l,save:o}=He("profile",{nickname:""}),f=m.useRef(!1);m.useEffect(()=>{if(l!=="ready"||f.current||(s.nickname||"").trim())return;f.current=!0;const h=Jw(Pw()).slice(0,32);if(!h)return;(async()=>{try{await n(h)}catch{}finally{o({nickname:h})}})()},[o,n,l,s.nickname])}function Ww(){const n=tn(),s=ft(w=>w.connected),{identifySession:l}=wn(),{value:o,status:f,save:d}=He("identity",{cookieUserId:""}),{value:h,status:p}=He("profile",{nickname:""}),x=m.useRef(!1),g=m.useRef(null),v=m.useRef(null),y=f==="ready"&&p==="ready",S=(o?.cookieUserId||"").trim(),E=(h?.nickname||"").trim(),C=m.useCallback(()=>{v.current&&(clearTimeout(v.current),v.current=null)},[]),k=m.useCallback(async()=>{if(!(!y||!s||!n?.id||x.current)){x.current=!0;try{const j=((await l({cookieUserId:S,nickname:E}))?.cookieUserId||"").trim();j&&j!==S&&d(N=>({...N||{},cookieUserId:j})),g.current=n.id,C()}catch{C(),v.current=setTimeout(()=>{k()},2e3)}finally{x.current=!1}}},[C,s,S,l,E,y,d,n?.id]);m.useEffect(()=>{!y||!s||!n?.id||g.current!==n.id&&k()},[s,y,k,n?.id]),m.useEffect(()=>{!y||!s||!n?.id||k()},[y,s,n?.id,S,E,k]),m.useEffect(()=>{const w=()=>{n?.connected&&k()},j=()=>{typeof document>"u"||document.visibilityState!=="visible"||n?.connected&&k()};return window.addEventListener("online",w),document.addEventListener("visibilitychange",j),()=>{window.removeEventListener("online",w),document.removeEventListener("visibilitychange",j)}},[k,n?.connected]),m.useEffect(()=>()=>C(),[C])}const Sf=1e4,jf=28,e2=14;function Ao({layout:n="desktop",className:s="",dismissable:l=!0}){const{session:o}=nn(),f=o?.communityGoal?.text?String(o.communityGoal.text).trim():"",d=n==="mobile-portrait"||n==="mobile-landscape"||n==="mobile",[h,p]=m.useState(!1),[x,g]=m.useState(jf),[v,y]=m.useState(0),S=m.useRef(null),E=m.useRef(null),C=m.useRef(null);m.useEffect(()=>{if(!f){p(!1);return}if(p(!0),!d||!l)return;const N=Date.now();y(Sf);const T=setTimeout(()=>p(!1),Sf),M=setInterval(()=>{const O=Date.now()-N;y(Math.max(0,Sf-O))},250);return()=>{clearTimeout(T),clearInterval(M)}},[f,d]),m.useEffect(()=>{if(!f)return;g(jf);let N=0;const T=()=>{const O=C.current,K=E.current;if(!O||!K)return;const _=O.clientWidth;if(!_)return;const U=K.scrollWidth;if(!U)return;const X=Math.min(1,_/U),F=Math.max(e2,Math.floor(jf*X));g(F)};N=window.requestAnimationFrame(T);const M=new ResizeObserver(()=>{window.cancelAnimationFrame(N),N=window.requestAnimationFrame(T)});return S.current&&M.observe(S.current),()=>{window.cancelAnimationFrame(N),M.disconnect()}},[f]);const k=m.useMemo(()=>["panel-section flex w-full items-center justify-center",d?"rounded-none":"rounded","px-1 py-1 text-center font-semibold tracking-tight",s].filter(Boolean).join(" "),[s,d]);if(!f||!h)return null;const w=d?Math.ceil(v/1e3):null,j=l?{onClick:()=>p(!1),role:"button",tabIndex:0}:{};return r.jsx("div",{ref:S,className:k,style:{fontSize:`${x}px`,lineHeight:1.1},...j,children:r.jsxs("span",{className:"flex w-full items-stretch gap-0.5 whitespace-nowrap rounded-md",children:[r.jsxs("span",{className:"flex flex-col justify-center border-r border-slate-700/60 px-0.5 text-[0.55em] font-semibold leading-tight text-slate-400",children:[r.jsx("span",{children:"Community"}),r.jsx("span",{children:"Goal"})]}),r.jsx("span",{ref:C,className:"flex-1 overflow-hidden text-slate-100",children:r.jsx("span",{ref:E,className:"block",children:f})}),d?r.jsxs("span",{className:"flex items-center border-l border-slate-700/60 px-0.5 text-[0.55em] font-semibold uppercase tracking-wide text-slate-400",children:[w,"s"]}):null]})})}const t2=3200;function n2(n={}){const s=typeof n.rewardName=="string"&&n.rewardName.trim()?n.rewardName.trim():"Unknown Event",l=Number.isFinite(Number(n.goal))?Math.max(1,Math.floor(Number(n.goal))):0,o=Number.isFinite(Number(n.count))?Math.max(0,Math.floor(Number(n.count))):l;return{rewardName:s,goal:l,count:o,ts:Date.now()}}function ev(){const n=tn(),{value:s}=He("audio",ct),l=Number.isFinite(s?.masterVolume)?s.masterVolume:ct.masterVolume,o=Number.isFinite(s?.alertVolume)?s.alertVolume:ct.alertVolume,f=Math.max(0,Math.min(1,l*o)),[d,h]=m.useState(null),p=m.useRef(null);return m.useEffect(()=>{p.current=new Audio("/tada.mp3"),p.current.volume=f,p.current.load()},[f]),m.useEffect(()=>{let x=null;function g(){const S=p.current;S&&(S.volume=f,S.currentTime=0,S.play().catch(()=>{}))}function v(S={}){const E=n2(S);h(E),g(),x&&clearTimeout(x),x=setTimeout(()=>{h(null)},t2)}function y(S){v(S?.detail||{})}return n.on("buttonBox:rewardRun",v),window.addEventListener("buttonBox:rewardRunLocalTest",y),()=>{x&&clearTimeout(x),n.off("buttonBox:rewardRun",v),window.removeEventListener("buttonBox:rewardRunLocalTest",y)}},[f,n]),d?r.jsx("div",{className:"pointer-events-none fixed inset-0 z-[120] flex items-center justify-center bg-black/75 px-1 py-1",children:r.jsx("div",{className:"pointer-events-auto w-full max-w-2xl",children:r.jsx("div",{className:"surface rounded-none border-2 border-cyan-300 bg-amber-100/95 px-1 py-1 text-center text-black shadow-2xl",children:r.jsxs("div",{className:"flex items-center justify-center gap-1 border-2 border-blue-600 bg-yellow-100 px-0.5 py-1",children:[r.jsx("img",{src:"/party1.gif",alt:"",className:"hidden shrink-0 sm:block"}),r.jsxs("div",{children:[r.jsxs("p",{className:"font-mono text-2xl font-bold text-red-700 sm:text-3xl",children:["Running event: ",d.rewardName,"!"]}),r.jsxs("p",{className:"font-mono text-xl font-bold text-blue-800 sm:text-2xl",children:[d.count,"/",d.goal]})]}),r.jsx("img",{src:"/party2.gif",alt:"",className:"hidden shrink-0 sm:block"})]})})})}):null}function a2(){const[n,s]=m.useState(()=>typeof window>"u"||window.innerWidth>=1024?"desktop":window.innerWidth>window.innerHeight?"mobile-landscape":"mobile-portrait");return m.useEffect(()=>{function l(){if(typeof window>"u")return;const{innerWidth:o,innerHeight:f}=window;o>=1024?s("desktop"):o>f?s("mobile-landscape"):s("mobile-portrait")}return l(),window.addEventListener("resize",l),()=>window.removeEventListener("resize",l)},[]),n}function s2({layout:n,onOpenHelpOverlay:s}){return r.jsxs("div",{className:"flex h-full gap-0.5 overflow-hidden",children:[r.jsxs("div",{className:"flex min-w-0 flex-[1.22] flex-col gap-0.5 overflow-y-auto pr-0",children:[r.jsx(xd,{}),r.jsx(nj,{}),r.jsx(dd,{})]}),r.jsxs("div",{className:"flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto",children:[r.jsx(Ao,{layout:n}),r.jsx(_w,{layout:n,onOpenHelpOverlay:s})]})]})}function tv({layout:n,onOpenHelpOverlay:s,roomPanelId:l,showTelemetry:o=!0}){const f=ft(k=>k.session),{state:d}=_t(),{value:h}=He("vipAudio",{openMicEnabled:!1,pttMode:"live"}),p=f?.isVerified?"bg-emerald-400":"bg-amber-400",x=String(f?.assignment?.roverId||"").trim(),g=x?f?.audioForward?.[x]:null,v=!!d?.mic?.pttActive,y=!!h?.openMicEnabled,S=h?.pttMode==="clip"?"clip":"live",E=!!(x&&f?.isVerified&&(S==="clip"?v:y||v)),C=!!(x&&f?.isVerified&&S==="clip"&&g?.source==="upload"&&g?.state==="playing");return r.jsx("section",{className:"panel text-base",children:r.jsxs(yd,{defaultTab:"chat",children:[r.jsxs(Sd,{children:[r.jsx(Wt,{id:"chat",children:"Chat"}),r.jsx(Wt,{id:"vip",highlight:C?"green":E?"pink":"none",children:r.jsxs("span",{className:"inline-flex items-center gap-0.5",children:[r.jsx("span",{children:"VIP"}),r.jsx("span",{className:`inline-block h-1.5 w-1.5 rounded-full ${p}`,"aria-hidden":"true",title:f?.isVerified?"Verified":"Not verified"})]})}),r.jsx(Wt,{id:"roomcontrols",children:"Room Controls"}),r.jsx(Wt,{id:"help",children:"Help"}),r.jsx(Wt,{id:"settings",children:"Settings"})]}),r.jsxs(jd,{children:[r.jsx(en,{id:"chat",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(qo,{}),r.jsx(So,{})]})}),r.jsx(en,{id:"vip",keepMounted:!0,children:r.jsx(Jg,{})}),r.jsx(en,{id:"roomcontrols",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(Hg,{}),r.jsx(jo,{}),r.jsx(fd,{panelId:l})]})}),r.jsx(en,{id:"help",children:r.jsx(Kg,{layout:n,onOpenOverlay:s})}),r.jsx(en,{id:"settings",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(Fg,{}),r.jsx(dd,{})]})})]})]})})}function l2({onOpenHelpOverlay:n,swapMobileControlColumns:s=!1}){return r.jsxs("div",{className:"flex flex-col gap-0.5",children:[r.jsx(xd,{layoutFormat:"mobile-portrait"}),r.jsx(kA,{swapColumns:s}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]",children:[r.jsx(ad,{panelId:"replay-sources-mobile-portrait"}),r.jsx(Do,{})]}),r.jsx(tv,{layout:"mobile-portrait",onOpenHelpOverlay:n,roomPanelId:"mobile-portrait-room"})]})}function i2({onOpenHelpOverlay:n,swapMobileControlColumns:s=!1}){const l="self-start h-[min(100svh,32rem)]",o=s?r.jsx(bo,{layout:"landscape",className:l}):r.jsx(vo,{layout:"landscape",className:l}),f=s?r.jsx(vo,{layout:"landscape",className:l}):r.jsx(bo,{layout:"landscape",className:l});return r.jsxs("div",{className:"flex flex-col gap-0.5",children:[r.jsxs("section",{className:"grid min-h-screen grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] gap-0.5",children:[o,r.jsxs("div",{children:[r.jsx(xd,{layoutFormat:"mobile-landscape"}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]",children:[r.jsx(ad,{panelId:"replay-sources-mobile-landscape"}),r.jsx(Do,{})]})]}),f]}),r.jsx("div",{className:"flex flex-col gap-0.5 pb-0",children:r.jsx(tv,{layout:"mobile-landscape",onOpenHelpOverlay:n,roomPanelId:"mobile-landscape-room",showTelemetry:!1})})]})}function r2(){const n=a2(),s=n==="desktop",l=Kw(n);return r.jsx("div",{className:`bg-black text-slate-100 ${s?"h-screen overflow-hidden":"min-h-screen"}`,children:r.jsx(o2,{layout:n,isDesktop:s,fullscreen:l})})}function o2({layout:n,isDesktop:s,fullscreen:l}){Ad(),Ww();const{visible:o,mode:f,isIOS:d,nativeSupported:h,enterFullscreen:p,dismiss:x,showPrompt:g}=l,{value:v,status:y,save:S}=He("help",{showOnLoad:!0}),{value:E}=He("page",{swapMobileControlColumns:!1}),C=!!E?.swapMobileControlColumns,k=C?"left":"right",w=!s&&(d||h),[j,N]=m.useState(!1);m.useEffect(()=>{y==="ready"&&N(v?.showOnLoad!==!1)},[y,v?.showOnLoad]);const T=m.useCallback(()=>N(!0),[]),M=m.useCallback(()=>N(!1),[]),O=m.useCallback(async()=>{if(d){g();return}await p()||g()},[p,d,g]),K=m.useCallback(U=>{const X=!!U;S(F=>({...F??{},showOnLoad:X})),X||N(!1)},[S]),_=m.useMemo(()=>s?r.jsx(s2,{layout:n,onOpenHelpOverlay:T}):n==="mobile-landscape"?r.jsx(i2,{onOpenHelpOverlay:T,swapMobileControlColumns:C}):r.jsx(l2,{onOpenHelpOverlay:T,swapMobileControlColumns:C}),[s,n,T,C]);return r.jsxs(Zj,{children:[r.jsx(iA,{}),r.jsx(vA,{}),r.jsxs("main",{className:`flex w-full flex-col gap-0.5 text-base ${s?"h-full overflow-hidden":""}`,children:[s?null:r.jsx(Ao,{layout:n}),_]}),r.jsx(id,{}),r.jsx(ev,{}),r.jsx($w,{}),r.jsx(Uw,{}),r.jsx(Zw,{visible:j,layout:n,onClose:M,showOnLoad:v?.showOnLoad!==!1,onToggleShowOnLoad:K}),r.jsx(Gw,{visible:o,mode:f,onEnterFullscreen:p,onDismiss:x}),w?r.jsx(Yw,{side:k,onClick:O}):null]})}function nv(){const{session:n,setRole:s,subscribeAll:l,connected:o}=nn(),[f,d]=m.useState(!1);return m.useEffect(()=>{let h=!1;async function p(){if(n?.mode==="lockdown"){d(!1);return}try{n?.role!=="spectator"&&await s("spectator"),await l(),h||d(!0)}catch(x){console.error("Failed to enter spectator mode",x),h||d(!1)}}return p(),()=>{h=!0}},[o,n?.mode,n?.role,s,l]),f}function c2(){const[n,s]=m.useState(()=>typeof window>"u"?!1:window.matchMedia("(max-aspect-ratio: 4/3)").matches);return m.useEffect(()=>{if(typeof window>"u")return;const l=window.matchMedia("(max-aspect-ratio: 4/3)"),o=f=>s(f.matches);return l.addEventListener?l.addEventListener("change",o):l.addListener(o),()=>{l.removeEventListener?l.removeEventListener("change",o):l.removeListener(o)}},[]),n}function u2({roverId:n,session:s}){const l=s?.activeDrivers?.[n]||null,f=(s?.users||[]).find(x=>x.socketId===l)?.nickname||(l?l.slice(0,6):"No driver"),d=s?.mode,h=s?.turnQueues?.[n];return d==="turns"&&h?.current?`${f} (turns)`:f}function f2({rover:n,frame:s,sessionInfo:l,videoMode:o,snapshotFeed:f,audioInfo:d,session:h}){const p=u2({roverId:n.id,session:h});return r.jsx("article",{className:"min-h-[16rem] rounded bg-zinc-900 p-0 sm:min-h-[18rem]",children:r.jsx("div",{className:"min-h-0 overflow-hidden rounded bg-black/20",children:r.jsx(wi,{sessionInfo:l,videoMode:o,snapshotFeed:f,audioSessionInfo:d,label:n.name,roverColor:n.color||null,telemetryFrame:s,batteryConfig:n.battery,hudVariant:"spectator",driverLabel:p,hudForceMap:!0,hudMapPosition:"top-center"})})})}function d2({roster:n,frames:s,videoSources:l,snapshotFeeds:o,audioSources:f,session:d,canSpectateVideo:h}){return n.length===0?r.jsx("p",{className:"col-span-full text-slate-400",children:"No rovers registered."}):r.jsx("section",{className:"grid grid-cols-1 gap-0.5 md:grid-cols-2",children:n.map(p=>r.jsx(f2,{rover:p,frame:s[p.id],sessionInfo:h&&l[p.id]||null,videoMode:h?"whep":"snapshot",snapshotFeed:h?null:o[p.id],audioInfo:f[`${p.id}-audio`],session:d,showHudMap:!0,hudMapPosition:"bottom-left"},p.id))})}function m2(){return r.jsx("section",{className:"min-h-0",children:r.jsx("div",{className:"surface min-h-[14rem] overflow-hidden",children:r.jsx(fd,{defaultOrientation:"horizontal",hideLayoutToggle:!0,hideHeader:!0,panelId:"spectator-secondary"})})})}function h2({className:n=""}){return r.jsx("div",{className:`panel ${n}`,children:r.jsx(dd,{})})}function p2(){const{session:n}=nn(),s=n?.mode==="lockdown",l=!!n?.isLocalNetwork;Ad(),nv();const o=c2(),f=td(),d=n?.roster??[],h=md(d.map(w=>w.id),{enabled:!s&&!l,version:n?.mode}),p=l?d.map(w=>({type:"rover",id:w.id,key:w.id})):[],x=Ai(p,{enabled:!s&&l,version:n?.mode}),g=d.flatMap(w=>w.media?.audioPublishUrl?[{type:"rover",id:`${w.id}-audio`,key:`${w.id}-audio`}]:[]),v=Ai(g,{enabled:!s,version:n?.mode});if(s)return r.jsx("div",{className:"flex min-h-screen items-center justify-center bg-black text-slate-200",children:r.jsxs("div",{className:"surface max-w-md space-y-0.5 p-1 text-center text-sm",children:[r.jsx("p",{className:"text-lg font-semibold text-white",children:"Spectate disabled during lockdown."}),r.jsx("p",{className:"text-slate-300",children:"Please wait until the server leaves lockdown to view streams."})]})});const y=o?"flex min-h-screen flex-col bg-black text-slate-100 md:h-screen md:overflow-hidden":"grid min-h-screen grid-cols-1 gap-0.5 bg-black text-slate-100 md:h-full md:min-h-0 md:grid-cols-[minmax(0,1fr)_18rem] lg:grid-cols-[minmax(0,1fr)_20rem]",S=o?"order-2 flex min-h-0 min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto":"order-1 flex min-h-0 min-w-0 flex-col gap-0.5 md:overflow-y-auto",E=o?"order-1 grid w-full min-h-0 items-stretch gap-0.5 border-b border-slate-800/60 bg-slate-950/90 p-0.5 grid-cols-[minmax(0,1fr)_minmax(0,0.7fr)_minmax(0,1.9fr)_minmax(0,0.7fr)]":"order-2 flex min-h-0 min-w-0 flex-col gap-0.5 border-l border-slate-800/60 bg-slate-950/90 md:h-full md:overflow-y-auto",C="",k=o?"h-64":"";return r.jsxs("div",{className:"min-h-screen bg-black text-slate-100 md:h-screen md:overflow-hidden",children:[r.jsxs("main",{className:y,children:[r.jsxs("section",{className:E,children:[o?r.jsxs("div",{className:`${C} ${k} flex flex-col gap-0.5`,children:[r.jsx(Ao,{layout:"desktop",dismissable:!1,className:"text-sm"}),r.jsx(jo,{}),r.jsx("div",{className:"min-h-0 flex-1",children:r.jsx(So,{hideNicknameForm:!0,hideHeader:!0,compact:!0,fillHeight:!0,className:"h-full"})})]}):r.jsxs("div",{className:C,children:[r.jsx(Ao,{layout:"desktop",dismissable:!1,className:"text-sm"}),r.jsx(jo,{})]}),r.jsx("div",{className:`${C} ${o?`${k} overflow-y-auto`:""}`,children:r.jsx("div",{className:"panel h-full",children:r.jsx(Do,{title:"Rovers"})})}),o?null:r.jsx("div",{className:`${C} flex-1 min-h-0`,children:r.jsx(So,{hideNicknameForm:!0,hideHeader:!0,compact:!0,fillHeight:!0,className:"h-full"})}),r.jsx("div",{className:`${C} ${o?k:"flex-[1.1] min-h-0"}`,children:r.jsx(qo,{allowSpectatorInput:!0,hideSpectatorNotice:!0,fillHeight:!0})}),r.jsx("div",{className:`${C} ${o?k:""}`,children:r.jsx(h2,{className:`${o?"h-full":"h-40"} overflow-hidden`})})]}),r.jsxs("section",{className:S,children:[r.jsx(d2,{roster:d,frames:f,videoSources:x,snapshotFeeds:h,audioSources:v,session:n,canSpectateVideo:l}),r.jsx(m2,{})]})]}),r.jsx(id,{}),r.jsx(ev,{})]})}function x2(){return r.jsx(p2,{})}const g2=2e4,v2=3600*1e3;function b2({roverId:n,session:s}){const l=s?.activeDrivers?.[n]||null,f=(s?.users||[]).find(p=>p.socketId===l)?.nickname||(l?l.slice(0,6):"No driver"),d=s?.mode,h=s?.turnQueues?.[n];return d==="turns"&&h?.current?`${f}`:f}function y2({rover:n,frame:s}){const l=s?.sensors?.batteryChargeMah??null,o=n?.battery??null;return Ug({batteryState:n?.batteryState??null,charge:l,config:o})}function Ex({rover:n,frame:s,driverLabel:l,sessionInfo:o,videoMode:f="snapshot",snapshotFeed:d,withDivider:h=!1,showPreview:p=!0,variant:x="stacked"}){const g=y2({rover:n,frame:s}),v=g?.available?g.percentDisplay:null,y=x==="active";return r.jsxs("div",{className:`relative flex min-w-0 flex-1 flex-col gap-4 overflow-hidden bg-black px-0 py-0 ${h?"border-r border-slate-700/60":""}`,children:[r.jsx(Hf,{visual:g,orientation:y?"vertical":"horizontal",variant:"background"}),y?r.jsxs("div",{className:"relative z-10 flex min-w-0 flex-1 flex-col justify-between text-center",children:[r.jsx("div",{className:"min-w-0 bg-transparent px-0 py-0 leading-none",children:r.jsx(sl,{className:"font-semibold leading-none text-white rounded border border-transparent px-1 py-[1px]",maxSize:1e3,minSize:18,style:ca(n.color,.18),children:n.name||n.id})}),l?r.jsx("div",{className:"min-w-0 bg-transparent px-0 py-0 leading-none",children:r.jsx(sl,{className:"font-semibold leading-none text-white",maxSize:1e3,minSize:16,children:l})}):r.jsx("div",{}),r.jsx("div",{className:"relative min-w-0 overflow-hidden bg-transparent px-0 py-0 leading-none",children:r.jsx("div",{className:"relative",children:r.jsx(sl,{className:"font-semibold leading-none text-white",maxSize:1e3,minSize:16,children:v==null?"--%":`${v}%`})})})]}):r.jsxs(r.Fragment,{children:[r.jsxs("div",{className:"relative z-10 flex min-w-0 flex-col gap-1 text-center",children:[r.jsx("div",{className:"min-w-0 bg-transparent px-0 py-0 leading-none",children:r.jsx(sl,{className:"font-semibold leading-none text-white rounded border border-transparent px-1 py-[1px]",maxSize:1e3,minSize:18,style:ca(n.color,.18),children:n.name||n.id})}),l?r.jsx("div",{className:"min-w-0 bg-transparent px-0 py-0 leading-none",children:r.jsx(sl,{className:"font-semibold leading-none text-white",maxSize:1e3,minSize:16,children:l})}):null,r.jsx("div",{className:"relative min-w-0 overflow-hidden bg-transparent px-0 py-0 leading-none",children:r.jsx("div",{className:"relative",children:r.jsx(sl,{className:"font-semibold leading-none text-white",maxSize:80,minSize:16,children:v==null?"--%":`${v}%`})})})]}),r.jsx("div",{className:"relative z-10 flex min-w-0 flex-1 flex-col gap-4",children:r.jsx("div",{className:"flex w-full min-w-0 flex-1 flex-col items-center gap-4",children:p?r.jsx("div",{className:"mt-auto w-full",children:r.jsx("div",{className:"w-full aspect-[4/3]",children:r.jsx(wi,{sessionInfo:o,videoMode:f,snapshotFeed:d,audioSessionInfo:null,label:n.name||n.id,roverColor:n.color||null,telemetryFrame:s,batteryConfig:n.battery,layoutFormat:"mobile",hudVariant:"none",fitParent:!0})})}):null})})]})]})}function sl({children:n,className:s="",maxSize:l=1e3,minSize:o=14,style:f=void 0}){const d=m.useRef(null),h=m.useRef(null),[p,x]=m.useState(l);return m.useLayoutEffect(()=>{const g=d.current,v=h.current;if(!g||!v)return;let y=null;const S=()=>{const k=g.clientWidth;if(!k){E();return}let w=o,j=l,N=o;for(;w<=j;){const T=Math.floor((w+j)/2);v.style.fontSize=`${T}px`,v.scrollWidth<=k?(N=T,w=T+1):j=T-1}x(N)},E=()=>{y&&cancelAnimationFrame(y),y=requestAnimationFrame(S)};E();const C=new ResizeObserver(E);return C.observe(g),()=>{y&&cancelAnimationFrame(y),C.disconnect()}},[n,l,o]),r.jsx("div",{ref:d,className:"w-full min-w-0",children:r.jsx("div",{ref:h,className:`whitespace-nowrap ${s}`,style:{fontSize:`${p}px`,lineHeight:1.1,...f||{}},children:n})})}function S2(){const{session:n}=nn(),s=nv();Ad();const l=n?.mode==="lockdown",o=!!n?.isLocalNetwork,f=td(),d=n?.roster??[],[h,p]=m.useState(0),x=n?.activeDrivers||{},g=m.useMemo(()=>d.filter(F=>x[F.id]),[d,x]),v=g.length?g:d,y=md(v.map(F=>F.id),{enabled:!l&&!o,version:n?.mode}),S=m.useMemo(()=>o?v.map(F=>({type:"rover",id:F.id,key:F.id})):[],[o,v]),E=Ai(S,{enabled:!l&&o,version:n?.mode}),C=m.useMemo(()=>g.flatMap(F=>{if(!F?.id||!F.media?.audioPublishUrl)return[];const H=String(F.id);return[{type:"rover",id:`${H}-audio`,key:`${H}-audio`}]}),[g]),k=Ai(C,{enabled:!l,version:n?.mode}),w=m.useMemo(()=>{if(!g.length)return[];if(!o){const H=g.filter(te=>y[te.id]?.objectUrl);return H.length?H:g}const F=g.filter(H=>{const te=E[H.id];return te?.url&&!te?.error});return F.length?F:g},[g,y,E,o]),j=m.useMemo(()=>w.map(F=>({type:"rover",rover:F})),[w]),N=m.useMemo(()=>j.map(F=>`r:${F.rover.id}`).join("|"),[j]);m.useEffect(()=>{p(0)},[N]),m.useEffect(()=>{if(!j.length)return;const F=setInterval(()=>{p(H=>(H+1)%j.length)},g2);return()=>clearInterval(F)},[j.length,N]);const T=j.length?j[h%j.length]:null,M=T?.type==="rover"?T.rover:null,O=!o&&M&&y[M.id]||null,K=o&&M&&E[M.id]||null,_=M&&k[`${M.id}-audio`]||null,U=M&&f[M.id]||null,X=M?b2({roverId:M.id,session:n}):null;return l?r.jsx("div",{className:"relative flex h-screen w-screen items-center justify-center bg-black text-slate-200",children:r.jsxs("div",{className:"surface max-w-sm space-y-0.5 p-1 text-center text-sm",children:[r.jsx("p",{className:"text-lg font-semibold text-white",children:"Mini spectator is disabled in lockdown."}),r.jsx("p",{className:"text-slate-300",children:"It will automatically resume once lockdown ends."})]})}):g.length?r.jsxs("div",{className:"relative flex h-screen w-screen overflow-hidden bg-black text-slate-100",children:[r.jsx("section",{className:"relative flex h-full shrink-0 items-center justify-start overflow-hidden bg-black",style:{width:"min(100vw, calc(100vh * 4 / 3))"},children:s?M?r.jsx(A2,{children:o?r.jsx("div",{className:"relative h-full w-full",children:j.map(F=>{const H=F.rover,te=M?.id===H.id;return r.jsx("div",{className:`absolute inset-0 ${te?"opacity-100":"opacity-0 pointer-events-none"}`,children:r.jsx(wi,{sessionInfo:E[H.id]||null,videoMode:"whep",snapshotFeed:null,audioSessionInfo:te?_:null,forceMute:!te,label:H.name||H.id,roverColor:H.color||null,telemetryFrame:f[H.id]||null,batteryConfig:H.battery,layoutFormat:"mobile",hudVariant:"none",fitParent:!0})},H.id)})}):r.jsx(wi,{sessionInfo:null,videoMode:"snapshot",snapshotFeed:O,audioSessionInfo:_,label:M.name||M.id,roverColor:M.color||null,telemetryFrame:U,batteryConfig:M.battery,layoutFormat:"mobile",hudVariant:"none",fitParent:!0})}):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-500",children:g.length?"No sources available.":"No active drivers."}):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-500",children:"Switching to spectator…"})}),r.jsx("aside",{className:"flex h-full min-w-0 flex-1 flex-col border-l border-slate-800/60 bg-black",children:M?r.jsx(Ex,{rover:M,frame:U,driverLabel:X,sessionInfo:o?K:null,videoMode:o?"whep":"snapshot",snapshotFeed:o?null:y[M.id]||null,showPreview:!1,variant:"active"}):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-500",children:"No active rover."})})]}):r.jsx("div",{className:"relative flex h-screen w-screen overflow-hidden bg-black text-slate-100",children:r.jsx("section",{className:"flex h-full w-full gap-x-3 bg-slate-900 px-0",children:d.length?d.map((F,H)=>r.jsx(Ex,{rover:F,frame:f[F.id]||null,driverLabel:null,sessionInfo:o&&E[F.id]||null,videoMode:o?"whep":"snapshot",snapshotFeed:o?null:y[F.id]||null,showPreview:!0,withDivider:!1},F.id)):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-500",children:"No rovers available."})})})}function j2(){return m.useEffect(()=>{if(typeof window>"u")return;const n=setTimeout(()=>{const s=new URL(window.location.href);s.searchParams.set("refresh",Date.now().toString()),window.location.replace(s.toString())},v2);return()=>clearTimeout(n)},[]),r.jsxs(r.Fragment,{children:[r.jsx(S2,{}),r.jsx(id,{scale:3})]})}function A2({children:n}){return r.jsx("div",{className:"flex h-full w-full items-center justify-start overflow-hidden bg-black",children:r.jsx("div",{className:"relative flex items-center justify-center overflow-hidden bg-black",style:{width:"min(100%, calc(100vh * 4 / 3))",height:"min(100%, calc(100vw * 3 / 4))",maxWidth:"100%",maxHeight:"100%",aspectRatio:"4 / 3"},children:r.jsx("div",{className:"flex h-full w-full items-center justify-center overflow-hidden",children:n})})})}function to(n){if(!n||!n.length)return null;const s=Math.floor(Math.random()*n.length);return n[s]||null}function N2(){if(typeof document>"u"||!document.body)return;const n=document.body.getElementsByTagName("*");if(!n||n.length<2)return;const s=to(n),l=to(n),o=s?.classList?.[0]||null;if(o&&l?.classList)try{l.classList.add(o)}catch{}const f=to(n),d=to(n),h=f?.classList?.[1]||null;if(h&&d?.classList)try{d.classList.add(h)}catch{}}function w2(){const n=ft(s=>!!s.session?.moderation?.isDeterred);return m.useEffect(()=>{if(!n)return;const s=window.setInterval(()=>{N2()},1);return()=>{window.clearInterval(s)}},[n]),null}Ny.createRoot(document.getElementById("root")).render(r.jsx(m.StrictMode,{children:r.jsx(QS,{children:r.jsx(JS,{children:r.jsx(PS,{children:r.jsx(rj,{children:r.jsxs(Jj,{children:[r.jsx(w2,{}),r.jsx(U1,{children:r.jsxs(x1,{children:[r.jsx(no,{path:"/",element:r.jsx(r2,{})}),r.jsx(no,{path:"/spectate",element:r.jsx(x2,{})}),r.jsx(no,{path:"/mini",element:r.jsx(j2,{})})]})})]})})})})})})); +shrimp +fling`;function Jw(n){return n?n.charAt(0).toUpperCase()+n.slice(1):""}function Pw(){const n=String(Iw).split(/\r?\n/).map(l=>l.trim()).filter(Boolean);return n.length?(n[Math.floor(Math.random()*n.length)]||"").trim():""}function Ad(){const{setNickname:n}=wn(),{value:s,status:l,save:o}=He("profile",{nickname:""}),f=m.useRef(!1);m.useEffect(()=>{if(l!=="ready"||f.current||(s.nickname||"").trim())return;f.current=!0;const h=Jw(Pw()).slice(0,32);if(!h)return;(async()=>{try{await n(h)}catch{}finally{o({nickname:h})}})()},[o,n,l,s.nickname])}function Ww(){const n=tn(),s=ft(w=>w.connected),{identifySession:l}=wn(),{value:o,status:f,save:d}=He("identity",{cookieUserId:""}),{value:h,status:p}=He("profile",{nickname:""}),x=m.useRef(!1),g=m.useRef(null),v=m.useRef(null),y=f==="ready"&&p==="ready",S=(o?.cookieUserId||"").trim(),E=(h?.nickname||"").trim(),C=m.useCallback(()=>{v.current&&(clearTimeout(v.current),v.current=null)},[]),k=m.useCallback(async()=>{if(!(!y||!s||!n?.id||x.current)){x.current=!0;try{const j=((await l({cookieUserId:S,nickname:E}))?.cookieUserId||"").trim();j&&j!==S&&d(N=>({...N||{},cookieUserId:j})),g.current=n.id,C()}catch{C(),v.current=setTimeout(()=>{k()},2e3)}finally{x.current=!1}}},[C,s,S,l,E,y,d,n?.id]);m.useEffect(()=>{!y||!s||!n?.id||g.current!==n.id&&k()},[s,y,k,n?.id]),m.useEffect(()=>{!y||!s||!n?.id||k()},[y,s,n?.id,S,E,k]),m.useEffect(()=>{const w=()=>{n?.connected&&k()},j=()=>{typeof document>"u"||document.visibilityState!=="visible"||n?.connected&&k()};return window.addEventListener("online",w),document.addEventListener("visibilitychange",j),()=>{window.removeEventListener("online",w),document.removeEventListener("visibilitychange",j)}},[k,n?.connected]),m.useEffect(()=>()=>C(),[C])}const Sf=1e4,jf=28,e2=14;function Ao({layout:n="desktop",className:s="",dismissable:l=!0}){const{session:o}=nn(),f=o?.communityGoal?.text?String(o.communityGoal.text).trim():"",d=n==="mobile-portrait"||n==="mobile-landscape"||n==="mobile",[h,p]=m.useState(!1),[x,g]=m.useState(jf),[v,y]=m.useState(0),S=m.useRef(null),E=m.useRef(null),C=m.useRef(null);m.useEffect(()=>{if(!f){p(!1);return}if(p(!0),!d||!l)return;const N=Date.now();y(Sf);const T=setTimeout(()=>p(!1),Sf),M=setInterval(()=>{const O=Date.now()-N;y(Math.max(0,Sf-O))},250);return()=>{clearTimeout(T),clearInterval(M)}},[f,d]),m.useEffect(()=>{if(!f)return;g(jf);let N=0;const T=()=>{const O=C.current,K=E.current;if(!O||!K)return;const _=O.clientWidth;if(!_)return;const U=K.scrollWidth;if(!U)return;const X=Math.min(1,_/U),F=Math.max(e2,Math.floor(jf*X));g(F)};N=window.requestAnimationFrame(T);const M=new ResizeObserver(()=>{window.cancelAnimationFrame(N),N=window.requestAnimationFrame(T)});return S.current&&M.observe(S.current),()=>{window.cancelAnimationFrame(N),M.disconnect()}},[f]);const k=m.useMemo(()=>["panel-section flex w-full items-center justify-center",d?"rounded-none":"rounded","px-1 py-1 text-center font-semibold tracking-tight",s].filter(Boolean).join(" "),[s,d]);if(!f||!h)return null;const w=d?Math.ceil(v/1e3):null,j=l?{onClick:()=>p(!1),role:"button",tabIndex:0}:{};return r.jsx("div",{ref:S,className:k,style:{fontSize:`${x}px`,lineHeight:1.1},...j,children:r.jsxs("span",{className:"flex w-full items-stretch gap-0.5 whitespace-nowrap rounded-md",children:[r.jsxs("span",{className:"flex flex-col justify-center border-r border-slate-700/60 px-0.5 text-[0.55em] font-semibold leading-tight text-slate-400",children:[r.jsx("span",{children:"Community"}),r.jsx("span",{children:"Goal"})]}),r.jsx("span",{ref:C,className:"flex-1 overflow-hidden text-slate-100",children:r.jsx("span",{ref:E,className:"block",children:f})}),d?r.jsxs("span",{className:"flex items-center border-l border-slate-700/60 px-0.5 text-[0.55em] font-semibold uppercase tracking-wide text-slate-400",children:[w,"s"]}):null]})})}const t2=3200;function n2(n={}){const s=typeof n.rewardName=="string"&&n.rewardName.trim()?n.rewardName.trim():"Unknown Event",l=Number.isFinite(Number(n.goal))?Math.max(1,Math.floor(Number(n.goal))):0,o=Number.isFinite(Number(n.count))?Math.max(0,Math.floor(Number(n.count))):l;return{rewardName:s,goal:l,count:o,ts:Date.now()}}function ev(){const n=tn(),{value:s}=He("audio",ct),l=Number.isFinite(s?.masterVolume)?s.masterVolume:ct.masterVolume,o=Number.isFinite(s?.alertVolume)?s.alertVolume:ct.alertVolume,f=Math.max(0,Math.min(1,l*o)),[d,h]=m.useState(null),p=m.useRef(null);return m.useEffect(()=>{p.current=new Audio("/tada.mp3"),p.current.volume=f,p.current.load()},[f]),m.useEffect(()=>{let x=null;function g(){const S=p.current;S&&(S.volume=f,S.currentTime=0,S.play().catch(()=>{}))}function v(S={}){const E=n2(S);h(E),g(),x&&clearTimeout(x),x=setTimeout(()=>{h(null)},t2)}function y(S){v(S?.detail||{})}return n.on("buttonBox:rewardRun",v),window.addEventListener("buttonBox:rewardRunLocalTest",y),()=>{x&&clearTimeout(x),n.off("buttonBox:rewardRun",v),window.removeEventListener("buttonBox:rewardRunLocalTest",y)}},[f,n]),d?r.jsx("div",{className:"pointer-events-none fixed inset-0 z-[120] flex items-center justify-center bg-black/75 px-1 py-1",children:r.jsx("div",{className:"pointer-events-auto w-full max-w-2xl",children:r.jsx("div",{className:"surface rounded-none border-2 border-cyan-300 bg-amber-100/95 px-1 py-1 text-center text-black shadow-2xl",children:r.jsxs("div",{className:"flex items-center justify-center gap-1 border-2 border-blue-600 bg-yellow-100 px-0.5 py-1",children:[r.jsx("img",{src:"/party1.gif",alt:"",className:"hidden shrink-0 sm:block"}),r.jsxs("div",{children:[r.jsxs("p",{className:"font-mono text-2xl font-bold text-red-700 sm:text-3xl",children:["Running event: ",d.rewardName,"!"]}),r.jsxs("p",{className:"font-mono text-xl font-bold text-blue-800 sm:text-2xl",children:[d.count,"/",d.goal]})]}),r.jsx("img",{src:"/party2.gif",alt:"",className:"hidden shrink-0 sm:block"})]})})})}):null}function a2(){const[n,s]=m.useState(()=>typeof window>"u"||window.innerWidth>=1024?"desktop":window.innerWidth>window.innerHeight?"mobile-landscape":"mobile-portrait");return m.useEffect(()=>{function l(){if(typeof window>"u")return;const{innerWidth:o,innerHeight:f}=window;o>=1024?s("desktop"):o>f?s("mobile-landscape"):s("mobile-portrait")}return l(),window.addEventListener("resize",l),()=>window.removeEventListener("resize",l)},[]),n}function s2({layout:n,onOpenHelpOverlay:s}){return r.jsxs("div",{className:"flex h-full gap-0.5 overflow-hidden",children:[r.jsxs("div",{className:"flex min-w-0 flex-[1.22] flex-col gap-0.5 overflow-y-auto pr-0",children:[r.jsx(xd,{}),r.jsx(nj,{}),r.jsx(dd,{})]}),r.jsxs("div",{className:"flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto",children:[r.jsx(Ao,{layout:n}),r.jsx(_w,{layout:n,onOpenHelpOverlay:s})]})]})}function tv({layout:n,onOpenHelpOverlay:s,roomPanelId:l,showTelemetry:o=!0}){const f=ft(k=>k.session),{state:d}=_t(),{value:h}=He("vipAudio",{openMicEnabled:!1,pttMode:"live"}),p=f?.isVerified?"bg-emerald-400":"bg-amber-400",x=String(f?.assignment?.roverId||"").trim(),g=x?f?.audioForward?.[x]:null,v=!!d?.mic?.pttActive,y=!!h?.openMicEnabled,S=h?.pttMode==="clip"?"clip":"live",E=!!(x&&f?.isVerified&&(S==="clip"?v:y||v)),C=!!(x&&f?.isVerified&&S==="clip"&&g?.source==="upload"&&g?.state==="playing");return r.jsx("section",{className:"panel text-base",children:r.jsxs(yd,{defaultTab:"chat",children:[r.jsxs(Sd,{children:[r.jsx(Wt,{id:"chat",children:"Chat"}),r.jsx(Wt,{id:"vip",highlight:C?"green":E?"pink":"none",children:r.jsxs("span",{className:"inline-flex items-center gap-0.5",children:[r.jsx("span",{children:"VIP"}),r.jsx("span",{className:`inline-block h-1.5 w-1.5 rounded-full ${p}`,"aria-hidden":"true",title:f?.isVerified?"Verified":"Not verified"})]})}),r.jsx(Wt,{id:"roomcontrols",children:"Room Controls"}),r.jsx(Wt,{id:"help",children:"Help"}),r.jsx(Wt,{id:"settings",children:"Settings"})]}),r.jsxs(jd,{children:[r.jsx(en,{id:"chat",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(qo,{}),r.jsx(So,{})]})}),r.jsx(en,{id:"vip",keepMounted:!0,children:r.jsx(Jg,{})}),r.jsx(en,{id:"roomcontrols",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(Hg,{}),r.jsx(jo,{}),r.jsx(fd,{panelId:l})]})}),r.jsx(en,{id:"help",children:r.jsx(Kg,{layout:n,onOpenOverlay:s})}),r.jsx(en,{id:"settings",children:r.jsxs("div",{className:"space-y-0.5",children:[r.jsx(Fg,{}),r.jsx(dd,{})]})})]})]})})}function l2({onOpenHelpOverlay:n,swapMobileControlColumns:s=!1}){return r.jsxs("div",{className:"flex flex-col gap-0.5",children:[r.jsx(xd,{layoutFormat:"mobile-portrait"}),r.jsx(kA,{swapColumns:s}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]",children:[r.jsx(ad,{panelId:"replay-sources-mobile-portrait"}),r.jsx(Do,{})]}),r.jsx(tv,{layout:"mobile-portrait",onOpenHelpOverlay:n,roomPanelId:"mobile-portrait-room"})]})}function i2({onOpenHelpOverlay:n,swapMobileControlColumns:s=!1}){const l="self-start h-[min(100svh,32rem)]",o=s?r.jsx(bo,{layout:"landscape",className:l}):r.jsx(vo,{layout:"landscape",className:l}),f=s?r.jsx(vo,{layout:"landscape",className:l}):r.jsx(bo,{layout:"landscape",className:l});return r.jsxs("div",{className:"flex flex-col gap-0.5",children:[r.jsxs("section",{className:"grid min-h-screen grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] gap-0.5",children:[o,r.jsxs("div",{children:[r.jsx(xd,{layoutFormat:"mobile-landscape"}),r.jsxs("div",{className:"grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]",children:[r.jsx(ad,{panelId:"replay-sources-mobile-landscape"}),r.jsx(Do,{})]})]}),f]}),r.jsx("div",{className:"flex flex-col gap-0.5 pb-0",children:r.jsx(tv,{layout:"mobile-landscape",onOpenHelpOverlay:n,roomPanelId:"mobile-landscape-room",showTelemetry:!1})})]})}function r2(){const n=a2(),s=n==="desktop",l=Kw(n);return r.jsx("div",{className:`bg-black text-slate-100 ${s?"h-screen overflow-hidden":"min-h-screen"}`,children:r.jsx(o2,{layout:n,isDesktop:s,fullscreen:l})})}function o2({layout:n,isDesktop:s,fullscreen:l}){Ad(),Ww();const{visible:o,mode:f,isIOS:d,nativeSupported:h,enterFullscreen:p,dismiss:x,showPrompt:g}=l,{value:v,status:y,save:S}=He("help",{showOnLoad:!0}),{value:E}=He("page",{swapMobileControlColumns:!1}),C=!!E?.swapMobileControlColumns,k=C?"left":"right",w=!s&&(d||h),[j,N]=m.useState(!1);m.useEffect(()=>{y==="ready"&&N(v?.showOnLoad!==!1)},[y,v?.showOnLoad]);const T=m.useCallback(()=>N(!0),[]),M=m.useCallback(()=>N(!1),[]),O=m.useCallback(async()=>{if(d){g();return}await p()||g()},[p,d,g]),K=m.useCallback(U=>{const X=!!U;S(F=>({...F??{},showOnLoad:X})),X||N(!1)},[S]),_=m.useMemo(()=>s?r.jsx(s2,{layout:n,onOpenHelpOverlay:T}):n==="mobile-landscape"?r.jsx(i2,{onOpenHelpOverlay:T,swapMobileControlColumns:C}):r.jsx(l2,{onOpenHelpOverlay:T,swapMobileControlColumns:C}),[s,n,T,C]);return r.jsxs(Zj,{children:[r.jsx(iA,{}),r.jsx(vA,{}),r.jsxs("main",{className:`flex w-full flex-col gap-0.5 text-base ${s?"h-full overflow-hidden":""}`,children:[s?null:r.jsx(Ao,{layout:n}),_]}),r.jsx(id,{}),r.jsx(ev,{}),r.jsx($w,{}),r.jsx(Uw,{}),r.jsx(Zw,{visible:j,layout:n,onClose:M,showOnLoad:v?.showOnLoad!==!1,onToggleShowOnLoad:K}),r.jsx(Gw,{visible:o,mode:f,onEnterFullscreen:p,onDismiss:x}),w?r.jsx(Yw,{side:k,onClick:O}):null]})}function nv(){const{session:n,setRole:s,subscribeAll:l,connected:o}=nn(),[f,d]=m.useState(!1);return m.useEffect(()=>{let h=!1;async function p(){if(n?.mode==="lockdown"){d(!1);return}try{n?.role!=="spectator"&&await s("spectator"),await l(),h||d(!0)}catch(x){console.error("Failed to enter spectator mode",x),h||d(!1)}}return p(),()=>{h=!0}},[o,n?.mode,n?.role,s,l]),f}function c2(){const[n,s]=m.useState(()=>typeof window>"u"?!1:window.matchMedia("(max-aspect-ratio: 4/3)").matches);return m.useEffect(()=>{if(typeof window>"u")return;const l=window.matchMedia("(max-aspect-ratio: 4/3)"),o=f=>s(f.matches);return l.addEventListener?l.addEventListener("change",o):l.addListener(o),()=>{l.removeEventListener?l.removeEventListener("change",o):l.removeListener(o)}},[]),n}function u2({roverId:n,session:s}){const l=s?.activeDrivers?.[n]||null,f=(s?.users||[]).find(x=>x.socketId===l)?.nickname||(l?l.slice(0,6):"No driver"),d=s?.mode,h=s?.turnQueues?.[n];return d==="turns"&&h?.current?`${f} (turns)`:f}function f2({rover:n,frame:s,sessionInfo:l,videoMode:o,snapshotFeed:f,audioInfo:d,session:h}){const p=u2({roverId:n.id,session:h});return r.jsx("article",{className:"min-h-[16rem] rounded bg-zinc-900 p-0 sm:min-h-[18rem]",children:r.jsx("div",{className:"min-h-0 overflow-hidden rounded bg-black/20",children:r.jsx(wi,{sessionInfo:l,videoMode:o,snapshotFeed:f,audioSessionInfo:d,label:n.name,roverColor:n.color||null,telemetryFrame:s,batteryConfig:n.battery,hudVariant:"spectator",driverLabel:p,hudForceMap:!0,hudMapPosition:"top-center"})})})}function d2({roster:n,frames:s,videoSources:l,snapshotFeeds:o,audioSources:f,session:d,canSpectateVideo:h}){return n.length===0?r.jsx("p",{className:"col-span-full text-slate-400",children:"No rovers registered."}):r.jsx("section",{className:"grid grid-cols-1 gap-0.5 md:grid-cols-2",children:n.map(p=>r.jsx(f2,{rover:p,frame:s[p.id],sessionInfo:h&&l[p.id]||null,videoMode:h?"whep":"snapshot",snapshotFeed:h?null:o[p.id],audioInfo:f[`${p.id}-audio`],session:d,showHudMap:!0,hudMapPosition:"bottom-left"},p.id))})}function m2(){return r.jsx("section",{className:"min-h-0",children:r.jsx("div",{className:"surface min-h-[14rem] overflow-hidden",children:r.jsx(fd,{defaultOrientation:"horizontal",hideLayoutToggle:!0,hideHeader:!0,panelId:"spectator-secondary"})})})}function h2({className:n=""}){return r.jsx("div",{className:`panel ${n}`,children:r.jsx(dd,{})})}function p2(){const{session:n}=nn(),s=n?.mode==="lockdown",l=!!n?.isLocalNetwork;Ad(),nv();const o=c2(),f=td(),d=n?.roster??[],h=md(d.map(w=>w.id),{enabled:!s&&!l,version:n?.mode}),p=l?d.map(w=>({type:"rover",id:w.id,key:w.id})):[],x=Ai(p,{enabled:!s&&l,version:n?.mode}),g=d.flatMap(w=>w.media?.audioPublishUrl?[{type:"rover",id:`${w.id}-audio`,key:`${w.id}-audio`}]:[]),v=Ai(g,{enabled:!s,version:n?.mode});if(s)return r.jsx("div",{className:"flex min-h-screen items-center justify-center bg-black text-slate-200",children:r.jsxs("div",{className:"surface max-w-md space-y-0.5 p-1 text-center text-sm",children:[r.jsx("p",{className:"text-lg font-semibold text-white",children:"Spectate disabled during lockdown."}),r.jsx("p",{className:"text-slate-300",children:"Please wait until the server leaves lockdown to view streams."})]})});const y=o?"flex min-h-screen flex-col bg-black text-slate-100 md:h-screen md:overflow-hidden":"grid min-h-screen grid-cols-1 gap-0.5 bg-black text-slate-100 md:h-full md:min-h-0 md:grid-cols-[minmax(0,1fr)_18rem] lg:grid-cols-[minmax(0,1fr)_20rem]",S=o?"order-2 flex min-h-0 min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto":"order-1 flex min-h-0 min-w-0 flex-col gap-0.5 md:overflow-y-auto",E=o?"order-1 grid w-full min-h-0 items-stretch gap-0.5 border-b border-slate-800/60 bg-slate-950/90 p-0.5 grid-cols-[minmax(0,1fr)_minmax(0,0.7fr)_minmax(0,1.9fr)_minmax(0,0.7fr)]":"order-2 flex min-h-0 min-w-0 flex-col gap-0.5 border-l border-slate-800/60 bg-slate-950/90 md:h-full md:overflow-y-auto",C="",k=o?"h-64":"";return r.jsxs("div",{className:"min-h-screen bg-black text-slate-100 md:h-screen md:overflow-hidden",children:[r.jsxs("main",{className:y,children:[r.jsxs("section",{className:E,children:[o?r.jsxs("div",{className:`${C} ${k} flex flex-col gap-0.5`,children:[r.jsx(Ao,{layout:"desktop",dismissable:!1,className:"text-sm"}),r.jsx(jo,{}),r.jsx("div",{className:"min-h-0 flex-1",children:r.jsx(So,{hideNicknameForm:!0,hideHeader:!0,compact:!0,fillHeight:!0,className:"h-full"})})]}):r.jsxs("div",{className:C,children:[r.jsx(Ao,{layout:"desktop",dismissable:!1,className:"text-sm"}),r.jsx(jo,{})]}),r.jsx("div",{className:`${C} ${o?`${k} overflow-y-auto`:""}`,children:r.jsx("div",{className:"panel h-full",children:r.jsx(Do,{title:"Rovers"})})}),o?null:r.jsx("div",{className:`${C} flex-1 min-h-0`,children:r.jsx(So,{hideNicknameForm:!0,hideHeader:!0,compact:!0,fillHeight:!0,className:"h-full"})}),r.jsx("div",{className:`${C} ${o?k:"flex-[1.1] min-h-0"}`,children:r.jsx(qo,{allowSpectatorInput:!0,hideSpectatorNotice:!0,fillHeight:!0})}),r.jsx("div",{className:`${C} ${o?k:""}`,children:r.jsx(h2,{className:`${o?"h-full":"h-40"} overflow-hidden`})})]}),r.jsxs("section",{className:S,children:[r.jsx(d2,{roster:d,frames:f,videoSources:x,snapshotFeeds:h,audioSources:v,session:n,canSpectateVideo:l}),r.jsx(m2,{})]})]}),r.jsx(id,{}),r.jsx(ev,{})]})}function x2(){return r.jsx(p2,{})}function g2({children:n}){return r.jsx("div",{className:"flex h-full w-full items-center justify-start overflow-hidden bg-black",children:r.jsx("div",{className:"relative flex items-center justify-center overflow-hidden bg-black",style:{width:"min(100%, calc(100vh * 4 / 3))",height:"min(100%, calc(100vw * 3 / 4))",maxWidth:"100%",maxHeight:"100%",aspectRatio:"4 / 3"},children:r.jsx("div",{className:"flex h-full w-full items-center justify-center overflow-hidden",children:n})})})}function v2({roverId:n,session:s}){const l=s?.activeDrivers?.[n]||null,f=(s?.users||[]).find(p=>p.socketId===l)?.nickname||(l?l.slice(0,6):"No driver"),d=s?.mode,h=s?.turnQueues?.[n];return d==="turns"&&h?.current?`${f}`:f}function b2({rover:n,frame:s}){const l=s?.sensors?.batteryChargeMah??null,o=n?.battery??null;return Ug({batteryState:n?.batteryState??null,charge:l,config:o})}function sl({children:n,className:s="",maxSize:l=1e3,minSize:o=14,style:f=void 0}){const d=m.useRef(null),h=m.useRef(null),[p,x]=m.useState(l);return m.useLayoutEffect(()=>{const g=d.current,v=h.current;if(!g||!v)return;let y=null;const S=()=>{const k=g.clientWidth;if(!k){E();return}let w=o,j=l,N=o;for(;w<=j;){const T=Math.floor((w+j)/2);v.style.fontSize=`${T}px`,v.scrollWidth<=k?(N=T,w=T+1):j=T-1}x(N)},E=()=>{y&&cancelAnimationFrame(y),y=requestAnimationFrame(S)};E();const C=new ResizeObserver(E);return C.observe(g),()=>{y&&cancelAnimationFrame(y),C.disconnect()}},[n,l,o]),r.jsx("div",{ref:d,className:"w-full min-w-0",children:r.jsx("div",{ref:h,className:`whitespace-nowrap ${s}`,style:{fontSize:`${p}px`,lineHeight:1.1,...f||{}},children:n})})}function Ex({rover:n,frame:s,driverLabel:l,sessionInfo:o,videoMode:f="snapshot",snapshotFeed:d,withDivider:h=!1,showPreview:p=!0,variant:x="stacked"}){const g=b2({rover:n,frame:s}),v=g?.available?g.percentDisplay:null,y=x==="active";return r.jsxs("div",{className:`relative flex min-w-0 flex-1 flex-col gap-4 overflow-hidden bg-black px-0 py-0 ${h?"border-r border-slate-700/60":""}`,children:[r.jsx(Hf,{visual:g,orientation:y?"vertical":"horizontal",variant:"background"}),y?r.jsxs("div",{className:"relative z-10 flex min-w-0 flex-1 flex-col justify-between text-center",children:[r.jsx("div",{className:"min-w-0 bg-transparent px-0 py-0 leading-none",children:r.jsx(sl,{className:"font-semibold leading-none text-white rounded border border-transparent px-1 py-[1px]",maxSize:1e3,minSize:18,style:ca(n.color,.18),children:n.name||n.id})}),l?r.jsx("div",{className:"min-w-0 bg-transparent px-0 py-0 leading-none",children:r.jsx(sl,{className:"font-semibold leading-none text-white",maxSize:1e3,minSize:16,children:l})}):r.jsx("div",{}),r.jsx("div",{className:"relative min-w-0 overflow-hidden bg-transparent px-0 py-0 leading-none",children:r.jsx("div",{className:"relative",children:r.jsx(sl,{className:"font-semibold leading-none text-white",maxSize:1e3,minSize:16,children:v==null?"--%":`${v}%`})})})]}):r.jsxs(r.Fragment,{children:[r.jsxs("div",{className:"relative z-10 flex min-w-0 flex-col gap-1 text-center",children:[r.jsx("div",{className:"min-w-0 bg-transparent px-0 py-0 leading-none",children:r.jsx(sl,{className:"font-semibold leading-none text-white rounded border border-transparent px-1 py-[1px]",maxSize:1e3,minSize:18,style:ca(n.color,.18),children:n.name||n.id})}),l?r.jsx("div",{className:"min-w-0 bg-transparent px-0 py-0 leading-none",children:r.jsx(sl,{className:"font-semibold leading-none text-white",maxSize:1e3,minSize:16,children:l})}):null,r.jsx("div",{className:"relative min-w-0 overflow-hidden bg-transparent px-0 py-0 leading-none",children:r.jsx("div",{className:"relative",children:r.jsx(sl,{className:"font-semibold leading-none text-white",maxSize:80,minSize:16,children:v==null?"--%":`${v}%`})})})]}),r.jsx("div",{className:"relative z-10 flex min-w-0 flex-1 flex-col gap-4",children:r.jsx("div",{className:"flex w-full min-w-0 flex-1 flex-col items-center gap-4",children:p?r.jsx("div",{className:"mt-auto w-full",children:r.jsx("div",{className:"w-full aspect-[4/3]",children:r.jsx(wi,{sessionInfo:o,videoMode:f,snapshotFeed:d,audioSessionInfo:null,label:n.name||n.id,roverColor:n.color||null,telemetryFrame:s,batteryConfig:n.battery,layoutFormat:"mobile",hudVariant:"none",fitParent:!0})})}):null})})]})]})}const y2=2e4,S2=3600*1e3;function j2(){const{session:n}=nn(),s=nv();Ad();const l=n?.mode==="lockdown",o=!!n?.isLocalNetwork,f=td(),d=n?.roster??[],[h,p]=m.useState(0),x=n?.activeDrivers||{},g=m.useMemo(()=>d.filter(F=>x[F.id]),[d,x]),v=g.length?g:d,y=md(v.map(F=>F.id),{enabled:!l&&!o,version:n?.mode}),S=m.useMemo(()=>o?v.map(F=>({type:"rover",id:F.id,key:F.id})):[],[o,v]),E=Ai(S,{enabled:!l&&o,version:n?.mode}),C=m.useMemo(()=>g.flatMap(F=>{if(!F?.id||!F.media?.audioPublishUrl)return[];const H=String(F.id);return[{type:"rover",id:`${H}-audio`,key:`${H}-audio`}]}),[g]),k=Ai(C,{enabled:!l,version:n?.mode}),w=m.useMemo(()=>{if(!g.length)return[];if(!o){const H=g.filter(te=>y[te.id]?.objectUrl);return H.length?H:g}const F=g.filter(H=>{const te=E[H.id];return te?.url&&!te?.error});return F.length?F:g},[g,y,E,o]),j=m.useMemo(()=>w.map(F=>({type:"rover",rover:F})),[w]),N=m.useMemo(()=>j.map(F=>`r:${F.rover.id}`).join("|"),[j]);m.useEffect(()=>{p(0)},[N]),m.useEffect(()=>{if(!j.length)return;const F=setInterval(()=>{p(H=>(H+1)%j.length)},y2);return()=>clearInterval(F)},[j.length,N]);const T=j.length?j[h%j.length]:null,M=T?.type==="rover"?T.rover:null,O=!o&&M&&y[M.id]||null,K=o&&M&&E[M.id]||null,_=M&&k[`${M.id}-audio`]||null,U=M&&f[M.id]||null,X=M?v2({roverId:M.id,session:n}):null;return l?r.jsx("div",{className:"relative flex h-screen w-screen items-center justify-center bg-black text-slate-200",children:r.jsxs("div",{className:"surface max-w-sm space-y-0.5 p-1 text-center text-sm",children:[r.jsx("p",{className:"text-lg font-semibold text-white",children:"Mini spectator is disabled in lockdown."}),r.jsx("p",{className:"text-slate-300",children:"It will automatically resume once lockdown ends."})]})}):g.length?r.jsxs("div",{className:"relative flex h-screen w-screen overflow-hidden bg-black text-slate-100",children:[r.jsx("section",{className:"relative flex h-full shrink-0 items-center justify-start overflow-hidden bg-black",style:{width:"min(100vw, calc(100vh * 4 / 3))"},children:s?M?r.jsx(g2,{children:o?r.jsx("div",{className:"relative h-full w-full",children:j.map(F=>{const H=F.rover,te=M?.id===H.id;return r.jsx("div",{className:`absolute inset-0 ${te?"opacity-100":"opacity-0 pointer-events-none"}`,children:r.jsx(wi,{sessionInfo:E[H.id]||null,videoMode:"whep",snapshotFeed:null,audioSessionInfo:te?_:null,forceMute:!te,label:H.name||H.id,roverColor:H.color||null,telemetryFrame:f[H.id]||null,batteryConfig:H.battery,layoutFormat:"mobile",hudVariant:"none",fitParent:!0})},H.id)})}):r.jsx(wi,{sessionInfo:null,videoMode:"snapshot",snapshotFeed:O,audioSessionInfo:_,label:M.name||M.id,roverColor:M.color||null,telemetryFrame:U,batteryConfig:M.battery,layoutFormat:"mobile",hudVariant:"none",fitParent:!0})}):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-500",children:g.length?"No sources available.":"No active drivers."}):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-500",children:"Switching to spectator…"})}),r.jsx("aside",{className:"flex h-full min-w-0 flex-1 flex-col border-l border-slate-800/60 bg-black",children:M?r.jsx(Ex,{rover:M,frame:U,driverLabel:X,sessionInfo:o?K:null,videoMode:o?"whep":"snapshot",snapshotFeed:o?null:y[M.id]||null,showPreview:!1,variant:"active"}):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-500",children:"No active rover."})})]}):r.jsx("div",{className:"relative flex h-screen w-screen overflow-hidden bg-black text-slate-100",children:r.jsx("section",{className:"flex h-full w-full gap-x-3 bg-slate-900 px-0",children:d.length?d.map(F=>r.jsx(Ex,{rover:F,frame:f[F.id]||null,driverLabel:null,sessionInfo:o&&E[F.id]||null,videoMode:o?"whep":"snapshot",snapshotFeed:o?null:y[F.id]||null,showPreview:!0,withDivider:!1},F.id)):r.jsx("div",{className:"flex h-full w-full items-center justify-center text-sm text-slate-500",children:"No rovers available."})})})}function A2(){return m.useEffect(()=>{if(typeof window>"u")return;const n=setTimeout(()=>{const s=new URL(window.location.href);s.searchParams.set("refresh",Date.now().toString()),window.location.replace(s.toString())},S2);return()=>clearTimeout(n)},[]),r.jsxs(r.Fragment,{children:[r.jsx(j2,{}),r.jsx(id,{scale:3})]})}function to(n){if(!n||!n.length)return null;const s=Math.floor(Math.random()*n.length);return n[s]||null}function N2(){if(typeof document>"u"||!document.body)return;const n=document.body.getElementsByTagName("*");if(!n||n.length<2)return;const s=to(n),l=to(n),o=s?.classList?.[0]||null;if(o&&l?.classList)try{l.classList.add(o)}catch{}const f=to(n),d=to(n),h=f?.classList?.[1]||null;if(h&&d?.classList)try{d.classList.add(h)}catch{}}function w2(){const n=ft(s=>!!s.session?.moderation?.isDeterred);return m.useEffect(()=>{if(!n)return;const s=window.setInterval(()=>{N2()},1);return()=>{window.clearInterval(s)}},[n]),null}Ny.createRoot(document.getElementById("root")).render(r.jsx(m.StrictMode,{children:r.jsx(QS,{children:r.jsx(JS,{children:r.jsx(PS,{children:r.jsx(rj,{children:r.jsxs(Jj,{children:[r.jsx(w2,{}),r.jsx(U1,{children:r.jsxs(x1,{children:[r.jsx(no,{path:"/",element:r.jsx(r2,{})}),r.jsx(no,{path:"/spectate",element:r.jsx(x2,{})}),r.jsx(no,{path:"/mini",element:r.jsx(A2,{})})]})})]})})})})})})); diff --git a/server/public/index.html b/server/public/index.html index bcf39c86..0c3a6a49 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -11,7 +11,7 @@ Multi Roomba Rover - + diff --git a/webui/src/mini/MiniSummaryApp.jsx b/webui/src/mini/MiniSummaryApp.jsx index f5b0ae75..5495c230 100644 --- a/webui/src/mini/MiniSummaryApp.jsx +++ b/webui/src/mini/MiniSummaryApp.jsx @@ -1,450 +1,3 @@ -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { useSession } from '../context/SessionContext.jsx'; -import { useTelemetryFrames } from '../context/TelemetryContext.jsx'; -import { useVideoRequests } from '../hooks/useVideoRequests.js'; -import { useRoverSnapshots } from '../hooks/useRoverSnapshots.js'; -import { useSpectatorMode } from '../hooks/useSpectatorMode.js'; -import VideoTile from '../components/VideoTile.jsx'; -import TopDownMap from '../components/TopDownMap.jsx'; -import AlertFeed from '../components/AlertFeed.jsx'; -import useDefaultNickname from '../hooks/useDefaultNickname.js'; -import BatteryBar from '../components/BatteryBar.jsx'; -import { buildBatteryVisual } from '../lib/battery.js'; -import { roverNameChromeStyle } from '../lib/roverColor.js'; +import MiniSummaryAppRoot from './MiniSummaryApp/MiniSummaryAppRoot.jsx'; -const ROTATE_MS = 20000; -const HARD_REFRESH_MS = 1 * 60 * 60 * 1000; - -function formatDriverLabel({ roverId, session }) { - const activeDriverId = session?.activeDrivers?.[roverId] || null; - const user = (session?.users || []).find((entry) => entry.socketId === activeDriverId); - const label = user?.nickname || (activeDriverId ? activeDriverId.slice(0, 6) : 'No driver'); - const mode = session?.mode; - const turnInfo = session?.turnQueues?.[roverId]; - return mode === 'turns' && turnInfo?.current ? `${label}` : label; -} - -function getBatteryVisual({ rover, frame }) { - const charge = frame?.sensors?.batteryChargeMah ?? null; - const config = rover?.battery ?? null; - return buildBatteryVisual({ batteryState: rover?.batteryState ?? null, charge, config }); -} - -function InfoColumn({ - rover, - frame, - driverLabel, - sessionInfo, - videoMode = 'snapshot', - snapshotFeed, - withDivider = false, - showPreview = true, - variant = 'stacked', -}) { - const batteryVisual = getBatteryVisual({ rover, frame }); - const batteryPercent = batteryVisual?.available ? batteryVisual.percentDisplay : null; - const isActiveView = variant === 'active'; - return ( -
- - {isActiveView ? ( -
-
- - {rover.name || rover.id} - -
- {driverLabel ? ( -
- - {driverLabel} - -
- ) : ( -
- )} -
-
- - {batteryPercent == null ? '--%' : `${batteryPercent}%`} - -
-
-
- ) : ( - <> -
-
- - {rover.name || rover.id} - -
- {driverLabel ? ( -
- - {driverLabel} - -
- ) : null} -
-
- - {batteryPercent == null ? '--%' : `${batteryPercent}%`} - -
-
-
- -
-
- {showPreview ? ( -
-
- -
-
- ) : null} -
-
- - )} -
- ); -} - -function AutoFitText({ children, className = '', maxSize = 1000, minSize = 14, style = undefined }) { - const containerRef = useRef(null); - const textRef = useRef(null); - const [fontSize, setFontSize] = useState(maxSize); - - useLayoutEffect(() => { - const container = containerRef.current; - const textEl = textRef.current; - if (!container || !textEl) return undefined; - - let raf = null; - const fit = () => { - const width = container.clientWidth; - if (!width) { - scheduleFit(); - return; - } - let low = minSize; - let high = maxSize; - let best = minSize; - while (low <= high) { - const mid = Math.floor((low + high) / 2); - textEl.style.fontSize = `${mid}px`; - const fits = textEl.scrollWidth <= width; - if (fits) { - best = mid; - low = mid + 1; - } else { - high = mid - 1; - } - } - setFontSize(best); - }; - - const scheduleFit = () => { - if (raf) cancelAnimationFrame(raf); - raf = requestAnimationFrame(fit); - }; - - scheduleFit(); - const ro = new ResizeObserver(scheduleFit); - ro.observe(container); - return () => { - if (raf) cancelAnimationFrame(raf); - ro.disconnect(); - }; - }, [children, maxSize, minSize]); - - return ( -
-
- {children} -
-
- ); -} - -function MiniSummaryContent() { - const { session } = useSession(); - const spectatorReady = useSpectatorMode(); - useDefaultNickname(); - const inLockdown = session?.mode === 'lockdown'; - const canSpectateVideo = Boolean(session?.isLocalNetwork); - const frames = useTelemetryFrames(); - const roster = session?.roster ?? []; - const [index, setIndex] = useState(0); - const activeDrivers = session?.activeDrivers || {}; - const driverRoster = useMemo( - () => roster.filter((rover) => activeDrivers[rover.id]), - [roster, activeDrivers], - ); - const snapshotRoster = driverRoster.length ? driverRoster : roster; - - const snapshotFeeds = useRoverSnapshots( - snapshotRoster.map((rover) => rover.id), - { enabled: !inLockdown && !canSpectateVideo, version: session?.mode }, - ); - const videoEntries = useMemo( - () => - canSpectateVideo - ? snapshotRoster.map((rover) => ({ type: 'rover', id: rover.id, key: rover.id })) - : [], - [canSpectateVideo, snapshotRoster], - ); - const videoSources = useVideoRequests(videoEntries, { enabled: !inLockdown && canSpectateVideo, version: session?.mode }); - const audioEntries = useMemo( - () => - driverRoster.flatMap((rover) => { - if (!rover?.id || !rover.media?.audioPublishUrl) return []; - const id = String(rover.id); - return [{ type: 'rover', id: `${id}-audio`, key: `${id}-audio` }]; - }), - [driverRoster], - ); - const audioSources = useVideoRequests(audioEntries, { enabled: !inLockdown, version: session?.mode }); - - const roverPool = useMemo(() => { - if (!driverRoster.length) return []; - if (!canSpectateVideo) { - const withSnapshot = driverRoster.filter((rover) => snapshotFeeds[rover.id]?.objectUrl); - return withSnapshot.length ? withSnapshot : driverRoster; - } - const withVideo = driverRoster.filter((rover) => { - const sessionInfo = videoSources[rover.id]; - return sessionInfo?.url && !sessionInfo?.error; - }); - return withVideo.length ? withVideo : driverRoster; - }, [driverRoster, snapshotFeeds, videoSources, canSpectateVideo]); - - const rotationPool = useMemo(() => { - return roverPool.map((rover) => ({ type: 'rover', rover })); - }, [roverPool]); - - const rotationKey = useMemo( - () => - rotationPool - .map((entry) => `r:${entry.rover.id}`) - .join('|'), - [rotationPool], - ); - - useEffect(() => { - setIndex(0); - }, [rotationKey]); - - useEffect(() => { - if (!rotationPool.length) return undefined; - const timer = setInterval(() => { - setIndex((prev) => (prev + 1) % rotationPool.length); - }, ROTATE_MS); - return () => clearInterval(timer); - }, [rotationPool.length, rotationKey]); - - const activeEntry = rotationPool.length ? rotationPool[index % rotationPool.length] : null; - const activeRover = activeEntry?.type === 'rover' ? activeEntry.rover : null; - - const activeSnapshot = !canSpectateVideo && activeRover ? snapshotFeeds[activeRover.id] || null : null; - const activeVideo = canSpectateVideo && activeRover ? videoSources[activeRover.id] || null : null; - const activeAudio = activeRover ? audioSources[`${activeRover.id}-audio`] || null : null; - const activeFrame = activeRover ? frames[activeRover.id] || null : null; - const driverLabel = activeRover ? formatDriverLabel({ roverId: activeRover.id, session }) : null; - - if (inLockdown) { - return ( -
-
-

Mini spectator is disabled in lockdown.

-

It will automatically resume once lockdown ends.

-
-
- ); - } - - if (!driverRoster.length) { - return ( -
-
- {roster.length ? ( - roster.map((rover, idx) => ( - - )) - ) : ( -
- No rovers available. -
- )} -
-
- ); - } - - return ( -
-
- {!spectatorReady ? ( -
- Switching to spectator… -
- ) : activeRover ? ( - - {canSpectateVideo ? ( -
- {rotationPool.map((entry) => { - const rover = entry.rover; - const isActive = activeRover?.id === rover.id; - return ( -
- -
- ); - })} -
- ) : ( - - )} -
- ) : ( -
- {driverRoster.length ? 'No sources available.' : 'No active drivers.'} -
- )} -
- -
- ); -} - -export default function MiniSummaryApp() { - useEffect(() => { - if (typeof window === 'undefined') return undefined; - const timer = setTimeout(() => { - const url = new URL(window.location.href); - url.searchParams.set('refresh', Date.now().toString()); - window.location.replace(url.toString()); - }, HARD_REFRESH_MS); - return () => clearTimeout(timer); - }, []); - - return ( - <> - - - - ); -} - -function FitViewportFrame({ children }) { - return ( -
-
-
{children}
-
-
- ); -} +export default MiniSummaryAppRoot; diff --git a/webui/src/mini/MiniSummaryApp/MiniSummaryAppRoot.jsx b/webui/src/mini/MiniSummaryApp/MiniSummaryAppRoot.jsx new file mode 100644 index 00000000..78c2f4c4 --- /dev/null +++ b/webui/src/mini/MiniSummaryApp/MiniSummaryAppRoot.jsx @@ -0,0 +1,24 @@ +// Top-level mini summary app shell with hard-refresh timer and alerts. +import { useEffect } from 'react'; +import AlertFeed from '../../components/AlertFeed.jsx'; +import MiniSummaryContent from './MiniSummaryContent.jsx'; +import { HARD_REFRESH_MS } from './constants.js'; + +export default function MiniSummaryAppRoot() { + useEffect(() => { + if (typeof window === 'undefined') return undefined; + const timer = setTimeout(() => { + const url = new URL(window.location.href); + url.searchParams.set('refresh', Date.now().toString()); + window.location.replace(url.toString()); + }, HARD_REFRESH_MS); + return () => clearTimeout(timer); + }, []); + + return ( + <> + + + + ); +} diff --git a/webui/src/mini/MiniSummaryApp/MiniSummaryContent.jsx b/webui/src/mini/MiniSummaryApp/MiniSummaryContent.jsx new file mode 100644 index 00000000..3b6b599c --- /dev/null +++ b/webui/src/mini/MiniSummaryApp/MiniSummaryContent.jsx @@ -0,0 +1,224 @@ +// Main mini summary screen composition and rotation logic. +import { useEffect, useMemo, useState } from 'react'; +import { useSession } from '../../context/SessionContext.jsx'; +import { useTelemetryFrames } from '../../context/TelemetryContext.jsx'; +import { useVideoRequests } from '../../hooks/useVideoRequests.js'; +import { useRoverSnapshots } from '../../hooks/useRoverSnapshots.js'; +import { useSpectatorMode } from '../../hooks/useSpectatorMode.js'; +import useDefaultNickname from '../../hooks/useDefaultNickname.js'; +import VideoTile from '../../components/VideoTile.jsx'; +import FitViewportFrame from './components/FitViewportFrame.jsx'; +import InfoColumn from './components/InfoColumn.jsx'; +import { ROTATE_MS } from './constants.js'; +import { formatDriverLabel } from './utils.js'; + +export default function MiniSummaryContent() { + const { session } = useSession(); + const spectatorReady = useSpectatorMode(); + useDefaultNickname(); + const inLockdown = session?.mode === 'lockdown'; + const canSpectateVideo = Boolean(session?.isLocalNetwork); + const frames = useTelemetryFrames(); + const roster = session?.roster ?? []; + const [index, setIndex] = useState(0); + const activeDrivers = session?.activeDrivers || {}; + const driverRoster = useMemo( + () => roster.filter((rover) => activeDrivers[rover.id]), + [roster, activeDrivers], + ); + const snapshotRoster = driverRoster.length ? driverRoster : roster; + + const snapshotFeeds = useRoverSnapshots( + snapshotRoster.map((rover) => rover.id), + { enabled: !inLockdown && !canSpectateVideo, version: session?.mode }, + ); + const videoEntries = useMemo( + () => + canSpectateVideo + ? snapshotRoster.map((rover) => ({ type: 'rover', id: rover.id, key: rover.id })) + : [], + [canSpectateVideo, snapshotRoster], + ); + const videoSources = useVideoRequests(videoEntries, { + enabled: !inLockdown && canSpectateVideo, + version: session?.mode, + }); + const audioEntries = useMemo( + () => + driverRoster.flatMap((rover) => { + if (!rover?.id || !rover.media?.audioPublishUrl) return []; + const id = String(rover.id); + return [{ type: 'rover', id: `${id}-audio`, key: `${id}-audio` }]; + }), + [driverRoster], + ); + const audioSources = useVideoRequests(audioEntries, { enabled: !inLockdown, version: session?.mode }); + + const roverPool = useMemo(() => { + if (!driverRoster.length) return []; + if (!canSpectateVideo) { + const withSnapshot = driverRoster.filter((rover) => snapshotFeeds[rover.id]?.objectUrl); + return withSnapshot.length ? withSnapshot : driverRoster; + } + const withVideo = driverRoster.filter((rover) => { + const sessionInfo = videoSources[rover.id]; + return sessionInfo?.url && !sessionInfo?.error; + }); + return withVideo.length ? withVideo : driverRoster; + }, [driverRoster, snapshotFeeds, videoSources, canSpectateVideo]); + + const rotationPool = useMemo(() => { + return roverPool.map((rover) => ({ type: 'rover', rover })); + }, [roverPool]); + + const rotationKey = useMemo( + () => + rotationPool + .map((entry) => `r:${entry.rover.id}`) + .join('|'), + [rotationPool], + ); + + useEffect(() => { + setIndex(0); + }, [rotationKey]); + + useEffect(() => { + if (!rotationPool.length) return undefined; + const timer = setInterval(() => { + setIndex((prev) => (prev + 1) % rotationPool.length); + }, ROTATE_MS); + return () => clearInterval(timer); + }, [rotationPool.length, rotationKey]); + + const activeEntry = rotationPool.length ? rotationPool[index % rotationPool.length] : null; + const activeRover = activeEntry?.type === 'rover' ? activeEntry.rover : null; + + const activeSnapshot = !canSpectateVideo && activeRover ? snapshotFeeds[activeRover.id] || null : null; + const activeVideo = canSpectateVideo && activeRover ? videoSources[activeRover.id] || null : null; + const activeAudio = activeRover ? audioSources[`${activeRover.id}-audio`] || null : null; + const activeFrame = activeRover ? frames[activeRover.id] || null : null; + const driverLabel = activeRover ? formatDriverLabel({ roverId: activeRover.id, session }) : null; + + if (inLockdown) { + return ( +
+
+

Mini spectator is disabled in lockdown.

+

It will automatically resume once lockdown ends.

+
+
+ ); + } + + if (!driverRoster.length) { + return ( +
+
+ {roster.length ? ( + roster.map((rover) => ( + + )) + ) : ( +
+ No rovers available. +
+ )} +
+
+ ); + } + + return ( +
+
+ {!spectatorReady ? ( +
+ Switching to spectator… +
+ ) : activeRover ? ( + + {canSpectateVideo ? ( +
+ {rotationPool.map((entry) => { + const rover = entry.rover; + const isActive = activeRover?.id === rover.id; + return ( +
+ +
+ ); + })} +
+ ) : ( + + )} +
+ ) : ( +
+ {driverRoster.length ? 'No sources available.' : 'No active drivers.'} +
+ )} +
+ +
+ ); +} diff --git a/webui/src/mini/MiniSummaryApp/components/AutoFitText.jsx b/webui/src/mini/MiniSummaryApp/components/AutoFitText.jsx new file mode 100644 index 00000000..8435b02a --- /dev/null +++ b/webui/src/mini/MiniSummaryApp/components/AutoFitText.jsx @@ -0,0 +1,63 @@ +// Auto-fitting single-line text component for mini summary layout. +import { useLayoutEffect, useRef, useState } from 'react'; + +export default function AutoFitText({ children, className = '', maxSize = 1000, minSize = 14, style = undefined }) { + const containerRef = useRef(null); + const textRef = useRef(null); + const [fontSize, setFontSize] = useState(maxSize); + + useLayoutEffect(() => { + const container = containerRef.current; + const textEl = textRef.current; + if (!container || !textEl) return undefined; + + let raf = null; + const fit = () => { + const width = container.clientWidth; + if (!width) { + scheduleFit(); + return; + } + let low = minSize; + let high = maxSize; + let best = minSize; + while (low <= high) { + const mid = Math.floor((low + high) / 2); + textEl.style.fontSize = `${mid}px`; + const fits = textEl.scrollWidth <= width; + if (fits) { + best = mid; + low = mid + 1; + } else { + high = mid - 1; + } + } + setFontSize(best); + }; + + const scheduleFit = () => { + if (raf) cancelAnimationFrame(raf); + raf = requestAnimationFrame(fit); + }; + + scheduleFit(); + const ro = new ResizeObserver(scheduleFit); + ro.observe(container); + return () => { + if (raf) cancelAnimationFrame(raf); + ro.disconnect(); + }; + }, [children, maxSize, minSize]); + + return ( +
+
+ {children} +
+
+ ); +} diff --git a/webui/src/mini/MiniSummaryApp/components/FitViewportFrame.jsx b/webui/src/mini/MiniSummaryApp/components/FitViewportFrame.jsx new file mode 100644 index 00000000..1903998f --- /dev/null +++ b/webui/src/mini/MiniSummaryApp/components/FitViewportFrame.jsx @@ -0,0 +1,19 @@ +// 4:3 viewport fitting frame for active mini-summary video. +export default function FitViewportFrame({ children }) { + return ( +
+
+
{children}
+
+
+ ); +} diff --git a/webui/src/mini/MiniSummaryApp/components/InfoColumn.jsx b/webui/src/mini/MiniSummaryApp/components/InfoColumn.jsx new file mode 100644 index 00000000..aae33879 --- /dev/null +++ b/webui/src/mini/MiniSummaryApp/components/InfoColumn.jsx @@ -0,0 +1,118 @@ +// Rover information and optional preview column for mini summary UI. +import VideoTile from '../../../components/VideoTile.jsx'; +import BatteryBar from '../../../components/BatteryBar.jsx'; +import { roverNameChromeStyle } from '../../../lib/roverColor.js'; +import { getBatteryVisual } from '../utils.js'; +import AutoFitText from './AutoFitText.jsx'; + +export default function InfoColumn({ + rover, + frame, + driverLabel, + sessionInfo, + videoMode = 'snapshot', + snapshotFeed, + withDivider = false, + showPreview = true, + variant = 'stacked', +}) { + const batteryVisual = getBatteryVisual({ rover, frame }); + const batteryPercent = batteryVisual?.available ? batteryVisual.percentDisplay : null; + const isActiveView = variant === 'active'; + return ( +
+ + {isActiveView ? ( +
+
+ + {rover.name || rover.id} + +
+ {driverLabel ? ( +
+ + {driverLabel} + +
+ ) : ( +
+ )} +
+
+ + {batteryPercent == null ? '--%' : `${batteryPercent}%`} + +
+
+
+ ) : ( + <> +
+
+ + {rover.name || rover.id} + +
+ {driverLabel ? ( +
+ + {driverLabel} + +
+ ) : null} +
+
+ + {batteryPercent == null ? '--%' : `${batteryPercent}%`} + +
+
+
+ +
+
+ {showPreview ? ( +
+
+ +
+
+ ) : null} +
+
+ + )} +
+ ); +} diff --git a/webui/src/mini/MiniSummaryApp/constants.js b/webui/src/mini/MiniSummaryApp/constants.js new file mode 100644 index 00000000..b92bfc89 --- /dev/null +++ b/webui/src/mini/MiniSummaryApp/constants.js @@ -0,0 +1,3 @@ +// Mini summary app timing constants. +export const ROTATE_MS = 20000; +export const HARD_REFRESH_MS = 1 * 60 * 60 * 1000; diff --git a/webui/src/mini/MiniSummaryApp/utils.js b/webui/src/mini/MiniSummaryApp/utils.js new file mode 100644 index 00000000..cb7df3cb --- /dev/null +++ b/webui/src/mini/MiniSummaryApp/utils.js @@ -0,0 +1,17 @@ +// Mini summary app session/telemetry formatting utilities. +import { buildBatteryVisual } from '../../lib/battery.js'; + +export function formatDriverLabel({ roverId, session }) { + const activeDriverId = session?.activeDrivers?.[roverId] || null; + const user = (session?.users || []).find((entry) => entry.socketId === activeDriverId); + const label = user?.nickname || (activeDriverId ? activeDriverId.slice(0, 6) : 'No driver'); + const mode = session?.mode; + const turnInfo = session?.turnQueues?.[roverId]; + return mode === 'turns' && turnInfo?.current ? `${label}` : label; +} + +export function getBatteryVisual({ rover, frame }) { + const charge = frame?.sensors?.batteryChargeMah ?? null; + const config = rover?.battery ?? null; + return buildBatteryVisual({ batteryState: rover?.batteryState ?? null, charge, config }); +}