diff --git a/pi/bin/video-publisher.sh b/pi/bin/video-publisher.sh index 4624d55d..83e4cb11 100644 --- a/pi/bin/video-publisher.sh +++ b/pi/bin/video-publisher.sh @@ -13,8 +13,8 @@ source "$ENV_FILE" : "${PUBLISH_URL:?PUBLISH_URL not set in ${ENV_FILE}}" -VIDEO_WIDTH="${VIDEO_WIDTH:-1280}" -VIDEO_HEIGHT="${VIDEO_HEIGHT:-720}" +VIDEO_WIDTH="${VIDEO_WIDTH:-1920}" +VIDEO_HEIGHT="${VIDEO_HEIGHT:-1080}" VIDEO_FPS="${VIDEO_FPS:-30}" VIDEO_BITRATE="${VIDEO_BITRATE:-3000000}" diff --git a/pi_camera_servo_outline.md b/pi_camera_servo_outline.md new file mode 100644 index 00000000..66632b32 --- /dev/null +++ b/pi_camera_servo_outline.md @@ -0,0 +1,13 @@ +# general idea +- servo connected to pin 19 on the pi + - it will allow the camera to look up and down +- use go-rpio + - it has a hardware PWM implementation +- mechanical design will have limits + - set the limits in roverd config +- control the servo from the web browser + - expose both a slider and buttons for "nudging" it up and down + +## verification of functionality before full implementation +- make sure that the go program can properly use the GPIO correctly before full implementation +- modify the roverd installer to add the needed stuff to enable PWM access \ No newline at end of file diff --git a/problems.md b/problems.md new file mode 100644 index 00000000..2474124c --- /dev/null +++ b/problems.md @@ -0,0 +1,17 @@ +## video stream +- video stream needs to always autoplay, even if it cant unmute itself + - currently, it wont play at all unless it can unmute +- video stream currently unloads and reloads based on the tab being active + - just remove this feature. It needs to stay running all the time. + +## server sensor decoding +- OI mode is not decoded properly + - says "safe" when in passive, and "unknown" when in full. + +## UI and session info +- UI needs a display for the turns mode + +## Mobile UI +- need to make it good +- joystick needs to be floating +- aux motor buttons need redone \ No newline at end of file diff --git a/server/index.js b/server/index.js index dee223b0..2f7bce17 100644 --- a/server/index.js +++ b/server/index.js @@ -17,4 +17,6 @@ require('./src/services/assignmentService'); require('./src/services/videoSessions'); require('./src/services/videoAuthService'); require('./src/services/videoSocketService'); +require('./src/services/logStreamService'); +require('./src/services/sessionService'); require('./src/services/httpServer'); diff --git a/server/mediamtx/mediamtx.yml b/server/mediamtx/mediamtx.yml index 8faf0ca1..ac8bf89a 100644 --- a/server/mediamtx/mediamtx.yml +++ b/server/mediamtx/mediamtx.yml @@ -33,3 +33,5 @@ paths: all: source: publisher sourceOnDemand: no + # webrtcMaxPlayoutDelay: 0 + # alwaysRemuxWhep: no diff --git a/server/public/assets/index-BvgUoEqO.js b/server/public/assets/index-BvgUoEqO.js new file mode 100644 index 00000000..5fef3562 --- /dev/null +++ b/server/public/assets/index-BvgUoEqO.js @@ -0,0 +1,11 @@ +(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const f of document.querySelectorAll('link[rel="modulepreload"]'))r(f);new MutationObserver(f=>{for(const d of f)if(d.type==="childList")for(const m of d.addedNodes)m.tagName==="LINK"&&m.rel==="modulepreload"&&r(m)}).observe(document,{childList:!0,subtree:!0});function c(f){const d={};return f.integrity&&(d.integrity=f.integrity),f.referrerPolicy&&(d.referrerPolicy=f.referrerPolicy),f.crossOrigin==="use-credentials"?d.credentials="include":f.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function r(f){if(f.ep)return;f.ep=!0;const d=c(f);fetch(f.href,d)}})();var lr={exports:{}},Ga={};var Zh;function M0(){if(Zh)return Ga;Zh=1;var u=Symbol.for("react.transitional.element"),i=Symbol.for("react.fragment");function c(r,f,d){var m=null;if(d!==void 0&&(m=""+d),f.key!==void 0&&(m=""+f.key),"key"in f){d={};for(var b in f)b!=="key"&&(d[b]=f[b])}else d=f;return f=d.ref,{$$typeof:u,type:r,key:m,ref:f!==void 0?f:null,props:d}}return Ga.Fragment=i,Ga.jsx=c,Ga.jsxs=c,Ga}var Kh;function D0(){return Kh||(Kh=1,lr.exports=M0()),lr.exports}var p=D0(),ar={exports:{}},I={};var Jh;function j0(){if(Jh)return I;Jh=1;var u=Symbol.for("react.transitional.element"),i=Symbol.for("react.portal"),c=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),f=Symbol.for("react.profiler"),d=Symbol.for("react.consumer"),m=Symbol.for("react.context"),b=Symbol.for("react.forward_ref"),y=Symbol.for("react.suspense"),v=Symbol.for("react.memo"),T=Symbol.for("react.lazy"),S=Symbol.for("react.activity"),C=Symbol.iterator;function z(_){return _===null||typeof _!="object"?null:(_=C&&_[C]||_["@@iterator"],typeof _=="function"?_:null)}var L={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},X=Object.assign,q={};function H(_,B,V){this.props=_,this.context=B,this.refs=q,this.updater=V||L}H.prototype.isReactComponent={},H.prototype.setState=function(_,B){if(typeof _!="object"&&typeof _!="function"&&_!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,_,B,"setState")},H.prototype.forceUpdate=function(_){this.updater.enqueueForceUpdate(this,_,"forceUpdate")};function Y(){}Y.prototype=H.prototype;function k(_,B,V){this.props=_,this.context=B,this.refs=q,this.updater=V||L}var F=k.prototype=new Y;F.constructor=k,X(F,H.prototype),F.isPureReactComponent=!0;var ye=Array.isArray;function Ne(){}var $={H:null,A:null,T:null,S:null},Te=Object.prototype.hasOwnProperty;function xe(_,B,V){var Z=V.ref;return{$$typeof:u,type:_,key:B,ref:Z!==void 0?Z:null,props:V}}function Ue(_,B){return xe(_.type,B,_.props)}function Xe(_){return typeof _=="object"&&_!==null&&_.$$typeof===u}function We(_){var B={"=":"=0",":":"=2"};return"$"+_.replace(/[=:]/g,function(V){return B[V]})}var Hn=/\/+/g;function Xt(_,B){return typeof _=="object"&&_!==null&&_.key!=null?We(""+_.key):B.toString(36)}function Rt(_){switch(_.status){case"fulfilled":return _.value;case"rejected":throw _.reason;default:switch(typeof _.status=="string"?_.then(Ne,Ne):(_.status="pending",_.then(function(B){_.status==="pending"&&(_.status="fulfilled",_.value=B)},function(B){_.status==="pending"&&(_.status="rejected",_.reason=B)})),_.status){case"fulfilled":return _.value;case"rejected":throw _.reason}}throw _}function j(_,B,V,Z,ee){var le=typeof _;(le==="undefined"||le==="boolean")&&(_=null);var he=!1;if(_===null)he=!0;else switch(le){case"bigint":case"string":case"number":he=!0;break;case"object":switch(_.$$typeof){case u:case i:he=!0;break;case T:return he=_._init,j(he(_._payload),B,V,Z,ee)}}if(he)return ee=ee(_),he=Z===""?"."+Xt(_,0):Z,ye(ee)?(V="",he!=null&&(V=he.replace(Hn,"$&/")+"/"),j(ee,B,V,"",function(Wl){return Wl})):ee!=null&&(Xe(ee)&&(ee=Ue(ee,V+(ee.key==null||_&&_.key===ee.key?"":(""+ee.key).replace(Hn,"$&/")+"/")+he)),B.push(ee)),1;he=0;var Pe=Z===""?".":Z+":";if(ye(_))for(var De=0;De<_.length;De++)Z=_[De],le=Pe+Xt(Z,De),he+=j(Z,B,V,le,ee);else if(De=z(_),typeof De=="function")for(_=De.call(_),De=0;!(Z=_.next()).done;)Z=Z.value,le=Pe+Xt(Z,De++),he+=j(Z,B,V,le,ee);else if(le==="object"){if(typeof _.then=="function")return j(Rt(_),B,V,Z,ee);throw B=String(_),Error("Objects are not valid as a React child (found: "+(B==="[object Object]"?"object with keys {"+Object.keys(_).join(", ")+"}":B)+"). If you meant to render a collection of children, use an array instead.")}return he}function G(_,B,V){if(_==null)return _;var Z=[],ee=0;return j(_,Z,"","",function(le){return B.call(V,le,ee++)}),Z}function P(_){if(_._status===-1){var B=_._result;B=B(),B.then(function(V){(_._status===0||_._status===-1)&&(_._status=1,_._result=V)},function(V){(_._status===0||_._status===-1)&&(_._status=2,_._result=V)}),_._status===-1&&(_._status=0,_._result=B)}if(_._status===1)return _._result.default;throw _._result}var ve=typeof reportError=="function"?reportError:function(_){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var B=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof _=="object"&&_!==null&&typeof _.message=="string"?String(_.message):String(_),error:_});if(!window.dispatchEvent(B))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",_);return}console.error(_)},_e={map:G,forEach:function(_,B,V){G(_,function(){B.apply(this,arguments)},V)},count:function(_){var B=0;return G(_,function(){B++}),B},toArray:function(_){return G(_,function(B){return B})||[]},only:function(_){if(!Xe(_))throw Error("React.Children.only expected to receive a single React element child.");return _}};return I.Activity=S,I.Children=_e,I.Component=H,I.Fragment=c,I.Profiler=f,I.PureComponent=k,I.StrictMode=r,I.Suspense=y,I.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=$,I.__COMPILER_RUNTIME={__proto__:null,c:function(_){return $.H.useMemoCache(_)}},I.cache=function(_){return function(){return _.apply(null,arguments)}},I.cacheSignal=function(){return null},I.cloneElement=function(_,B,V){if(_==null)throw Error("The argument must be a React element, but you passed "+_+".");var Z=X({},_.props),ee=_.key;if(B!=null)for(le in B.key!==void 0&&(ee=""+B.key),B)!Te.call(B,le)||le==="key"||le==="__self"||le==="__source"||le==="ref"&&B.ref===void 0||(Z[le]=B[le]);var le=arguments.length-2;if(le===1)Z.children=V;else if(1>>1,_e=j[ve];if(0>>1;ve<_;){var B=2*(ve+1)-1,V=j[B],Z=B+1,ee=j[Z];if(0>f(V,P))Z<_e&&0>f(ee,V)?(j[ve]=ee,j[Z]=P,ve=Z):(j[ve]=V,j[B]=P,ve=B);else if(Z<_e&&0>f(ee,P))j[ve]=ee,j[Z]=P,ve=Z;else break e}}return G}function f(j,G){var P=j.sortIndex-G.sortIndex;return P!==0?P:j.id-G.id}if(u.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var d=performance;u.unstable_now=function(){return d.now()}}else{var m=Date,b=m.now();u.unstable_now=function(){return m.now()-b}}var y=[],v=[],T=1,S=null,C=3,z=!1,L=!1,X=!1,q=!1,H=typeof setTimeout=="function"?setTimeout:null,Y=typeof clearTimeout=="function"?clearTimeout:null,k=typeof setImmediate<"u"?setImmediate:null;function F(j){for(var G=c(v);G!==null;){if(G.callback===null)r(v);else if(G.startTime<=j)r(v),G.sortIndex=G.expirationTime,i(y,G);else break;G=c(v)}}function ye(j){if(X=!1,F(j),!L)if(c(y)!==null)L=!0,Ne||(Ne=!0,We());else{var G=c(v);G!==null&&Rt(ye,G.startTime-j)}}var Ne=!1,$=-1,Te=5,xe=-1;function Ue(){return q?!0:!(u.unstable_now()-xej&&Ue());){var ve=S.callback;if(typeof ve=="function"){S.callback=null,C=S.priorityLevel;var _e=ve(S.expirationTime<=j);if(j=u.unstable_now(),typeof _e=="function"){S.callback=_e,F(j),G=!0;break t}S===c(y)&&r(y),F(j)}else r(y);S=c(y)}if(S!==null)G=!0;else{var _=c(v);_!==null&&Rt(ye,_.startTime-j),G=!1}}break e}finally{S=null,C=P,z=!1}G=void 0}}finally{G?We():Ne=!1}}}var We;if(typeof k=="function")We=function(){k(Xe)};else if(typeof MessageChannel<"u"){var Hn=new MessageChannel,Xt=Hn.port2;Hn.port1.onmessage=Xe,We=function(){Xt.postMessage(null)}}else We=function(){H(Xe,0)};function Rt(j,G){$=H(function(){j(u.unstable_now())},G)}u.unstable_IdlePriority=5,u.unstable_ImmediatePriority=1,u.unstable_LowPriority=4,u.unstable_NormalPriority=3,u.unstable_Profiling=null,u.unstable_UserBlockingPriority=2,u.unstable_cancelCallback=function(j){j.callback=null},u.unstable_forceFrameRate=function(j){0>j||125ve?(j.sortIndex=P,i(v,j),c(y)===null&&j===c(v)&&(X?(Y($),$=-1):X=!0,Rt(ye,P-ve))):(j.sortIndex=_e,i(y,j),L||z||(L=!0,Ne||(Ne=!0,We()))),j},u.unstable_shouldYield=Ue,u.unstable_wrapCallback=function(j){var G=C;return function(){var P=C;C=G;try{return j.apply(this,arguments)}finally{C=P}}}})(sr)),sr}var Fh;function U0(){return Fh||(Fh=1,ur.exports=w0()),ur.exports}var cr={exports:{}},Fe={};var Ph;function B0(){if(Ph)return Fe;Ph=1;var u=Mu();function i(y){var v="https://react.dev/errors/"+y;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(u)}catch(i){console.error(i)}}return u(),cr.exports=B0(),cr.exports}var em;function H0(){if(em)return Va;em=1;var u=U0(),i=Mu(),c=L0();function r(e){var t="https://react.dev/errors/"+e;if(1_e||(e.current=ve[_e],ve[_e]=null,_e--)}function V(e,t){_e++,ve[_e]=e.current,e.current=t}var Z=_(null),ee=_(null),le=_(null),he=_(null);function Pe(e,t){switch(V(le,t),V(ee,e),V(Z,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?ph(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=ph(t),e=yh(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}B(Z),V(Z,e)}function De(){B(Z),B(ee),B(le)}function Wl(e){e.memoizedState!==null&&V(he,e);var t=Z.current,n=yh(t,e.type);t!==n&&(V(ee,e),V(Z,n))}function ti(e){ee.current===e&&(B(Z),B(ee)),he.current===e&&(B(he),qa._currentValue=P)}var qu,Gr;function qn(e){if(qu===void 0)try{throw Error()}catch(n){var t=n.stack.trim().match(/\n( *(at )?)/);qu=t&&t[1]||"",Gr=-1)":-1a||g[l]!==R[a]){var D=` +`+g[l].replace(" at new "," at ");return e.displayName&&D.includes("")&&(D=D.replace("",e.displayName)),D}while(1<=l&&0<=a);break}}}finally{Yu=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:"")?qn(n):""}function cp(e,t){switch(e.tag){case 26:case 27:case 5:return qn(e.type);case 16:return qn("Lazy");case 13:return e.child!==t&&t!==null?qn("Suspense Fallback"):qn("Suspense");case 19:return qn("SuspenseList");case 0:case 15:return ku(e.type,!1);case 11:return ku(e.type.render,!1);case 1:return ku(e.type,!0);case 31:return qn("Activity");default:return""}}function Vr(e){try{var t="",n=null;do t+=cp(e,n),n=e,e=e.return;while(e);return t}catch(l){return` +Error generating stack: `+l.message+` +`+l.stack}}var Xu=Object.prototype.hasOwnProperty,Gu=u.unstable_scheduleCallback,Vu=u.unstable_cancelCallback,rp=u.unstable_shouldYield,op=u.unstable_requestPaint,st=u.unstable_now,fp=u.unstable_getCurrentPriorityLevel,Qr=u.unstable_ImmediatePriority,Zr=u.unstable_UserBlockingPriority,ni=u.unstable_NormalPriority,dp=u.unstable_LowPriority,Kr=u.unstable_IdlePriority,hp=u.log,mp=u.unstable_setDisableYieldValue,Fl=null,ct=null;function dn(e){if(typeof hp=="function"&&mp(e),ct&&typeof ct.setStrictMode=="function")try{ct.setStrictMode(Fl,e)}catch{}}var rt=Math.clz32?Math.clz32:vp,pp=Math.log,yp=Math.LN2;function vp(e){return e>>>=0,e===0?32:31-(pp(e)/yp|0)|0}var li=256,ai=262144,ii=4194304;function Yn(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function ui(e,t,n){var l=e.pendingLanes;if(l===0)return 0;var a=0,s=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var h=l&134217727;return h!==0?(l=h&~s,l!==0?a=Yn(l):(o&=h,o!==0?a=Yn(o):n||(n=h&~e,n!==0&&(a=Yn(n))))):(h=l&~s,h!==0?a=Yn(h):o!==0?a=Yn(o):n||(n=l&~e,n!==0&&(a=Yn(n)))),a===0?0:t!==0&&t!==a&&(t&s)===0&&(s=a&-a,n=t&-t,s>=n||s===32&&(n&4194048)!==0)?t:a}function Pl(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function gp(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Jr(){var e=ii;return ii<<=1,(ii&62914560)===0&&(ii=4194304),e}function Qu(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Il(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function bp(e,t,n,l,a,s){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var h=e.entanglements,g=e.expirationTimes,R=e.hiddenUpdates;for(n=o&~n;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var Ap=/[\n"\\]/g;function gt(e){return e.replace(Ap,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Fu(e,t,n,l,a,s,o,h){e.name="",o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"?e.type=o:e.removeAttribute("type"),t!=null?o==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+vt(t)):e.value!==""+vt(t)&&(e.value=""+vt(t)):o!=="submit"&&o!=="reset"||e.removeAttribute("value"),t!=null?Pu(e,o,vt(t)):n!=null?Pu(e,o,vt(n)):l!=null&&e.removeAttribute("value"),a==null&&s!=null&&(e.defaultChecked=!!s),a!=null&&(e.checked=a&&typeof a!="function"&&typeof a!="symbol"),h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"?e.name=""+vt(h):e.removeAttribute("name")}function so(e,t,n,l,a,s,o,h){if(s!=null&&typeof s!="function"&&typeof s!="symbol"&&typeof s!="boolean"&&(e.type=s),t!=null||n!=null){if(!(s!=="submit"&&s!=="reset"||t!=null)){Wu(e);return}n=n!=null?""+vt(n):"",t=t!=null?""+vt(t):n,h||t===e.value||(e.value=t),e.defaultValue=t}l=l??a,l=typeof l!="function"&&typeof l!="symbol"&&!!l,e.checked=h?e.checked:!!l,e.defaultChecked=!!l,o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"&&(e.name=o),Wu(e)}function Pu(e,t,n){t==="number"&&ri(e.ownerDocument)===e||e.defaultValue===""+n||(e.defaultValue=""+n)}function hl(e,t,n,l){if(e=e.options,t){t={};for(var a=0;a"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ls=!1;if(Qt)try{var la={};Object.defineProperty(la,"passive",{get:function(){ls=!0}}),window.addEventListener("test",la,la),window.removeEventListener("test",la,la)}catch{ls=!1}var mn=null,as=null,fi=null;function po(){if(fi)return fi;var e,t=as,n=t.length,l,a="value"in mn?mn.value:mn.textContent,s=a.length;for(e=0;e=ua),xo=" ",_o=!1;function Eo(e,t){switch(e){case"keyup":return Ip.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function To(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var vl=!1;function ty(e,t){switch(e){case"compositionend":return To(t);case"keypress":return t.which!==32?null:(_o=!0,xo);case"textInput":return e=t.data,e===xo&&_o?null:e;default:return null}}function ny(e,t){if(vl)return e==="compositionend"||!rs&&Eo(e,t)?(e=po(),fi=as=mn=null,vl=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=l}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Do(n)}}function wo(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?wo(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Uo(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=ri(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=ri(e.document)}return t}function ds(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var oy=Qt&&"documentMode"in document&&11>=document.documentMode,gl=null,hs=null,oa=null,ms=!1;function Bo(e,t,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ms||gl==null||gl!==ri(l)||(l=gl,"selectionStart"in l&&ds(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),oa&&ra(oa,l)||(oa=l,l=au(hs,"onSelect"),0>=o,a-=o,Ut=1<<32-rt(t)+a|n<ne?(se=K,K=null):se=K.sibling;var fe=O(A,K,N[ne],w);if(fe===null){K===null&&(K=se);break}e&&K&&fe.alternate===null&&t(A,K),x=s(fe,x,ne),oe===null?J=fe:oe.sibling=fe,oe=fe,K=se}if(ne===N.length)return n(A,K),ce&&Kt(A,ne),J;if(K===null){for(;nene?(se=K,K=null):se=K.sibling;var Bn=O(A,K,fe.value,w);if(Bn===null){K===null&&(K=se);break}e&&K&&Bn.alternate===null&&t(A,K),x=s(Bn,x,ne),oe===null?J=Bn:oe.sibling=Bn,oe=Bn,K=se}if(fe.done)return n(A,K),ce&&Kt(A,ne),J;if(K===null){for(;!fe.done;ne++,fe=N.next())fe=U(A,fe.value,w),fe!==null&&(x=s(fe,x,ne),oe===null?J=fe:oe.sibling=fe,oe=fe);return ce&&Kt(A,ne),J}for(K=l(K);!fe.done;ne++,fe=N.next())fe=M(K,A,ne,fe.value,w),fe!==null&&(e&&fe.alternate!==null&&K.delete(fe.key===null?ne:fe.key),x=s(fe,x,ne),oe===null?J=fe:oe.sibling=fe,oe=fe);return e&&K.forEach(function(z0){return t(A,z0)}),ce&&Kt(A,ne),J}function Se(A,x,N,w){if(typeof N=="object"&&N!==null&&N.type===X&&N.key===null&&(N=N.props.children),typeof N=="object"&&N!==null){switch(N.$$typeof){case z:e:{for(var J=N.key;x!==null;){if(x.key===J){if(J=N.type,J===X){if(x.tag===7){n(A,x.sibling),w=a(x,N.props.children),w.return=A,A=w;break e}}else if(x.elementType===J||typeof J=="object"&&J!==null&&J.$$typeof===Te&&Fn(J)===x.type){n(A,x.sibling),w=a(x,N.props),ya(w,N),w.return=A,A=w;break e}n(A,x);break}else t(A,x);x=x.sibling}N.type===X?(w=Zn(N.props.children,A.mode,w,N.key),w.return=A,A=w):(w=xi(N.type,N.key,N.props,null,A.mode,w),ya(w,N),w.return=A,A=w)}return o(A);case L:e:{for(J=N.key;x!==null;){if(x.key===J)if(x.tag===4&&x.stateNode.containerInfo===N.containerInfo&&x.stateNode.implementation===N.implementation){n(A,x.sibling),w=a(x,N.children||[]),w.return=A,A=w;break e}else{n(A,x);break}else t(A,x);x=x.sibling}w=xs(N,A.mode,w),w.return=A,A=w}return o(A);case Te:return N=Fn(N),Se(A,x,N,w)}if(Rt(N))return Q(A,x,N,w);if(We(N)){if(J=We(N),typeof J!="function")throw Error(r(150));return N=J.call(N),W(A,x,N,w)}if(typeof N.then=="function")return Se(A,x,Ri(N),w);if(N.$$typeof===k)return Se(A,x,Ti(A,N),w);Oi(A,N)}return typeof N=="string"&&N!==""||typeof N=="number"||typeof N=="bigint"?(N=""+N,x!==null&&x.tag===6?(n(A,x.sibling),w=a(x,N),w.return=A,A=w):(n(A,x),w=Ss(N,A.mode,w),w.return=A,A=w),o(A)):n(A,x)}return function(A,x,N,w){try{pa=0;var J=Se(A,x,N,w);return Ol=null,J}catch(K){if(K===Rl||K===Ci)throw K;var oe=ft(29,K,null,A.mode);return oe.lanes=w,oe.return=A,oe}finally{}}}var In=uf(!0),sf=uf(!1),bn=!1;function js(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function ws(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Sn(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function xn(e,t,n){var l=e.updateQueue;if(l===null)return null;if(l=l.shared,(de&2)!==0){var a=l.pending;return a===null?t.next=t:(t.next=a.next,a.next=t),l.pending=t,t=Si(e),Go(e,null,n),t}return bi(e,l,t,n),Si(e)}function va(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194048)!==0)){var l=t.lanes;l&=e.pendingLanes,n|=l,t.lanes=n,Wr(e,n)}}function Us(e,t){var n=e.updateQueue,l=e.alternate;if(l!==null&&(l=l.updateQueue,n===l)){var a=null,s=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};s===null?a=s=o:s=s.next=o,n=n.next}while(n!==null);s===null?a=s=t:s=s.next=t}else a=s=t;n={baseState:l.baseState,firstBaseUpdate:a,lastBaseUpdate:s,shared:l.shared,callbacks:l.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Bs=!1;function ga(){if(Bs){var e=Nl;if(e!==null)throw e}}function ba(e,t,n,l){Bs=!1;var a=e.updateQueue;bn=!1;var s=a.firstBaseUpdate,o=a.lastBaseUpdate,h=a.shared.pending;if(h!==null){a.shared.pending=null;var g=h,R=g.next;g.next=null,o===null?s=R:o.next=R,o=g;var D=e.alternate;D!==null&&(D=D.updateQueue,h=D.lastBaseUpdate,h!==o&&(h===null?D.firstBaseUpdate=R:h.next=R,D.lastBaseUpdate=g))}if(s!==null){var U=a.baseState;o=0,D=R=g=null,h=s;do{var O=h.lane&-536870913,M=O!==h.lane;if(M?(ue&O)===O:(l&O)===O){O!==0&&O===Cl&&(Bs=!0),D!==null&&(D=D.next={lane:0,tag:h.tag,payload:h.payload,callback:null,next:null});e:{var Q=e,W=h;O=t;var Se=n;switch(W.tag){case 1:if(Q=W.payload,typeof Q=="function"){U=Q.call(Se,U,O);break e}U=Q;break e;case 3:Q.flags=Q.flags&-65537|128;case 0:if(Q=W.payload,O=typeof Q=="function"?Q.call(Se,U,O):Q,O==null)break e;U=S({},U,O);break e;case 2:bn=!0}}O=h.callback,O!==null&&(e.flags|=64,M&&(e.flags|=8192),M=a.callbacks,M===null?a.callbacks=[O]:M.push(O))}else M={lane:O,tag:h.tag,payload:h.payload,callback:h.callback,next:null},D===null?(R=D=M,g=U):D=D.next=M,o|=O;if(h=h.next,h===null){if(h=a.shared.pending,h===null)break;M=h,h=M.next,M.next=null,a.lastBaseUpdate=M,a.shared.pending=null}}while(!0);D===null&&(g=U),a.baseState=g,a.firstBaseUpdate=R,a.lastBaseUpdate=D,s===null&&(a.shared.lanes=0),Cn|=o,e.lanes=o,e.memoizedState=U}}function cf(e,t){if(typeof e!="function")throw Error(r(191,e));e.call(t)}function rf(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;es?s:8;var o=j.T,h={};j.T=h,tc(e,!1,t,n);try{var g=a(),R=j.S;if(R!==null&&R(h,g),g!==null&&typeof g=="object"&&typeof g.then=="function"){var D=by(g,l);_a(e,t,D,yt(e))}else _a(e,t,l,yt(e))}catch(U){_a(e,t,{then:function(){},status:"rejected",reason:U},yt())}finally{G.p=s,o!==null&&h.types!==null&&(o.types=h.types),j.T=o}}function Ay(){}function Is(e,t,n,l){if(e.tag!==5)throw Error(r(476));var a=kf(e).queue;Yf(e,a,t,P,n===null?Ay:function(){return Xf(e),n(l)})}function kf(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:P,baseState:P,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ft,lastRenderedState:P},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ft,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Xf(e){var t=kf(e);t.next===null&&(t=e.alternate.memoizedState),_a(e,t.next.queue,{},yt())}function ec(){return Ke(qa)}function Gf(){return we().memoizedState}function Vf(){return we().memoizedState}function Cy(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=yt();e=Sn(n);var l=xn(t,e,n);l!==null&&(ut(l,t,n),va(l,t,n)),t={cache:Os()},e.payload=t;return}t=t.return}}function Ny(e,t,n){var l=yt();n={lane:l,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},qi(e)?Zf(t,n):(n=gs(e,t,n,l),n!==null&&(ut(n,e,l),Kf(n,t,l)))}function Qf(e,t,n){var l=yt();_a(e,t,n,l)}function _a(e,t,n,l){var a={lane:l,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(qi(e))Zf(t,a);else{var s=e.alternate;if(e.lanes===0&&(s===null||s.lanes===0)&&(s=t.lastRenderedReducer,s!==null))try{var o=t.lastRenderedState,h=s(o,n);if(a.hasEagerState=!0,a.eagerState=h,ot(h,o))return bi(e,t,a,0),Ee===null&&gi(),!1}catch{}finally{}if(n=gs(e,t,a,l),n!==null)return ut(n,e,l),Kf(n,t,l),!0}return!1}function tc(e,t,n,l){if(l={lane:2,revertLane:jc(),gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},qi(e)){if(t)throw Error(r(479))}else t=gs(e,n,l,2),t!==null&&ut(t,e,2)}function qi(e){var t=e.alternate;return e===te||t!==null&&t===te}function Zf(e,t){Ml=Di=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Kf(e,t,n){if((n&4194048)!==0){var l=t.lanes;l&=e.pendingLanes,n|=l,t.lanes=n,Wr(e,n)}}var Ea={readContext:Ke,use:Ui,useCallback:ze,useContext:ze,useEffect:ze,useImperativeHandle:ze,useLayoutEffect:ze,useInsertionEffect:ze,useMemo:ze,useReducer:ze,useRef:ze,useState:ze,useDebugValue:ze,useDeferredValue:ze,useTransition:ze,useSyncExternalStore:ze,useId:ze,useHostTransitionStatus:ze,useFormState:ze,useActionState:ze,useOptimistic:ze,useMemoCache:ze,useCacheRefresh:ze};Ea.useEffectEvent=ze;var Jf={readContext:Ke,use:Ui,useCallback:function(e,t){return Ie().memoizedState=[e,t===void 0?null:t],e},useContext:Ke,useEffect:Mf,useImperativeHandle:function(e,t,n){n=n!=null?n.concat([e]):null,Li(4194308,4,Uf.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Li(4194308,4,e,t)},useInsertionEffect:function(e,t){Li(4,2,e,t)},useMemo:function(e,t){var n=Ie();t=t===void 0?null:t;var l=e();if(el){dn(!0);try{e()}finally{dn(!1)}}return n.memoizedState=[l,t],l},useReducer:function(e,t,n){var l=Ie();if(n!==void 0){var a=n(t);if(el){dn(!0);try{n(t)}finally{dn(!1)}}}else a=t;return l.memoizedState=l.baseState=a,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:a},l.queue=e,e=e.dispatch=Ny.bind(null,te,e),[l.memoizedState,e]},useRef:function(e){var t=Ie();return e={current:e},t.memoizedState=e},useState:function(e){e=Js(e);var t=e.queue,n=Qf.bind(null,te,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Fs,useDeferredValue:function(e,t){var n=Ie();return Ps(n,e,t)},useTransition:function(){var e=Js(!1);return e=Yf.bind(null,te,e.queue,!0,!1),Ie().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var l=te,a=Ie();if(ce){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Ee===null)throw Error(r(349));(ue&127)!==0||pf(l,t,n)}a.memoizedState=n;var s={value:n,getSnapshot:t};return a.queue=s,Mf(vf.bind(null,l,s,e),[e]),l.flags|=2048,jl(9,{destroy:void 0},yf.bind(null,l,s,n,t),null),n},useId:function(){var e=Ie(),t=Ee.identifierPrefix;if(ce){var n=Bt,l=Ut;n=(l&~(1<<32-rt(l)-1)).toString(32)+n,t="_"+t+"R_"+n,n=ji++,0<\/script>",s=s.removeChild(s.firstChild);break;case"select":s=typeof l.is=="string"?o.createElement("select",{is:l.is}):o.createElement("select"),l.multiple?s.multiple=!0:l.size&&(s.size=l.size);break;default:s=typeof l.is=="string"?o.createElement(a,{is:l.is}):o.createElement(a)}}s[Qe]=t,s[et]=l;e:for(o=t.child;o!==null;){if(o.tag===5||o.tag===6)s.appendChild(o.stateNode);else if(o.tag!==4&&o.tag!==27&&o.child!==null){o.child.return=o,o=o.child;continue}if(o===t)break e;for(;o.sibling===null;){if(o.return===null||o.return===t)break e;o=o.return}o.sibling.return=o.return,o=o.sibling}t.stateNode=s;e:switch($e(s,a,l),a){case"button":case"input":case"select":case"textarea":l=!!l.autoFocus;break e;case"img":l=!0;break e;default:l=!1}l&&It(t)}}return Ce(t),pc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==l&&It(t);else{if(typeof l!="string"&&t.stateNode===null)throw Error(r(166));if(e=le.current,Tl(t)){if(e=t.stateNode,n=t.memoizedProps,l=null,a=Ze,a!==null)switch(a.tag){case 27:case 5:l=a.memoizedProps}e[Qe]=t,e=!!(e.nodeValue===n||l!==null&&l.suppressHydrationWarning===!0||hh(e.nodeValue,n)),e||vn(t,!0)}else e=iu(e).createTextNode(l),e[Qe]=t,t.stateNode=e}return Ce(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(l=Tl(t),n!==null){if(e===null){if(!l)throw Error(r(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(r(557));e[Qe]=t}else Kn(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Ce(t),e=!1}else n=As(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(ht(t),t):(ht(t),null);if((t.flags&128)!==0)throw Error(r(558))}return Ce(t),null;case 13:if(l=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Tl(t),l!==null&&l.dehydrated!==null){if(e===null){if(!a)throw Error(r(318));if(a=t.memoizedState,a=a!==null?a.dehydrated:null,!a)throw Error(r(317));a[Qe]=t}else Kn(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Ce(t),a=!1}else a=As(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(ht(t),t):(ht(t),null)}return ht(t),(t.flags&128)!==0?(t.lanes=n,t):(n=l!==null,e=e!==null&&e.memoizedState!==null,n&&(l=t.child,a=null,l.alternate!==null&&l.alternate.memoizedState!==null&&l.alternate.memoizedState.cachePool!==null&&(a=l.alternate.memoizedState.cachePool.pool),s=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(s=l.memoizedState.cachePool.pool),s!==a&&(l.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Vi(t,t.updateQueue),Ce(t),null);case 4:return De(),e===null&&Lc(t.stateNode.containerInfo),Ce(t),null;case 10:return $t(t.type),Ce(t),null;case 19:if(B(je),l=t.memoizedState,l===null)return Ce(t),null;if(a=(t.flags&128)!==0,s=l.rendering,s===null)if(a)Aa(l,!1);else{if(Me!==0||e!==null&&(e.flags&128)!==0)for(e=t.child;e!==null;){if(s=Mi(e),s!==null){for(t.flags|=128,Aa(l,!1),e=s.updateQueue,t.updateQueue=e,Vi(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)Vo(n,e),n=n.sibling;return V(je,je.current&1|2),ce&&Kt(t,l.treeForkCount),t.child}e=e.sibling}l.tail!==null&&st()>$i&&(t.flags|=128,a=!0,Aa(l,!1),t.lanes=4194304)}else{if(!a)if(e=Mi(s),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Vi(t,e),Aa(l,!0),l.tail===null&&l.tailMode==="hidden"&&!s.alternate&&!ce)return Ce(t),null}else 2*st()-l.renderingStartTime>$i&&n!==536870912&&(t.flags|=128,a=!0,Aa(l,!1),t.lanes=4194304);l.isBackwards?(s.sibling=t.child,t.child=s):(e=l.last,e!==null?e.sibling=s:t.child=s,l.last=s)}return l.tail!==null?(e=l.tail,l.rendering=e,l.tail=e.sibling,l.renderingStartTime=st(),e.sibling=null,n=je.current,V(je,a?n&1|2:n&1),ce&&Kt(t,l.treeForkCount),e):(Ce(t),null);case 22:case 23:return ht(t),Hs(),l=t.memoizedState!==null,e!==null?e.memoizedState!==null!==l&&(t.flags|=8192):l&&(t.flags|=8192),l?(n&536870912)!==0&&(t.flags&128)===0&&(Ce(t),t.subtreeFlags&6&&(t.flags|=8192)):Ce(t),n=t.updateQueue,n!==null&&Vi(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),l=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(l=t.memoizedState.cachePool.pool),l!==n&&(t.flags|=2048),e!==null&&B(Wn),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),$t(Be),Ce(t),null;case 25:return null;case 30:return null}throw Error(r(156,t.tag))}function Dy(e,t){switch(Es(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return $t(Be),De(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return ti(t),null;case 31:if(t.memoizedState!==null){if(ht(t),t.alternate===null)throw Error(r(340));Kn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(ht(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Kn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return B(je),null;case 4:return De(),null;case 10:return $t(t.type),null;case 22:case 23:return ht(t),Hs(),e!==null&&B(Wn),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return $t(Be),null;case 25:return null;default:return null}}function gd(e,t){switch(Es(t),t.tag){case 3:$t(Be),De();break;case 26:case 27:case 5:ti(t);break;case 4:De();break;case 31:t.memoizedState!==null&&ht(t);break;case 13:ht(t);break;case 19:B(je);break;case 10:$t(t.type);break;case 22:case 23:ht(t),Hs(),e!==null&&B(Wn);break;case 24:$t(Be)}}function Ca(e,t){try{var n=t.updateQueue,l=n!==null?n.lastEffect:null;if(l!==null){var a=l.next;n=a;do{if((n.tag&e)===e){l=void 0;var s=n.create,o=n.inst;l=s(),o.destroy=l}n=n.next}while(n!==a)}}catch(h){pe(t,t.return,h)}}function Tn(e,t,n){try{var l=t.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var s=a.next;l=s;do{if((l.tag&e)===e){var o=l.inst,h=o.destroy;if(h!==void 0){o.destroy=void 0,a=t;var g=n,R=h;try{R()}catch(D){pe(a,g,D)}}}l=l.next}while(l!==s)}}catch(D){pe(t,t.return,D)}}function bd(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{rf(t,n)}catch(l){pe(e,e.return,l)}}}function Sd(e,t,n){n.props=tl(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(l){pe(e,t,l)}}function Na(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var l=e.stateNode;break;case 30:l=e.stateNode;break;default:l=e.stateNode}typeof n=="function"?e.refCleanup=n(l):n.current=l}}catch(a){pe(e,t,a)}}function Lt(e,t){var n=e.ref,l=e.refCleanup;if(n!==null)if(typeof l=="function")try{l()}catch(a){pe(e,t,a)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n=="function")try{n(null)}catch(a){pe(e,t,a)}else n.current=null}function xd(e){var t=e.type,n=e.memoizedProps,l=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":n.autoFocus&&l.focus();break e;case"img":n.src?l.src=n.src:n.srcSet&&(l.srcset=n.srcSet)}}catch(a){pe(e,e.return,a)}}function yc(e,t,n){try{var l=e.stateNode;e0(l,e.type,n,t),l[et]=t}catch(a){pe(e,e.return,a)}}function _d(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Mn(e.type)||e.tag===4}function vc(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||_d(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Mn(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function gc(e,t,n){var l=e.tag;if(l===5||l===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Vt));else if(l!==4&&(l===27&&Mn(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(gc(e,t,n),e=e.sibling;e!==null;)gc(e,t,n),e=e.sibling}function Qi(e,t,n){var l=e.tag;if(l===5||l===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(l!==4&&(l===27&&Mn(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Qi(e,t,n),e=e.sibling;e!==null;)Qi(e,t,n),e=e.sibling}function Ed(e){var t=e.stateNode,n=e.memoizedProps;try{for(var l=e.type,a=t.attributes;a.length;)t.removeAttributeNode(a[0]);$e(t,l,n),t[Qe]=e,t[et]=n}catch(s){pe(e,e.return,s)}}var en=!1,qe=!1,bc=!1,Td=typeof WeakSet=="function"?WeakSet:Set,Ve=null;function jy(e,t){if(e=e.containerInfo,Yc=du,e=Uo(e),ds(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var l=n.getSelection&&n.getSelection();if(l&&l.rangeCount!==0){n=l.anchorNode;var a=l.anchorOffset,s=l.focusNode;l=l.focusOffset;try{n.nodeType,s.nodeType}catch{n=null;break e}var o=0,h=-1,g=-1,R=0,D=0,U=e,O=null;t:for(;;){for(var M;U!==n||a!==0&&U.nodeType!==3||(h=o+a),U!==s||l!==0&&U.nodeType!==3||(g=o+l),U.nodeType===3&&(o+=U.nodeValue.length),(M=U.firstChild)!==null;)O=U,U=M;for(;;){if(U===e)break t;if(O===n&&++R===a&&(h=o),O===s&&++D===l&&(g=o),(M=U.nextSibling)!==null)break;U=O,O=U.parentNode}U=M}n=h===-1||g===-1?null:{start:h,end:g}}else n=null}n=n||{start:0,end:0}}else n=null;for(kc={focusedElem:e,selectionRange:n},du=!1,Ve=t;Ve!==null;)if(t=Ve,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,Ve=e;else for(;Ve!==null;){switch(t=Ve,s=t.alternate,e=t.flags,t.tag){case 0:if((e&4)!==0&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(n=0;n title"))),$e(s,l,n),s[Qe]=e,Ge(s),l=s;break e;case"link":var o=zh("link","href",a).get(l+(n.href||""));if(o){for(var h=0;hSe&&(o=Se,Se=W,W=o);var A=jo(h,W),x=jo(h,Se);if(A&&x&&(M.rangeCount!==1||M.anchorNode!==A.node||M.anchorOffset!==A.offset||M.focusNode!==x.node||M.focusOffset!==x.offset)){var N=U.createRange();N.setStart(A.node,A.offset),M.removeAllRanges(),W>Se?(M.addRange(N),M.extend(x.node,x.offset)):(N.setEnd(x.node,x.offset),M.addRange(N))}}}}for(U=[],M=h;M=M.parentNode;)M.nodeType===1&&U.push({element:M,left:M.scrollLeft,top:M.scrollTop});for(typeof h.focus=="function"&&h.focus(),h=0;hn?32:n,j.T=null,n=Cc,Cc=null;var s=Rn,o=un;if(ke=0,Hl=Rn=null,un=0,(de&6)!==0)throw Error(r(331));var h=de;if(de|=4,Ud(s.current),Dd(s,s.current,o,n),de=h,ja(0,!1),ct&&typeof ct.onPostCommitFiberRoot=="function")try{ct.onPostCommitFiberRoot(Fl,s)}catch{}return!0}finally{G.p=a,j.T=l,Id(e,t)}}function th(e,t,n){t=St(n,t),t=ic(e.stateNode,t,2),e=xn(e,t,2),e!==null&&(Il(e,2),Ht(e))}function pe(e,t,n){if(e.tag===3)th(e,e,n);else for(;t!==null;){if(t.tag===3){th(t,e,n);break}else if(t.tag===1){var l=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof l.componentDidCatch=="function"&&(Nn===null||!Nn.has(l))){e=St(n,e),n=nd(2),l=xn(t,n,2),l!==null&&(ld(n,l,t,e),Il(l,2),Ht(l));break}}t=t.return}}function zc(e,t,n){var l=e.pingCache;if(l===null){l=e.pingCache=new By;var a=new Set;l.set(t,a)}else a=l.get(t),a===void 0&&(a=new Set,l.set(t,a));a.has(n)||(_c=!0,a.add(n),e=ky.bind(null,e,t,n),t.then(e,e))}function ky(e,t,n){var l=e.pingCache;l!==null&&l.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Ee===e&&(ue&n)===n&&(Me===4||Me===3&&(ue&62914560)===ue&&300>st()-Ji?(de&2)===0&&ql(e,0):Ec|=n,Ll===ue&&(Ll=0)),Ht(e)}function nh(e,t){t===0&&(t=Jr()),e=Qn(e,t),e!==null&&(Il(e,t),Ht(e))}function Xy(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),nh(e,n)}function Gy(e,t){var n=0;switch(e.tag){case 31:case 13:var l=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:l=e.stateNode;break;case 22:l=e.stateNode._retryCache;break;default:throw Error(r(314))}l!==null&&l.delete(t),nh(e,n)}function Vy(e,t){return Gu(e,t)}var tu=null,kl=null,Mc=!1,nu=!1,Dc=!1,zn=0;function Ht(e){e!==kl&&e.next===null&&(kl===null?tu=kl=e:kl=kl.next=e),nu=!0,Mc||(Mc=!0,Zy())}function ja(e,t){if(!Dc&&nu){Dc=!0;do for(var n=!1,l=tu;l!==null;){if(e!==0){var a=l.pendingLanes;if(a===0)var s=0;else{var o=l.suspendedLanes,h=l.pingedLanes;s=(1<<31-rt(42|e)+1)-1,s&=a&~(o&~h),s=s&201326741?s&201326741|1:s?s|2:0}s!==0&&(n=!0,uh(l,s))}else s=ue,s=ui(l,l===Ee?s:0,l.cancelPendingCommit!==null||l.timeoutHandle!==-1),(s&3)===0||Pl(l,s)||(n=!0,uh(l,s));l=l.next}while(n);Dc=!1}}function Qy(){lh()}function lh(){nu=Mc=!1;var e=0;zn!==0&&n0()&&(e=zn);for(var t=st(),n=null,l=tu;l!==null;){var a=l.next,s=ah(l,t);s===0?(l.next=null,n===null?tu=a:n.next=a,a===null&&(kl=n)):(n=l,(e!==0||(s&3)!==0)&&(nu=!0)),l=a}ke!==0&&ke!==5||ja(e),zn!==0&&(zn=0)}function ah(e,t){for(var n=e.suspendedLanes,l=e.pingedLanes,a=e.expirationTimes,s=e.pendingLanes&-62914561;0h)break;var D=g.transferSize,U=g.initiatorType;D&&mh(U)&&(g=g.responseEnd,o+=D*(g"u"?null:document;function Ch(e,t,n){var l=Xl;if(l&&typeof t=="string"&&t){var a=gt(t);a='link[rel="'+e+'"][href="'+a+'"]',typeof n=="string"&&(a+='[crossorigin="'+n+'"]'),Ah.has(a)||(Ah.add(a),e={rel:e,crossOrigin:n,href:t},l.querySelector(a)===null&&(t=l.createElement("link"),$e(t,"link",e),Ge(t),l.head.appendChild(t)))}}function f0(e){sn.D(e),Ch("dns-prefetch",e,null)}function d0(e,t){sn.C(e,t),Ch("preconnect",e,t)}function h0(e,t,n){sn.L(e,t,n);var l=Xl;if(l&&e&&t){var a='link[rel="preload"][as="'+gt(t)+'"]';t==="image"&&n&&n.imageSrcSet?(a+='[imagesrcset="'+gt(n.imageSrcSet)+'"]',typeof n.imageSizes=="string"&&(a+='[imagesizes="'+gt(n.imageSizes)+'"]')):a+='[href="'+gt(e)+'"]';var s=a;switch(t){case"style":s=Gl(e);break;case"script":s=Vl(e)}Ct.has(s)||(e=S({rel:"preload",href:t==="image"&&n&&n.imageSrcSet?void 0:e,as:t},n),Ct.set(s,e),l.querySelector(a)!==null||t==="style"&&l.querySelector(La(s))||t==="script"&&l.querySelector(Ha(s))||(t=l.createElement("link"),$e(t,"link",e),Ge(t),l.head.appendChild(t)))}}function m0(e,t){sn.m(e,t);var n=Xl;if(n&&e){var l=t&&typeof t.as=="string"?t.as:"script",a='link[rel="modulepreload"][as="'+gt(l)+'"][href="'+gt(e)+'"]',s=a;switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":s=Vl(e)}if(!Ct.has(s)&&(e=S({rel:"modulepreload",href:e},t),Ct.set(s,e),n.querySelector(a)===null)){switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(n.querySelector(Ha(s)))return}l=n.createElement("link"),$e(l,"link",e),Ge(l),n.head.appendChild(l)}}}function p0(e,t,n){sn.S(e,t,n);var l=Xl;if(l&&e){var a=fl(l).hoistableStyles,s=Gl(e);t=t||"default";var o=a.get(s);if(!o){var h={loading:0,preload:null};if(o=l.querySelector(La(s)))h.loading=5;else{e=S({rel:"stylesheet",href:e,"data-precedence":t},n),(n=Ct.get(s))&&Jc(e,n);var g=o=l.createElement("link");Ge(g),$e(g,"link",e),g._p=new Promise(function(R,D){g.onload=R,g.onerror=D}),g.addEventListener("load",function(){h.loading|=1}),g.addEventListener("error",function(){h.loading|=2}),h.loading|=4,su(o,t,l)}o={type:"stylesheet",instance:o,count:1,state:h},a.set(s,o)}}}function y0(e,t){sn.X(e,t);var n=Xl;if(n&&e){var l=fl(n).hoistableScripts,a=Vl(e),s=l.get(a);s||(s=n.querySelector(Ha(a)),s||(e=S({src:e,async:!0},t),(t=Ct.get(a))&&$c(e,t),s=n.createElement("script"),Ge(s),$e(s,"link",e),n.head.appendChild(s)),s={type:"script",instance:s,count:1,state:null},l.set(a,s))}}function v0(e,t){sn.M(e,t);var n=Xl;if(n&&e){var l=fl(n).hoistableScripts,a=Vl(e),s=l.get(a);s||(s=n.querySelector(Ha(a)),s||(e=S({src:e,async:!0,type:"module"},t),(t=Ct.get(a))&&$c(e,t),s=n.createElement("script"),Ge(s),$e(s,"link",e),n.head.appendChild(s)),s={type:"script",instance:s,count:1,state:null},l.set(a,s))}}function Nh(e,t,n,l){var a=(a=le.current)?uu(a):null;if(!a)throw Error(r(446));switch(e){case"meta":case"title":return null;case"style":return typeof n.precedence=="string"&&typeof n.href=="string"?(t=Gl(n.href),n=fl(a).hoistableStyles,l=n.get(t),l||(l={type:"style",instance:null,count:0,state:null},n.set(t,l)),l):{type:"void",instance:null,count:0,state:null};case"link":if(n.rel==="stylesheet"&&typeof n.href=="string"&&typeof n.precedence=="string"){e=Gl(n.href);var s=fl(a).hoistableStyles,o=s.get(e);if(o||(a=a.ownerDocument||a,o={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},s.set(e,o),(s=a.querySelector(La(e)))&&!s._p&&(o.instance=s,o.state.loading=5),Ct.has(e)||(n={rel:"preload",as:"style",href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},Ct.set(e,n),s||g0(a,e,n,o.state))),t&&l===null)throw Error(r(528,""));return o}if(t&&l!==null)throw Error(r(529,""));return null;case"script":return t=n.async,n=n.src,typeof n=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=Vl(n),n=fl(a).hoistableScripts,l=n.get(t),l||(l={type:"script",instance:null,count:0,state:null},n.set(t,l)),l):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,e))}}function Gl(e){return'href="'+gt(e)+'"'}function La(e){return'link[rel="stylesheet"]['+e+"]"}function Rh(e){return S({},e,{"data-precedence":e.precedence,precedence:null})}function g0(e,t,n,l){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?l.loading=1:(t=e.createElement("link"),l.preload=t,t.addEventListener("load",function(){return l.loading|=1}),t.addEventListener("error",function(){return l.loading|=2}),$e(t,"link",n),Ge(t),e.head.appendChild(t))}function Vl(e){return'[src="'+gt(e)+'"]'}function Ha(e){return"script[async]"+e}function Oh(e,t,n){if(t.count++,t.instance===null)switch(t.type){case"style":var l=e.querySelector('style[data-href~="'+gt(n.href)+'"]');if(l)return t.instance=l,Ge(l),l;var a=S({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return l=(e.ownerDocument||e).createElement("style"),Ge(l),$e(l,"style",a),su(l,n.precedence,e),t.instance=l;case"stylesheet":a=Gl(n.href);var s=e.querySelector(La(a));if(s)return t.state.loading|=4,t.instance=s,Ge(s),s;l=Rh(n),(a=Ct.get(a))&&Jc(l,a),s=(e.ownerDocument||e).createElement("link"),Ge(s);var o=s;return o._p=new Promise(function(h,g){o.onload=h,o.onerror=g}),$e(s,"link",l),t.state.loading|=4,su(s,n.precedence,e),t.instance=s;case"script":return s=Vl(n.src),(a=e.querySelector(Ha(s)))?(t.instance=a,Ge(a),a):(l=n,(a=Ct.get(s))&&(l=S({},n),$c(l,a)),e=e.ownerDocument||e,a=e.createElement("script"),Ge(a),$e(a,"link",l),e.head.appendChild(a),t.instance=a);case"void":return null;default:throw Error(r(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(l=t.instance,t.state.loading|=4,su(l,n.precedence,e));return t.instance}function su(e,t,n){for(var l=n.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),a=l.length?l[l.length-1]:null,s=a,o=0;o title"):null)}function b0(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;switch(t.rel){case"stylesheet":return e=t.disabled,typeof t.precedence=="string"&&e==null;default:return!0}case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function Dh(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function S0(e,t,n,l){if(n.type==="stylesheet"&&(typeof l.media!="string"||matchMedia(l.media).matches!==!1)&&(n.state.loading&4)===0){if(n.instance===null){var a=Gl(l.href),s=t.querySelector(La(a));if(s){t=s._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=ru.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=s,Ge(s);return}s=t.ownerDocument||t,l=Rh(l),(a=Ct.get(a))&&Jc(l,a),s=s.createElement("link"),Ge(s);var o=s;o._p=new Promise(function(h,g){o.onload=h,o.onerror=g}),$e(s,"link",l),n.instance=s}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&(n.state.loading&3)===0&&(e.count++,n=ru.bind(e),t.addEventListener("load",n),t.addEventListener("error",n))}}var Wc=0;function x0(e,t){return e.stylesheets&&e.count===0&&fu(e,e.stylesheets),0Wc?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(l),clearTimeout(a)}}:null}function ru(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)fu(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var ou=null;function fu(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,ou=new Map,t.forEach(_0,e),ou=null,ru.call(e))}function _0(e,t){if(!(t.state.loading&4)){var n=ou.get(e);if(n)var l=n.get(null);else{n=new Map,ou.set(e,n);for(var a=e.querySelectorAll("link[data-precedence],style[data-precedence]"),s=0;s"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(u)}catch(i){console.error(i)}}return u(),ir.exports=H0(),ir.exports}var Y0=q0();var nm="popstate";function k0(u={}){function i(r,f){let{pathname:d,search:m,hash:b}=r.location;return yr("",{pathname:d,search:m,hash:b},f.state&&f.state.usr||null,f.state&&f.state.key||"default")}function c(r,f){return typeof f=="string"?f:Wa(f)}return G0(i,c,null,u)}function Oe(u,i){if(u===!1||u===null||typeof u>"u")throw new Error(i)}function jt(u,i){if(!u){typeof console<"u"&&console.warn(i);try{throw new Error(i)}catch{}}}function X0(){return Math.random().toString(36).substring(2,10)}function lm(u,i){return{usr:u.state,key:u.key,idx:i}}function yr(u,i,c=null,r){return{pathname:typeof u=="string"?u:u.pathname,search:"",hash:"",...typeof i=="string"?Kl(i):i,state:c,key:i&&i.key||r||X0()}}function Wa({pathname:u="/",search:i="",hash:c=""}){return i&&i!=="?"&&(u+=i.charAt(0)==="?"?i:"?"+i),c&&c!=="#"&&(u+=c.charAt(0)==="#"?c:"#"+c),u}function Kl(u){let i={};if(u){let c=u.indexOf("#");c>=0&&(i.hash=u.substring(c),u=u.substring(0,c));let r=u.indexOf("?");r>=0&&(i.search=u.substring(r),u=u.substring(0,r)),u&&(i.pathname=u)}return i}function G0(u,i,c,r={}){let{window:f=document.defaultView,v5Compat:d=!1}=r,m=f.history,b="POP",y=null,v=T();v==null&&(v=0,m.replaceState({...m.state,idx:v},""));function T(){return(m.state||{idx:null}).idx}function S(){b="POP";let q=T(),H=q==null?null:q-v;v=q,y&&y({action:b,location:X.location,delta:H})}function C(q,H){b="PUSH";let Y=yr(X.location,q,H);v=T()+1;let k=lm(Y,v),F=X.createHref(Y);try{m.pushState(k,"",F)}catch(ye){if(ye instanceof DOMException&&ye.name==="DataCloneError")throw ye;f.location.assign(F)}d&&y&&y({action:b,location:X.location,delta:1})}function z(q,H){b="REPLACE";let Y=yr(X.location,q,H);v=T();let k=lm(Y,v),F=X.createHref(Y);m.replaceState(k,"",F),d&&y&&y({action:b,location:X.location,delta:0})}function L(q){return V0(q)}let X={get action(){return b},get location(){return u(f,m)},listen(q){if(y)throw new Error("A history only accepts one active listener");return f.addEventListener(nm,S),y=q,()=>{f.removeEventListener(nm,S),y=null}},createHref(q){return i(f,q)},createURL:L,encodeLocation(q){let H=L(q);return{pathname:H.pathname,search:H.search,hash:H.hash}},push:C,replace:z,go(q){return m.go(q)}};return X}function V0(u,i=!1){let c="http://localhost";typeof window<"u"&&(c=window.location.origin!=="null"?window.location.origin:window.location.href),Oe(c,"No window.location.(origin|href) available to create URL");let r=typeof u=="string"?u:Wa(u);return r=r.replace(/ $/,"%20"),!i&&r.startsWith("//")&&(r=c+r),new URL(r,c)}function _m(u,i,c="/"){return Q0(u,i,c,!1)}function Q0(u,i,c,r){let f=typeof i=="string"?Kl(i):i,d=on(f.pathname||"/",c);if(d==null)return null;let m=Em(u);Z0(m);let b=null;for(let y=0;b==null&&y{let T={relativePath:v===void 0?m.path||"":v,caseSensitive:m.caseSensitive===!0,childrenIndex:b,route:m};if(T.relativePath.startsWith("/")){if(!T.relativePath.startsWith(r)&&y)return;Oe(T.relativePath.startsWith(r),`Absolute route path "${T.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),T.relativePath=T.relativePath.slice(r.length)}let S=rn([r,T.relativePath]),C=c.concat(T);m.children&&m.children.length>0&&(Oe(m.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${S}".`),Em(m.children,i,C,S,y)),!(m.path==null&&!m.index)&&i.push({path:S,score:I0(S,m.index),routesMeta:C})};return u.forEach((m,b)=>{if(m.path===""||!m.path?.includes("?"))d(m,b);else for(let y of Tm(m.path))d(m,b,!0,y)}),i}function Tm(u){let i=u.split("/");if(i.length===0)return[];let[c,...r]=i,f=c.endsWith("?"),d=c.replace(/\?$/,"");if(r.length===0)return f?[d,""]:[d];let m=Tm(r.join("/")),b=[];return b.push(...m.map(y=>y===""?d:[d,y].join("/"))),f&&b.push(...m),b.map(y=>u.startsWith("/")&&y===""?"/":y)}function Z0(u){u.sort((i,c)=>i.score!==c.score?c.score-i.score:ev(i.routesMeta.map(r=>r.childrenIndex),c.routesMeta.map(r=>r.childrenIndex)))}var K0=/^:[\w-]+$/,J0=3,$0=2,W0=1,F0=10,P0=-2,am=u=>u==="*";function I0(u,i){let c=u.split("/"),r=c.length;return c.some(am)&&(r+=P0),i&&(r+=$0),c.filter(f=>!am(f)).reduce((f,d)=>f+(K0.test(d)?J0:d===""?W0:F0),r)}function ev(u,i){return u.length===i.length&&u.slice(0,-1).every((r,f)=>r===i[f])?u[u.length-1]-i[i.length-1]:0}function tv(u,i,c=!1){let{routesMeta:r}=u,f={},d="/",m=[];for(let b=0;b{if(T==="*"){let L=b[C]||"";m=d.slice(0,d.length-L.length).replace(/(.)\/+$/,"$1")}const z=b[C];return S&&!z?v[T]=void 0:v[T]=(z||"").replace(/%2F/g,"/"),v},{}),pathname:d,pathnameBase:m,pattern:u}}function nv(u,i=!1,c=!0){jt(u==="*"||!u.endsWith("*")||u.endsWith("/*"),`Route path "${u}" will be treated as if it were "${u.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${u.replace(/\*$/,"/*")}".`);let r=[],f="^"+u.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(m,b,y)=>(r.push({paramName:b,isOptional:y!=null}),y?"/?([^\\/]+)?":"/([^\\/]+)")).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return u.endsWith("*")?(r.push({paramName:"*"}),f+=u==="*"||u==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):c?f+="\\/*$":u!==""&&u!=="/"&&(f+="(?:(?=\\/|$))"),[new RegExp(f,i?void 0:"i"),r]}function lv(u){try{return u.split("/").map(i=>decodeURIComponent(i).replace(/\//g,"%2F")).join("/")}catch(i){return jt(!1,`The URL path "${u}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${i}).`),u}}function on(u,i){if(i==="/")return u;if(!u.toLowerCase().startsWith(i.toLowerCase()))return null;let c=i.endsWith("/")?i.length-1:i.length,r=u.charAt(c);return r&&r!=="/"?null:u.slice(c)||"/"}var av=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,iv=u=>av.test(u);function uv(u,i="/"){let{pathname:c,search:r="",hash:f=""}=typeof u=="string"?Kl(u):u,d;if(c)if(iv(c))d=c;else{if(c.includes("//")){let m=c;c=c.replace(/\/\/+/g,"/"),jt(!1,`Pathnames cannot have embedded double slashes - normalizing ${m} -> ${c}`)}c.startsWith("/")?d=im(c.substring(1),"/"):d=im(c,i)}else d=i;return{pathname:d,search:rv(r),hash:ov(f)}}function im(u,i){let c=i.replace(/\/+$/,"").split("/");return u.split("/").forEach(f=>{f===".."?c.length>1&&c.pop():f!=="."&&c.push(f)}),c.length>1?c.join("/"):"/"}function rr(u,i,c,r){return`Cannot include a '${u}' character in a manually specified \`to.${i}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${c}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function sv(u){return u.filter((i,c)=>c===0||i.route.path&&i.route.path.length>0)}function Am(u){let i=sv(u);return i.map((c,r)=>r===i.length-1?c.pathname:c.pathnameBase)}function Cm(u,i,c,r=!1){let f;typeof u=="string"?f=Kl(u):(f={...u},Oe(!f.pathname||!f.pathname.includes("?"),rr("?","pathname","search",f)),Oe(!f.pathname||!f.pathname.includes("#"),rr("#","pathname","hash",f)),Oe(!f.search||!f.search.includes("#"),rr("#","search","hash",f)));let d=u===""||f.pathname==="",m=d?"/":f.pathname,b;if(m==null)b=c;else{let S=i.length-1;if(!r&&m.startsWith("..")){let C=m.split("/");for(;C[0]==="..";)C.shift(),S-=1;f.pathname=C.join("/")}b=S>=0?i[S]:"/"}let y=uv(f,b),v=m&&m!=="/"&&m.endsWith("/"),T=(d||m===".")&&c.endsWith("/");return!y.pathname.endsWith("/")&&(v||T)&&(y.pathname+="/"),y}var rn=u=>u.join("/").replace(/\/\/+/g,"/"),cv=u=>u.replace(/\/+$/,"").replace(/^\/*/,"/"),rv=u=>!u||u==="?"?"":u.startsWith("?")?u:"?"+u,ov=u=>!u||u==="#"?"":u.startsWith("#")?u:"#"+u;function fv(u){return u!=null&&typeof u.status=="number"&&typeof u.statusText=="string"&&typeof u.internal=="boolean"&&"data"in u}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Nm=["POST","PUT","PATCH","DELETE"];new Set(Nm);var dv=["GET",...Nm];new Set(dv);var Jl=E.createContext(null);Jl.displayName="DataRouter";var Du=E.createContext(null);Du.displayName="DataRouterState";E.createContext(!1);var Rm=E.createContext({isTransitioning:!1});Rm.displayName="ViewTransition";var hv=E.createContext(new Map);hv.displayName="Fetchers";var mv=E.createContext(null);mv.displayName="Await";var kt=E.createContext(null);kt.displayName="Navigation";var Fa=E.createContext(null);Fa.displayName="Location";var fn=E.createContext({outlet:null,matches:[],isDataRoute:!1});fn.displayName="Route";var Ar=E.createContext(null);Ar.displayName="RouteError";function pv(u,{relative:i}={}){Oe(Pa(),"useHref() may be used only in the context of a component.");let{basename:c,navigator:r}=E.useContext(kt),{hash:f,pathname:d,search:m}=Ia(u,{relative:i}),b=d;return c!=="/"&&(b=d==="/"?c:rn([c,d])),r.createHref({pathname:b,search:m,hash:f})}function Pa(){return E.useContext(Fa)!=null}function sl(){return Oe(Pa(),"useLocation() may be used only in the context of a component."),E.useContext(Fa).location}var Om="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function zm(u){E.useContext(kt).static||E.useLayoutEffect(u)}function yv(){let{isDataRoute:u}=E.useContext(fn);return u?Ov():vv()}function vv(){Oe(Pa(),"useNavigate() may be used only in the context of a component.");let u=E.useContext(Jl),{basename:i,navigator:c}=E.useContext(kt),{matches:r}=E.useContext(fn),{pathname:f}=sl(),d=JSON.stringify(Am(r)),m=E.useRef(!1);return zm(()=>{m.current=!0}),E.useCallback((y,v={})=>{if(jt(m.current,Om),!m.current)return;if(typeof y=="number"){c.go(y);return}let T=Cm(y,JSON.parse(d),f,v.relative==="path");u==null&&i!=="/"&&(T.pathname=T.pathname==="/"?i:rn([i,T.pathname])),(v.replace?c.replace:c.push)(T,v.state,v)},[i,c,d,f,u])}E.createContext(null);function Ia(u,{relative:i}={}){let{matches:c}=E.useContext(fn),{pathname:r}=sl(),f=JSON.stringify(Am(c));return E.useMemo(()=>Cm(u,JSON.parse(f),r,i==="path"),[u,f,r,i])}function gv(u,i){return Mm(u,i)}function Mm(u,i,c,r,f){Oe(Pa(),"useRoutes() may be used only in the context of a component.");let{navigator:d}=E.useContext(kt),{matches:m}=E.useContext(fn),b=m[m.length-1],y=b?b.params:{},v=b?b.pathname:"/",T=b?b.pathnameBase:"/",S=b&&b.route;{let Y=S&&S.path||"";Dm(v,!S||Y.endsWith("*")||Y.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${v}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let C=sl(),z;if(i){let Y=typeof i=="string"?Kl(i):i;Oe(T==="/"||Y.pathname?.startsWith(T),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${T}" but pathname "${Y.pathname}" was given in the \`location\` prop.`),z=Y}else z=C;let L=z.pathname||"/",X=L;if(T!=="/"){let Y=T.replace(/^\//,"").split("/");X="/"+L.replace(/^\//,"").split("/").slice(Y.length).join("/")}let q=_m(u,{pathname:X});jt(S||q!=null,`No routes matched location "${z.pathname}${z.search}${z.hash}" `),jt(q==null||q[q.length-1].route.element!==void 0||q[q.length-1].route.Component!==void 0||q[q.length-1].route.lazy!==void 0,`Matched leaf route at location "${z.pathname}${z.search}${z.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let H=Ev(q&&q.map(Y=>Object.assign({},Y,{params:Object.assign({},y,Y.params),pathname:rn([T,d.encodeLocation?d.encodeLocation(Y.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:Y.pathname]),pathnameBase:Y.pathnameBase==="/"?T:rn([T,d.encodeLocation?d.encodeLocation(Y.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:Y.pathnameBase])})),m,c,r,f);return i&&H?E.createElement(Fa.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...z},navigationType:"POP"}},H):H}function bv(){let u=Rv(),i=fv(u)?`${u.status} ${u.statusText}`:u instanceof Error?u.message:JSON.stringify(u),c=u instanceof Error?u.stack:null,r="rgba(200,200,200, 0.5)",f={padding:"0.5rem",backgroundColor:r},d={padding:"2px 4px",backgroundColor:r},m=null;return console.error("Error handled by React Router default ErrorBoundary:",u),m=E.createElement(E.Fragment,null,E.createElement("p",null,"💿 Hey developer 👋"),E.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",E.createElement("code",{style:d},"ErrorBoundary")," or"," ",E.createElement("code",{style:d},"errorElement")," prop on your route.")),E.createElement(E.Fragment,null,E.createElement("h2",null,"Unexpected Application Error!"),E.createElement("h3",{style:{fontStyle:"italic"}},i),c?E.createElement("pre",{style:f},c):null,m)}var Sv=E.createElement(bv,null),xv=class extends E.Component{constructor(u){super(u),this.state={location:u.location,revalidation:u.revalidation,error:u.error}}static getDerivedStateFromError(u){return{error:u}}static getDerivedStateFromProps(u,i){return i.location!==u.location||i.revalidation!=="idle"&&u.revalidation==="idle"?{error:u.error,location:u.location,revalidation:u.revalidation}:{error:u.error!==void 0?u.error:i.error,location:i.location,revalidation:u.revalidation||i.revalidation}}componentDidCatch(u,i){this.props.onError?this.props.onError(u,i):console.error("React Router caught the following error during render",u)}render(){return this.state.error!==void 0?E.createElement(fn.Provider,{value:this.props.routeContext},E.createElement(Ar.Provider,{value:this.state.error,children:this.props.component})):this.props.children}};function _v({routeContext:u,match:i,children:c}){let r=E.useContext(Jl);return r&&r.static&&r.staticContext&&(i.route.errorElement||i.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=i.route.id),E.createElement(fn.Provider,{value:u},c)}function Ev(u,i=[],c=null,r=null,f=null){if(u==null){if(!c)return null;if(c.errors)u=c.matches;else if(i.length===0&&!c.initialized&&c.matches.length>0)u=c.matches;else return null}let d=u,m=c?.errors;if(m!=null){let T=d.findIndex(S=>S.route.id&&m?.[S.route.id]!==void 0);Oe(T>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(m).join(",")}`),d=d.slice(0,Math.min(d.length,T+1))}let b=!1,y=-1;if(c)for(let T=0;T=0?d=d.slice(0,y+1):d=[d[0]];break}}}let v=c&&r?(T,S)=>{r(T,{location:c.location,params:c.matches?.[0]?.params??{},errorInfo:S})}:void 0;return d.reduceRight((T,S,C)=>{let z,L=!1,X=null,q=null;c&&(z=m&&S.route.id?m[S.route.id]:void 0,X=S.route.errorElement||Sv,b&&(y<0&&C===0?(Dm("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),L=!0,q=null):y===C&&(L=!0,q=S.route.hydrateFallbackElement||null)));let H=i.concat(d.slice(0,C+1)),Y=()=>{let k;return z?k=X:L?k=q:S.route.Component?k=E.createElement(S.route.Component,null):S.route.element?k=S.route.element:k=T,E.createElement(_v,{match:S,routeContext:{outlet:T,matches:H,isDataRoute:c!=null},children:k})};return c&&(S.route.ErrorBoundary||S.route.errorElement||C===0)?E.createElement(xv,{location:c.location,revalidation:c.revalidation,component:X,error:z,children:Y(),routeContext:{outlet:null,matches:H,isDataRoute:!0},onError:v}):Y()},null)}function Cr(u){return`${u} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Tv(u){let i=E.useContext(Jl);return Oe(i,Cr(u)),i}function Av(u){let i=E.useContext(Du);return Oe(i,Cr(u)),i}function Cv(u){let i=E.useContext(fn);return Oe(i,Cr(u)),i}function Nr(u){let i=Cv(u),c=i.matches[i.matches.length-1];return Oe(c.route.id,`${u} can only be used on routes that contain a unique "id"`),c.route.id}function Nv(){return Nr("useRouteId")}function Rv(){let u=E.useContext(Ar),i=Av("useRouteError"),c=Nr("useRouteError");return u!==void 0?u:i.errors?.[c]}function Ov(){let{router:u}=Tv("useNavigate"),i=Nr("useNavigate"),c=E.useRef(!1);return zm(()=>{c.current=!0}),E.useCallback(async(f,d={})=>{jt(c.current,Om),c.current&&(typeof f=="number"?u.navigate(f):await u.navigate(f,{fromRouteId:i,...d}))},[u,i])}var um={};function Dm(u,i,c){!i&&!um[u]&&(um[u]=!0,jt(!1,c))}E.memo(zv);function zv({routes:u,future:i,state:c,unstable_onError:r}){return Mm(u,void 0,c,r,i)}function vr(u){Oe(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Mv({basename:u="/",children:i=null,location:c,navigationType:r="POP",navigator:f,static:d=!1}){Oe(!Pa(),"You cannot render a inside another . You should never have more than one in your app.");let m=u.replace(/^\/*/,"/"),b=E.useMemo(()=>({basename:m,navigator:f,static:d,future:{}}),[m,f,d]);typeof c=="string"&&(c=Kl(c));let{pathname:y="/",search:v="",hash:T="",state:S=null,key:C="default"}=c,z=E.useMemo(()=>{let L=on(y,m);return L==null?null:{location:{pathname:L,search:v,hash:T,state:S,key:C},navigationType:r}},[m,y,v,T,S,C,r]);return jt(z!=null,` is not able to match the URL "${y}${v}${T}" because it does not start with the basename, so the won't render anything.`),z==null?null:E.createElement(kt.Provider,{value:b},E.createElement(Fa.Provider,{children:i,value:z}))}function Dv({children:u,location:i}){return gv(gr(u),i)}function gr(u,i=[]){let c=[];return E.Children.forEach(u,(r,f)=>{if(!E.isValidElement(r))return;let d=[...i,f];if(r.type===E.Fragment){c.push.apply(c,gr(r.props.children,d));return}Oe(r.type===vr,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),Oe(!r.props.index||!r.props.children,"An index route cannot have child routes.");let m={id:r.props.id||d.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,middleware:r.props.middleware,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(m.children=gr(r.props.children,d)),c.push(m)}),c}var Tu="get",Au="application/x-www-form-urlencoded";function ju(u){return u!=null&&typeof u.tagName=="string"}function jv(u){return ju(u)&&u.tagName.toLowerCase()==="button"}function wv(u){return ju(u)&&u.tagName.toLowerCase()==="form"}function Uv(u){return ju(u)&&u.tagName.toLowerCase()==="input"}function Bv(u){return!!(u.metaKey||u.altKey||u.ctrlKey||u.shiftKey)}function Lv(u,i){return u.button===0&&(!i||i==="_self")&&!Bv(u)}var bu=null;function Hv(){if(bu===null)try{new FormData(document.createElement("form"),0),bu=!1}catch{bu=!0}return bu}var qv=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function or(u){return u!=null&&!qv.has(u)?(jt(!1,`"${u}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Au}"`),null):u}function Yv(u,i){let c,r,f,d,m;if(wv(u)){let b=u.getAttribute("action");r=b?on(b,i):null,c=u.getAttribute("method")||Tu,f=or(u.getAttribute("enctype"))||Au,d=new FormData(u)}else if(jv(u)||Uv(u)&&(u.type==="submit"||u.type==="image")){let b=u.form;if(b==null)throw new Error('Cannot submit a - Role: user - -
-
- - -
- - -
- -
- - - - - -
-
- -
-
- -
-
-
-

Sensor Frames

-
Active rover: --
-
Driver: --
-
-
- -
-
No rover selected
- -
-

-        

Use WASD keys to drive the selected rover. Shift increases speed.

-
-
-
- - - - - - - - - - - -

Open spectator view

+
diff --git a/server/public/loader.js b/server/public/loader.js deleted file mode 100644 index 82c6b53f..00000000 --- a/server/public/loader.js +++ /dev/null @@ -1,22 +0,0 @@ -(function () { - const modules = {}; - const cache = {}; - - window.registerModule = function (name, factory) { - modules[name] = factory; - }; - - window.requireModule = function (name) { - if (cache[name]) { - return cache[name]; - } - const factory = modules[name]; - if (!factory) { - throw new Error(`Module ${name} not found`); - } - const exports = {}; - cache[name] = exports; - factory(requireModule, exports); - return exports; - }; -})(); diff --git a/server/public/spectate/index.html b/server/public/spectate/index.html deleted file mode 100644 index 5938b2eb..00000000 --- a/server/public/spectate/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - Rover Spectator - - - -

Rover Spectator

-
Connecting…
-
- - - - - - - - - diff --git a/server/public/spectate/spectator.js b/server/public/spectate/spectator.js deleted file mode 100644 index add8941e..00000000 --- a/server/public/spectate/spectator.js +++ /dev/null @@ -1,131 +0,0 @@ -registerModule('spectator/main', (require, exports) => { - const { socket } = require('globals/socket'); - const videoPlayer = require('services/videoPlayer'); - - const statusEl = document.getElementById('status'); - const grid = document.getElementById('grid'); - const cards = new Map(); - - socket.on('connect', () => { - statusEl.textContent = 'Connected'; - }); - - socket.on('disconnect', () => { - statusEl.textContent = 'Disconnected'; - stopAllVideos(); - }); - - socket.on('lockdown', ({ message }) => { - statusEl.textContent = message || 'Spectator mode disabled'; - stopAllVideos(); - }); - - socket.on('rovers', (list) => { - list.forEach((rover) => { - const card = ensureCard(rover.id); - card.querySelector('.meta').textContent = `Locked: ${rover.locked ? 'yes' : 'no'} | Last seen: ${new Date(rover.lastSeen).toLocaleTimeString()}`; - if (!card.dataset.videoActive) { - connectVideo(rover.id); - } - }); - Array.from(cards.keys()).forEach((id) => { - if (!list.find((r) => r.id === id)) { - removeCard(id); - } - }); - }); - - socket.on('sensorFrame', ({ roverId, sensors }) => { - const card = ensureCard(roverId); - card.querySelector('.sensor').textContent = sensors ? JSON.stringify(sensors, null, 2) : 'No data'; - }); - - function ensureCard(roverId) { - if (cards.has(roverId)) { - return cards.get(roverId); - } - const card = document.createElement('div'); - card.className = 'rover-card'; - card.dataset.videoActive = ''; - card.innerHTML = ` -

${roverId}

-
-
-
Video not connected
- -
Waiting for data...
- `; - const reconnectBtn = card.querySelector('.video-reconnect'); - reconnectBtn.addEventListener('click', () => connectVideo(roverId, true)); - grid.appendChild(card); - cards.set(roverId, card); - connectVideo(roverId, true); - return card; - } - - function connectVideo(roverId, force = false) { - const card = ensureCard(roverId); - const status = card.querySelector('.video-status'); - const wrap = card.querySelector('.video-wrap'); - const playerId = `spectator-${roverId}`; - if (!force && card.dataset.videoActive === 'yes') { - return; - } - status.textContent = 'Connecting video…'; - socket.emit('video:request', { roverId }, (resp = {}) => { - if (resp.error) { - status.textContent = `Video error: ${resp.error}`; - videoPlayer.stopStream(playerId); - card.dataset.videoActive = ''; - return; - } - videoPlayer - .startStream(playerId, { - url: resp.url, - token: resp.token, - mount: wrap, - onStatus: (state, detail) => { - if (state === 'playing') { - status.textContent = 'Video live'; - card.dataset.videoActive = 'yes'; - } else if (state === 'connecting') { - status.textContent = 'Connecting video…'; - } else if (state === 'error') { - status.textContent = `Video error: ${detail}`; - card.dataset.videoActive = ''; - } else if (state === 'stopped') { - status.textContent = 'Video stopped'; - card.dataset.videoActive = ''; - } - }, - }) - .catch((err) => { - status.textContent = `Video error: ${err.message}`; - card.dataset.videoActive = ''; - }); - }); - } - - function removeCard(roverId) { - const card = cards.get(roverId); - if (card) { - videoPlayer.stopStream(`spectator-${roverId}`); - card.remove(); - cards.delete(roverId); - } - } - - function stopAllVideos() { - Array.from(cards.keys()).forEach((id) => { - videoPlayer.stopStream(`spectator-${id}`); - const card = cards.get(id); - if (card) { - const status = card.querySelector('.video-status'); - status.textContent = 'Video stopped'; - card.dataset.videoActive = ''; - } - }); - } -}); - -requireModule('spectator/main'); diff --git a/server/public/src/globals/socket.js b/server/public/src/globals/socket.js deleted file mode 100644 index a3f50f70..00000000 --- a/server/public/src/globals/socket.js +++ /dev/null @@ -1,8 +0,0 @@ -registerModule('globals/socket', (require, exports) => { - const query = {}; - if (window.__desiredRole) { - query.role = window.__desiredRole; - } - const socket = io(query.role ? { query } : undefined); - exports.socket = socket; -}); diff --git a/server/public/src/helpers/formatters.js b/server/public/src/helpers/formatters.js deleted file mode 100644 index d4bf4e0f..00000000 --- a/server/public/src/helpers/formatters.js +++ /dev/null @@ -1,11 +0,0 @@ -registerModule('helpers/formatters', (require, exports) => { - function formatHex(base64) { - if (!base64) return ''; - const buffer = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); - return Array.from(buffer) - .map((b) => b.toString(16).padStart(2, '0')) - .join(' '); - } - - exports.formatHex = formatHex; -}); diff --git a/server/public/src/main.js b/server/public/src/main.js deleted file mode 100644 index c188854d..00000000 --- a/server/public/src/main.js +++ /dev/null @@ -1,7 +0,0 @@ -requireModule('globals/socket'); -requireModule('helpers/formatters'); -requireModule('services/state'); -requireModule('services/alertFeed'); -requireModule('services/authControls'); -requireModule('services/roverUI'); -requireModule('services/driveControls'); diff --git a/server/public/src/services/alertFeed.js b/server/public/src/services/alertFeed.js deleted file mode 100644 index 04305874..00000000 --- a/server/public/src/services/alertFeed.js +++ /dev/null @@ -1,25 +0,0 @@ -registerModule('services/alertFeed', (require, exports) => { - const { socket } = require('globals/socket'); - - const container = document.getElementById('alerts'); - if (!container) return; - - function pushAlert({ color = '#2196f3', title, message, ts }) { - const div = document.createElement('div'); - div.className = 'alert'; - div.style.borderColor = color; - div.style.background = color + '22'; - div.innerHTML = `${title || 'Notice'} - ${message || ''}`; - container.prepend(div); - while (container.children.length > 5) { - container.removeChild(container.lastChild); - } - setTimeout(() => { - if (div.parentElement === container) { - container.removeChild(div); - } - }, 15000); - } - - socket.on('alert', pushAlert); -}); diff --git a/server/public/src/services/authControls.js b/server/public/src/services/authControls.js deleted file mode 100644 index b3f730f0..00000000 --- a/server/public/src/services/authControls.js +++ /dev/null @@ -1,35 +0,0 @@ -registerModule('services/authControls', (require, exports) => { - const { socket } = require('globals/socket'); - const state = require('services/state'); - - const usernameInput = document.getElementById('username'); - const passwordInput = document.getElementById('password'); - const loginBtn = document.getElementById('loginBtn'); - const roleStatus = document.getElementById('roleStatus'); - - function updateRole(role) { - if (roleStatus) { - roleStatus.textContent = `Role: ${role}`; - } - state.setRole(role); - } - - loginBtn?.addEventListener('click', () => { - const username = usernameInput.value.trim(); - const password = passwordInput.value; - socket.emit('auth:login', { username, password }, (resp = {}) => { - if (!resp.success) { - alert(resp.error || 'Login failed'); - } else { - updateRole(resp.role); - } - passwordInput.value = ''; - }); - }); - - socket.on('auth:role', ({ role }) => { - updateRole(role); - }); - // initialize from default role - updateRole(state.getRole()); -}); diff --git a/server/public/src/services/driveControls.js b/server/public/src/services/driveControls.js deleted file mode 100644 index e502ecbe..00000000 --- a/server/public/src/services/driveControls.js +++ /dev/null @@ -1,127 +0,0 @@ -registerModule('services/driveControls', (require, exports) => { - const { socket } = require('globals/socket'); - const state = require('services/state'); - - const motorsStopBtn = document.getElementById('motorsStop'); - const sensorToggleBtn = document.getElementById('sensorToggle'); - const modeButtons = document.querySelectorAll('[data-oi]'); - - let sensorEnabled = false; - let lastDrive = { left: 0, right: 0 }; - const keys = new Set(); - - window.addEventListener('keydown', (event) => { - const key = event.key.toLowerCase(); - if (keys.has(key)) return; - keys.add(key); - maybeSendDriveUpdate(); - }); - - window.addEventListener('keyup', (event) => { - const key = event.key.toLowerCase(); - if (!keys.has(key)) return; - keys.delete(key); - maybeSendDriveUpdate(); - }); - - function maybeSendDriveUpdate() { - const roverId = state.getSelected(); - if (!roverId) return; - const speeds = computeDrive(); - if (speeds.left === lastDrive.left && speeds.right === lastDrive.right) return; - lastDrive = speeds; - socket.emit('command', { - roverId, - type: 'drive', - data: { driveDirect: speeds }, - }); - } - - function computeDrive() { - const forward = keys.has('w'); - const backward = keys.has('s'); - const left = keys.has('a'); - const right = keys.has('d'); - const fast = keys.has('shift'); - const base = fast ? 300 : 150; - let leftSpeed = 0; - let rightSpeed = 0; - - if (forward && !backward) { - leftSpeed += base; - rightSpeed += base; - } else if (backward && !forward) { - leftSpeed -= base; - rightSpeed -= base; - } - - if (left && !right) { - leftSpeed -= base; - rightSpeed += base; - } else if (right && !left) { - leftSpeed += base; - rightSpeed -= base; - } - - if (!forward && !backward && (left || right)) { - leftSpeed = left ? -base : base; - rightSpeed = left ? base : -base; - } - - return { - left: clamp(leftSpeed, -500, 500), - right: clamp(rightSpeed, -500, 500), - }; - } - - function clamp(value, min, max) { - return Math.max(min, Math.min(max, value)); - } - - motorsStopBtn?.addEventListener('click', () => { - const roverId = state.getSelected(); - if (!roverId) return; - socket.emit('command', { - roverId, - type: 'motors', - data: { motorPwm: { main: 0, side: 0, vacuum: 0 } }, - }); - }); - - sensorToggleBtn?.addEventListener('click', () => { - sensorEnabled = !sensorEnabled; - sensorToggleBtn.textContent = sensorEnabled ? 'Disable Sensor Stream' : 'Enable Sensor Stream'; - const roverId = state.getSelected(); - socket.emit('command', { - roverId, - type: 'sensorStream', - data: { sensorStream: { enable: sensorEnabled } }, - }); - }); - - const OI_COMMANDS = { - start: [128], - safe: [131], - full: [132], - passive: [128], - dock: [143], - }; - - modeButtons.forEach((btn) => { - btn.addEventListener('click', () => { - const cmd = btn.dataset.oi; - const bytes = OI_COMMANDS[cmd]; - if (!bytes) return; - const roverId = state.getSelected(); - socket.emit('command', { - roverId, - type: 'raw', - data: { raw: bytesToBase64(bytes) }, - }); - }); - }); - - function bytesToBase64(bytes) { - return btoa(String.fromCharCode(...bytes)); - } -}); diff --git a/server/public/src/services/roverUI.js b/server/public/src/services/roverUI.js deleted file mode 100644 index a30dfaa9..00000000 --- a/server/public/src/services/roverUI.js +++ /dev/null @@ -1,185 +0,0 @@ -registerModule('services/roverUI', (require, exports) => { - const { socket } = require('globals/socket'); - const { formatHex } = require('helpers/formatters'); - const videoPlayer = require('services/videoPlayer'); - const state = require('services/state'); - - const statusEl = document.getElementById('status'); - const roverSelect = document.getElementById('roverSelect'); - const sensorOutput = document.getElementById('sensorOutput'); - const lockBtn = document.getElementById('lockToggle'); - const forceCheckbox = document.getElementById('forceControl'); - const adminControls = document.getElementById('adminControls'); - const modeControls = document.getElementById('modeControls'); - const modeSelect = document.getElementById('modeSelect'); - const activeDriverEl = document.getElementById('activeDriver'); - const currentRoverEl = document.getElementById('currentRover'); - const videoContainer = document.getElementById('videoContainer'); - const driverVideo = document.getElementById('driverVideo'); - const videoStatus = document.getElementById('videoStatus'); - const videoReconnect = document.getElementById('videoReconnect'); - - let roster = []; - let lastSelection = null; - let currentVideoRover = null; - - state.onRoleChange((role) => { - const isAdmin = role === 'admin' || role === 'lockdown'; - if (lockBtn) lockBtn.disabled = !isAdmin; - if (roverSelect) roverSelect.disabled = !isAdmin; - if (adminControls) { - adminControls.style.display = isAdmin ? 'flex' : 'none'; - } - if (modeControls) { - modeControls.style.display = isAdmin ? 'block' : 'none'; - if (modeSelect) modeSelect.disabled = !isAdmin; - } - }); - - socket.on('rovers', (list) => { - roster = list; - roverSelect.innerHTML = ''; - list.forEach((rover) => { - const option = document.createElement('option'); - option.value = rover.id; - option.textContent = `${rover.name}${rover.locked ? ' (locked)' : ''}`; - roverSelect.appendChild(option); - }); - const selected = state.getSelected(); - if ((!selected || !list.find((r) => r.id === selected)) && list.length) { - state.setSelected(list[0].id); - } else if (!list.length) { - state.setSelected(null); - } - renderSelection(true); - }); - - roverSelect.addEventListener('change', () => { - const roverId = roverSelect.value; - const role = state.getRole(); - state.setSelected(roverId); - renderSelection(true); - if (role === 'admin' || role === 'lockdown') { - socket.emit('requestControl', { roverId, force: !!forceCheckbox?.checked }); - } - }); - - socket.on('controlGranted', ({ roverId }) => { - state.setSelected(roverId); - renderSelection(true); - statusEl.textContent = `Driving ${roverId}`; - maybeConnectVideo(true); - }); - - socket.on('sensorFrame', ({ roverId, frame, sensors }) => { - if (roverId !== state.getSelected()) return; - const lines = []; - if (sensors) { - Object.entries(sensors).forEach(([key, value]) => { - lines.push(`${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`); - }); - } - lines.push('', 'raw:', formatHex(frame?.data)); - sensorOutput.textContent = lines.join('\n'); - }); - - socket.on('activeDriver', ({ roverId, socketId }) => { - if (roverId !== state.getSelected()) return; - if (activeDriverEl) { - activeDriverEl.textContent = `Active driver: ${socketId || 'none'}`; - } - }); - - lockBtn?.addEventListener('click', () => { - const roverId = state.getSelected(); - const rover = roster.find((r) => r.id === roverId); - if (!rover) return; - socket.emit('lockRover', { roverId, locked: !rover.locked }); - }); - - modeSelect?.addEventListener('change', () => { - socket.emit('setMode', { mode: modeSelect.value }); - }); - - socket.on('mode', ({ mode }) => { - if (modeSelect) { - modeSelect.value = mode; - } - }); - - videoReconnect?.addEventListener('click', () => maybeConnectVideo(true)); - - socket.on('connect', () => { - statusEl.textContent = 'Connected'; - }); - socket.on('disconnect', () => { - statusEl.textContent = 'Disconnected'; - }); - - function renderSelection(clearOutput) { - const roverId = state.getSelected(); - if (roverSelect && roverId) { - roverSelect.value = roverId; - } else if (roverSelect && !roverId) { - roverSelect.value = ''; - } - if (currentRoverEl) { - currentRoverEl.textContent = `Active rover: ${roverId || '--'}`; - } - if (sensorOutput && (clearOutput || roverId !== lastSelection)) { - sensorOutput.textContent = roverId ? 'Waiting for sensor data...' : ''; - } - lastSelection = roverId; - maybeConnectVideo(clearOutput); - } - - function maybeConnectVideo(force = false) { - const roverId = state.getSelected(); - if (!roverId) { - currentVideoRover = null; - videoPlayer.stopStream('driver'); - setVideoStatus('No rover selected'); - return; - } - if (!force && roverId === currentVideoRover) { - return; - } - currentVideoRover = roverId; - setVideoStatus('Connecting video…'); - socket.emit('video:request', { roverId }, (resp = {}) => { - if (resp.error) { - setVideoStatus(`Video error: ${resp.error}`); - videoPlayer.stopStream('driver'); - return; - } - const { url, token } = resp; - videoPlayer - .startStream('driver', { - url, - token, - mount: videoContainer, - videoEl: driverVideo, - onStatus: (state, detail) => { - if (state === 'playing') { - setVideoStatus('Video live'); - } else if (state === 'connecting') { - setVideoStatus('Connecting video…'); - } else if (state === 'error') { - setVideoStatus(`Video error: ${detail}`); - } else if (state === 'stopped') { - setVideoStatus('Video stopped'); - } - }, - }) - .catch((err) => { - setVideoStatus(`Video error: ${err.message}`); - }); - }); - } - - function setVideoStatus(message) { - if (videoStatus) { - videoStatus.textContent = message; - } - } -}); diff --git a/server/public/src/services/state.js b/server/public/src/services/state.js deleted file mode 100644 index b1c55bef..00000000 --- a/server/public/src/services/state.js +++ /dev/null @@ -1,34 +0,0 @@ -registerModule('services/state', (require, exports) => { - let selectedRover = null; - let role = 'user'; - const roleListeners = new Set(); - - function setSelected(roverId) { - selectedRover = roverId; - } - - function getSelected() { - return selectedRover; - } - - function setRole(nextRole) { - role = nextRole; - roleListeners.forEach((cb) => cb(role)); - } - - function getRole() { - return role; - } - - function onRoleChange(cb) { - roleListeners.add(cb); - cb(role); - return () => roleListeners.delete(cb); - } - - exports.setSelected = setSelected; - exports.getSelected = getSelected; - exports.setRole = setRole; - exports.getRole = getRole; - exports.onRoleChange = onRoleChange; -}); diff --git a/server/public/src/services/videoPlayer.js b/server/public/src/services/videoPlayer.js deleted file mode 100644 index 80c45232..00000000 --- a/server/public/src/services/videoPlayer.js +++ /dev/null @@ -1,152 +0,0 @@ -registerModule('services/videoPlayer', (require, exports) => { - const players = new Map(); - - class Player { - constructor(id, options = {}) { - this.id = id; - this.options = options; - this.onStatus = options.onStatus || (() => {}); - this.mount = options.mount || null; - this.externalVideo = options.videoEl || null; - this.video = this.externalVideo || this.createVideoElement(); - this.video.muted = true; - this.status('idle'); - } - - createVideoElement() { - const el = document.createElement('video'); - el.autoplay = true; - el.playsInline = true; - el.controls = false; - el.muted = true; - el.style.width = '100%'; - el.style.maxHeight = '360px'; - if (this.mount) { - this.mount.innerHTML = ''; - this.mount.appendChild(el); - } - return el; - } - - attachAutoPlayLoop() { - this.autoPlayTimer = window.setInterval(() => { - if (!this.video) return; - if (!this.video.paused && !this.video.muted) { - clearInterval(this.autoPlayTimer); - this.autoPlayTimer = null; - return; - } - const attempt = () => this.video.play().catch(() => {}); - if (this.video.paused) { - attempt(); - } - if (this.video.muted) { - this.video.muted = false; - attempt(); - } - }, 2500); - } - - status(state, detail) { - this.onStatus(state, detail); - } - - async start(url, token) { - this.stopPeer(); - this.status('connecting'); - this.pc = new RTCPeerConnection(); - this.stream = new MediaStream(); - this.video.srcObject = this.stream; - this.pc.addTransceiver('video', { direction: 'recvonly' }); - this.pc.addTransceiver('audio', { direction: 'recvonly' }); - this.pc.ontrack = (event) => { - const [remoteStream] = event.streams; - if (remoteStream) { - this.video.srcObject = remoteStream; - } else { - this.stream.addTrack(event.track); - } - this.video.play().catch(() => {}); - }; - this.attachAutoPlayLoop(); - - if (!token) { - throw new Error('Missing video session token'); - } - const offer = await this.pc.createOffer(); - await this.pc.setLocalDescription(offer); - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/sdp', - Authorization: `Basic ${btoa(`${token}:${token}`)}`, - }, - body: offer.sdp, - }); - if (!response.ok) { - throw new Error(`WHEP HTTP ${response.status}`); - } - const answer = await response.text(); - await this.pc.setRemoteDescription({ type: 'answer', sdp: answer }); - this.status('playing'); - } - - stopPeer() { - if (this.autoPlayTimer) { - clearInterval(this.autoPlayTimer); - this.autoPlayTimer = null; - } - if (this.pc) { - this.pc.getSenders().forEach((sender) => sender.track && sender.track.stop()); - this.pc.getReceivers().forEach((receiver) => receiver.track && receiver.track.stop()); - this.pc.close(); - this.pc = null; - } - if (this.video) { - const tracks = this.video.srcObject?.getTracks() || []; - tracks.forEach((t) => t.stop()); - this.video.pause(); - this.video.srcObject = null; - } - } - - stop() { - this.stopPeer(); - if (!this.externalVideo && this.video && this.video.parentElement) { - this.video.parentElement.removeChild(this.video); - } - this.status('stopped'); - } - } - - async function startStream(id, { url, token, mount, videoEl, onStatus } = {}) { - stopStream(id); - const player = new Player(id, { mount, videoEl, onStatus }); - players.set(id, player); - try { - await player.start(url, token); - return player.video; - } catch (err) { - player.status('error', err.message); - player.stop(); - players.delete(id); - throw err; - } - } - - function stopStream(id) { - const existing = players.get(id); - if (existing) { - existing.stop(); - players.delete(id); - } - } - - function stopAll() { - Array.from(players.keys()).forEach(stopStream); - } - - exports.startStream = startStream; - exports.stopStream = stopStream; - exports.stopAll = stopAll; -}); diff --git a/server/public/vite.svg b/server/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/server/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/src/globals/http.js b/server/src/globals/http.js index e28eeb09..23cbd997 100644 --- a/server/src/globals/http.js +++ b/server/src/globals/http.js @@ -8,6 +8,9 @@ const app = express(); app.use(morgan('dev')); app.use(express.json()); app.use(express.static(config.staticDir)); +app.get('/spectate', (req, res) => { + res.sendFile(path.join(config.staticDir, 'index.html')); +}); const httpServer = http.createServer(app); diff --git a/server/src/globals/logger.js b/server/src/globals/logger.js index 3f9ffe12..78401109 100644 --- a/server/src/globals/logger.js +++ b/server/src/globals/logger.js @@ -1,8 +1,37 @@ +const sinks = new Set(); + +function notifySinks(level, label, args) { + if (!sinks.size) return; + const entry = { + level, + label: label || null, + args, + message: args.map((value) => { + if (typeof value === 'string') return value; + try { + return JSON.stringify(value); + } catch (err) { + return String(value); + } + }).join(' '), + timestamp: new Date().toISOString(), + }; + sinks.forEach((sink) => { + try { + sink(entry); + } catch (err) { + // avoid recursive logging + console.error(entry.timestamp, '[ERROR]', '[logger]', 'Log sink failed', err); + } + }); +} + function stamp(level, label, args) { const fields = [new Date().toISOString(), `[${level}]`]; if (label) { fields.push(`[${label}]`); } + notifySinks(level, label, args); return [...fields, ...args]; } @@ -14,7 +43,14 @@ function baseLogger(label) { }; } +function registerSink(fn) { + if (typeof fn !== 'function') return () => {}; + sinks.add(fn); + return () => sinks.delete(fn); +} + module.exports = { ...baseLogger(), child: (label) => baseLogger(label), + registerSink, }; diff --git a/server/src/services/alertService.js b/server/src/services/alertService.js index d3450c77..19e8a297 100644 --- a/server/src/services/alertService.js +++ b/server/src/services/alertService.js @@ -15,6 +15,7 @@ function sendAlert({ color, title, message, ts = Date.now() }) { ts, }; io.emit('alert', payload); + io.emit('alert:new', { id: `${ts}-${Math.random().toString(36).slice(2)}`, ...payload }); } module.exports = { diff --git a/server/src/services/assignmentService.js b/server/src/services/assignmentService.js index d7436851..094e685d 100644 --- a/server/src/services/assignmentService.js +++ b/server/src/services/assignmentService.js @@ -1,3 +1,4 @@ +const EventEmitter = require('events'); const io = require('../globals/io'); const logger = require('../globals/logger').child('assignment'); const { MODES, getMode, modeEvents } = require('./modeManager'); @@ -7,6 +8,7 @@ const roverManager = require('./roverManager'); const socketRefs = new Map(); // socketId -> socket const assignments = new Map(); // socketId -> roverId const waiting = new Set(); // socketIds waiting for placement +const assignmentEvents = new EventEmitter(); io.on('connection', (socket) => { socketRefs.set(socket.id, socket); @@ -64,6 +66,7 @@ function assignSocket(socket) { if (!target) { waiting.add(socket.id); logger.info('No rover available, user waiting', socket.id); + assignmentEvents.emit('update', socket.id); return; } try { @@ -71,9 +74,11 @@ function assignSocket(socket) { assignments.set(socket.id, target.id); waiting.delete(socket.id); logger.info('Assigned user to rover', socket.id, target.id); + assignmentEvents.emit('update', socket.id); } catch (err) { logger.warn('Failed to assign user', err.message); waiting.add(socket.id); + assignmentEvents.emit('update', socket.id); } } @@ -84,6 +89,8 @@ function unassignSocket(socket) { if (roverId) { roverManager.releaseControl(roverId, socket); assignments.delete(socket.id); + logger.info('Unassigned socket from rover', socket.id, roverId); + assignmentEvents.emit('update', socket.id); } } @@ -98,6 +105,7 @@ function reassignFromRover(roverId) { roverManager.releaseControl(rid, socket); assignments.delete(socketId); assignSocket(socket); + assignmentEvents.emit('update', socketId); } } @@ -109,6 +117,7 @@ function reassignWaiting() { } else { waiting.delete(socketId); } + assignmentEvents.emit('update', socketId); } } @@ -116,6 +125,8 @@ function releaseAssignment(socket, roverId) { roverManager.releaseControl(roverId, socket); assignments.delete(socket.id); waiting.add(socket.id); + logger.info('Released assignment back to queue', socket.id, roverId); + assignmentEvents.emit('update', socket.id); } function pickRover() { @@ -133,3 +144,21 @@ function pickRover() { candidates.sort((a, b) => a.drivers.size - b.drivers.size); return candidates[0]; } + +function describeAssignment(socketId) { + const assignedRoverId = assignments.get(socketId) || null; + const adminRoverId = assignedRoverId ? null : roverManager.getPrimaryRoverForSocket(socketId); + const waitingIndex = waiting.has(socketId) ? Array.from(waiting).indexOf(socketId) : -1; + const roverId = assignedRoverId || adminRoverId || null; + const waitingStatus = waiting.has(socketId); + return { + roverId, + status: assignedRoverId ? 'assigned' : adminRoverId ? 'admin' : waitingStatus ? 'waiting' : null, + queuePosition: waitingIndex >= 0 ? waitingIndex + 1 : null, + }; +} + +module.exports = { + assignmentEvents, + describeAssignment, +}; diff --git a/server/src/services/authService.js b/server/src/services/authService.js index 98e0677f..552faaad 100644 --- a/server/src/services/authService.js +++ b/server/src/services/authService.js @@ -36,6 +36,7 @@ io.on('connection', (socket) => { const requestedRole = socket.handshake?.query?.role; const initialRole = requestedRole === 'spectator' ? 'spectator' : 'user'; setRole(socket, initialRole); + logger.info('Socket connected with role', socket.id, initialRole); socket.emit('auth:role', { role: initialRole }); socket.on('auth:login', async ({ username, password }, cb = () => {}) => { try { @@ -45,18 +46,27 @@ io.on('connection', (socket) => { setRole(socket, role); socket.emit('auth:role', { role }); clearLockdownTimer(socket); + logger.info('Login success', socket.id, role); cb({ success: true, role: socket.data.role }); } catch (err) { + logger.warn('Login failed', socket.id, err.message); cb({ success: false, error: err.message }); } }); - socket.on('role:set', ({ role }) => { + function handleRoleChange({ role } = {}, cb = () => {}) { if (role === 'spectator' || role === 'user') { setRole(socket, role); socket.emit('auth:role', { role }); + logger.info('Role changed via client request', socket.id, role); + cb({ success: true, role }); + } else { + cb({ error: 'Invalid role' }); } - }); + } + + socket.on('role:set', handleRoleChange); + socket.on('session:setRole', handleRoleChange); }); module.exports = { diff --git a/server/src/services/commandService.js b/server/src/services/commandService.js index 7988d7c8..9d093885 100644 --- a/server/src/services/commandService.js +++ b/server/src/services/commandService.js @@ -1,6 +1,7 @@ const { v4: uuidv4 } = require('uuid'); const io = require('../globals/io'); const roverManager = require('./roverManager'); +const logger = require('../globals/logger').child('commandService'); const pendingCommands = new Map(); // id -> { roverId } @@ -13,6 +14,7 @@ function issueCommand(roverId, payload) { const message = { ...payload, id }; record.ws.send(JSON.stringify(message)); pendingCommands.set(id, { roverId, ts: Date.now(), type: payload.type }); + logger.info('Issued command', roverId, payload.type, id); return id; } @@ -20,6 +22,7 @@ function handleAck(msg) { const pending = pendingCommands.get(msg.id); if (!pending) return; pendingCommands.delete(msg.id); + logger.info('Command acknowledged', pending.roverId, pending.type, msg.status); io.emit('commandAck', { roverId: pending.roverId, id: msg.id, @@ -34,7 +37,8 @@ module.exports = { }; io.on('connection', (socket) => { - socket.on('command', ({ roverId, type, data } = {}, cb = () => {}) => { + function handleCommand({ roverId, type, data } = {}, cb) { + const reply = typeof cb === 'function' ? cb : () => {}; try { if (!roverId) { throw new Error('roverId required'); @@ -44,9 +48,14 @@ io.on('connection', (socket) => { } const payload = data ? { ...data } : {}; const id = issueCommand(roverId, { type, ...payload }); - cb({ id }); + logger.info('Queued command', socket.id, roverId, type); + reply({ id }); } catch (err) { - cb({ error: err.message }); + logger.warn('Command rejected', socket.id, err.message); + reply({ error: err.message }); } - }); + } + + socket.on('command', handleCommand); + socket.on('command:issue', handleCommand); }); diff --git a/server/src/services/logStreamService.js b/server/src/services/logStreamService.js new file mode 100644 index 00000000..4dc07a23 --- /dev/null +++ b/server/src/services/logStreamService.js @@ -0,0 +1,40 @@ +const { v4: uuidv4 } = require('uuid'); +const io = require('../globals/io'); +const loggerRoot = require('../globals/logger'); +const logger = loggerRoot.child('logStream'); + +const MAX_HISTORY = 200; +const history = []; + +function pushEntry(entry) { + history.push(entry); + if (history.length > MAX_HISTORY) { + history.shift(); + } +} + +function broadcast(entry) { + io.emit('log:entry', entry); +} + +function hydrateSocket(socket) { + if (!socket) return; + socket.emit('log:init', history); +} + +loggerRoot.registerSink(({ level, label, message, timestamp }) => { + const entry = { + id: uuidv4(), + level, + label, + message, + timestamp, + }; + pushEntry(entry); + broadcast(entry); +}); + +io.on('connection', (socket) => { + logger.info('Hydrating log history for', socket.id); + hydrateSocket(socket); +}); diff --git a/server/src/services/nicknameService.js b/server/src/services/nicknameService.js new file mode 100644 index 00000000..b11abfed --- /dev/null +++ b/server/src/services/nicknameService.js @@ -0,0 +1 @@ +const io = require('../globals/io'); diff --git a/server/src/services/roverManager.js b/server/src/services/roverManager.js index fff5a215..50ce4a57 100644 --- a/server/src/services/roverManager.js +++ b/server/src/services/roverManager.js @@ -115,6 +115,7 @@ function removeSocket(socket) { record.drivers.delete(socket.id); } turnService.driverRemoved(roverId, socket.id); + managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'remove' }); } socketToRovers.delete(socket.id); disableSpectator(socket); @@ -147,6 +148,7 @@ function requestControl(roverId, socket, options = {}) { socket.join(record.room); turnService.driverAdded(roverId, socket.id, force && isAdmin(socket)); socket.emit('controlGranted', { roverId }); + managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'add' }); sendAlert({ color: COLORS.success, title: 'Control Granted', @@ -168,6 +170,7 @@ function releaseControl(roverId, socket) { } socket.leave(record.room); turnService.driverRemoved(roverId, socket.id); + managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'remove' }); } function isDriver(roverId, socket) { @@ -180,6 +183,24 @@ function canDrive(roverId, socket) { return turnService.canDrive(roverId, socket) || isAdmin(socket); } +function getRoversForSocket(socketId) { + const joined = socketToRovers.get(socketId); + if (!joined || joined.size === 0) { + return []; + } + return Array.from(joined); +} + +function getPrimaryRoverForSocket(socketId) { + const joined = socketToRovers.get(socketId); + if (!joined || joined.size === 0) { + return null; + } + const iterator = joined.values(); + const first = iterator.next(); + return first.done ? null : first.value; +} + module.exports = { upsertRover, removeRover, @@ -196,6 +217,8 @@ module.exports = { disableSpectator, rovers, managerEvents, + getRoversForSocket, + getPrimaryRoverForSocket, }; roleEvents.on('change', ({ socket, role }) => { @@ -212,43 +235,72 @@ io.on('connection', (socket) => { enableSpectator(socket); } - socket.on('requestControl', ({ roverId, force } = {}) => { + function handleRequestControl({ roverId, force } = {}, cb = () => {}) { try { const targetId = roverId || Array.from(rovers.keys())[0]; if (!targetId) { throw new Error('No rovers available'); } + logger.info('Request control', socket.id, targetId, { force }); requestControl(targetId, socket, { force: Boolean(force) }); socket.emit('controlGranted', { roverId: targetId }); + cb({ success: true, roverId: targetId }); } catch (err) { + logger.warn('Request control failed', socket.id, err.message); sendAlert({ color: COLORS.warn, title: 'Control denied', message: err.message }); + cb({ error: err.message }); } - }); + } - socket.on('releaseControl', ({ roverId }) => { - if (!roverId) return; - releaseControl(roverId, socket); - }); - - socket.on('lockRover', ({ roverId, locked }) => { - if (!isAdmin(socket)) return; - try { - lockRover(roverId, locked); - } catch (err) { - sendAlert({ color: COLORS.error, title: 'Lock failed', message: err.message }); - } - }); - - socket.on('subscribeAll', () => { - if (socket.data?.role !== 'spectator') { + function handleReleaseControl({ roverId } = {}, cb = () => {}) { + if (!roverId) { + cb({ error: 'roverId required' }); return; } + logger.info('Release control', socket.id, roverId); + releaseControl(roverId, socket); + cb({ success: true, roverId }); + } + + function handleLockToggle({ roverId, locked } = {}, cb = () => {}) { + if (!isAdmin(socket)) { + cb({ error: 'Not authorized' }); + return; + } + try { + lockRover(roverId, locked); + logger.info('Lock state changed', roverId, locked); + cb({ success: true }); + } catch (err) { + logger.warn('Lock change failed', roverId, err.message); + sendAlert({ color: COLORS.error, title: 'Lock failed', message: err.message }); + cb({ error: err.message }); + } + } + + function handleSubscribeAll(_, cb = () => {}) { + if (socket.data?.role !== 'spectator') { + cb({ error: 'Spectator role required' }); + return; + } + logger.info('Spectator subscribing to all rovers', socket.id); for (const record of rovers.values()) { socket.join(record.room); } - }); + cb({ success: true }); + } + + socket.on('requestControl', handleRequestControl); + socket.on('session:requestControl', handleRequestControl); + socket.on('releaseControl', handleReleaseControl); + socket.on('session:releaseControl', handleReleaseControl); + socket.on('lockRover', handleLockToggle); + socket.on('session:lockRover', handleLockToggle); + socket.on('subscribeAll', handleSubscribeAll); + socket.on('session:subscribeAll', handleSubscribeAll); socket.on('disconnect', () => { + logger.info('Socket disconnected', socket.id); removeSocket(socket); }); }); diff --git a/server/src/services/sessionService.js b/server/src/services/sessionService.js new file mode 100644 index 00000000..08cb6fab --- /dev/null +++ b/server/src/services/sessionService.js @@ -0,0 +1,85 @@ +const io = require('../globals/io'); +const logger = require('../globals/logger').child('sessionService'); +const { getRole, roleEvents } = require('./roleService'); +const { getMode, modeEvents } = require('./modeManager'); +const roverManager = require('./roverManager'); +const { managerEvents } = roverManager; +const assignmentService = require('./assignmentService'); +const { getActiveDrivers, turnEvents } = require('./turnService'); + +function buildSession(socket) { + return { + socketId: socket?.id || null, + role: getRole(socket), + mode: getMode(), + roster: roverManager.getRoster(), + assignment: assignmentService.describeAssignment(socket?.id || ''), + activeDrivers: getActiveDrivers(), + }; +} + +function syncSocket(socket) { + if (!socket) return; + const payload = buildSession(socket); + logger.info('Syncing session', socket.id, payload.role, payload.assignment); + socket.emit('session:sync', payload); +} + +function syncAll() { + logger.info('Broadcasting session sync to all sockets'); + io.sockets.sockets.forEach((socket) => syncSocket(socket)); +} + +io.on('connection', (socket) => { + logger.info('New socket connected', socket.id); + syncSocket(socket); +}); + +roleEvents.on('change', ({ socket }) => { + if (!socket) return; + logger.info('Role changed; syncing session', socket.id); + syncSocket(socket); +}); + +assignmentService.assignmentEvents.on('update', (socketId) => { + const socket = io.sockets.sockets.get(socketId); + if (socket) { + logger.info('Assignment update; syncing session', socketId); + syncSocket(socket); + } +}); + +modeEvents.on('change', () => { + logger.info('Mode change detected; syncing all clients'); + syncAll(); +}); + +managerEvents.on('rover', () => { + logger.info('Rover roster change; syncing all clients'); + syncAll(); +}); + +managerEvents.on('lock', ({ roverId, locked }) => { + logger.info('Rover lock change', roverId, locked); + syncAll(); +}); + +managerEvents.on('driver', ({ socketId }) => { + if (!socketId) return; + const socket = io.sockets.sockets.get(socketId); + if (socket) { + logger.info('Driver assignment change; syncing session', socketId); + syncSocket(socket); + } +}); + +turnEvents.on('activeDriver', () => { + logger.info('Active driver change; syncing all clients'); + syncAll(); +}); + +module.exports = { + buildSession, + syncSocket, + syncAll, +}; diff --git a/server/src/services/turnService.js b/server/src/services/turnService.js index ff08659c..603466e0 100644 --- a/server/src/services/turnService.js +++ b/server/src/services/turnService.js @@ -1,3 +1,4 @@ +const EventEmitter = require('events'); const io = require('../globals/io'); const { sendAlert, COLORS } = require('./alertService'); const { MODES, getMode, modeEvents } = require('./modeManager'); @@ -5,6 +6,7 @@ const { MODES, getMode, modeEvents } = require('./modeManager'); const driverQueues = new Map(); // roverId -> { queue: [], current: socketId, timer: Timeout | null } const activeDrivers = new Map(); const TURN_DURATION_MS = 60 * 1000; +const turnEvents = new EventEmitter(); function driverAdded(roverId, socketId, force) { const queue = ensureQueue(roverId); @@ -104,7 +106,15 @@ function setActiveDriver(roverId, socketId) { } else { activeDrivers.set(roverId, socketId); } - io.emit('activeDriver', { roverId, socketId }); + turnEvents.emit('activeDriver', { roverId, socketId }); +} + +function getActiveDrivers() { + const map = {}; + activeDrivers.forEach((socketId, roverId) => { + map[roverId] = socketId; + }); + return map; } function stopRover(roverId) { @@ -126,4 +136,6 @@ module.exports = { driverRemoved, cleanupRover, canDrive, + getActiveDrivers, + turnEvents, }; diff --git a/ui_styling_overhaul_outline.md b/ui_styling_overhaul_outline.md new file mode 100644 index 00000000..79a91f76 --- /dev/null +++ b/ui_styling_overhaul_outline.md @@ -0,0 +1,67 @@ +# general idea: +- very compact UI +- utilitarian + - function before pretty + - tiny text, too. +- no large margins or padding. very little wasted space + - up to 1 tailwind unit unless otherwise needed +- black background, gray cards, white or gray text +- tailwind +- no title or bar at the top, vertical screen space is precious on all layouts +- alerts should be moved to a tiny toast popup that will show at the top center of the screen + +# three different layouts for the driver page +- desktop + - rover video central, no title or anything above it + - overlayed on rover video (HUD like in a video game) + - build the HUD into the video tile + - because everywhere the video is, the HUD should be too + - shows a dot that blinks on each sensor frame + - shows wheel drops and bumpers + - shows a big "loud" warning overlay during an overcurrent + - simple, easy to understand battery bar UNDERNEATH the video: + - takes in battery full, warn, and urgent values + - treat full as 100% and warn as 0% + - make the bar flash red when its 0% or lower + - on left side of rover video: + - sensor data, visualized. + - battery charge out of capacity (1763/2068) + - charging status + - OI mode + - battery voltage (in volts not mv) + - battery current + - a teeny show of the raw data (because we get it on the client and it looks cool) + - on right side of rover video + - a simple, easy control panel with the following actions: + - a button that runs the start OI, dock, then full commands (in that order) + - also contains a little status of whether or not the rover is in "driving mode" (OI in full mode) + - a button that tells the rover to seek dock + - explains that you should be straight in front of, and about a foot from the dock for a successful attempt + - contains two status indicators: + - docked / not docked (homebase true) + - charging / not charging (when chargingstate isn't "not charging" the rover is charging) + - below the upper control and video row + - center: + - a place for the room camera (not implemented yet) + - left: + - admin login and controls + - right: + - logs +- mobile in portrait mode + - rover video at the top, with HUD + - just below: + - joystick with buttons for aux. motor controls + - have a button to run all aux motors forward at max speed + - also have buttons to run motors forward / backward individually + - probably use some premade react joystick + - scroll down more to see: + - the same simple mode control buttons from the desktop layout + - also needs to have the admin login and controls + - needs to have all the functionality as the desktop page +- mobile in landscape mode + - made for videogame-like control of the rover + - control buttons and aux motors on the left + - rover video in the middle + - joystick on the right + - again, scroll down to see more + - also has to do everything diff --git a/webui/.gitignore b/webui/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/webui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/webui/README.md b/webui/README.md new file mode 100644 index 00000000..18bc70eb --- /dev/null +++ b/webui/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/webui/eslint.config.js b/webui/eslint.config.js new file mode 100644 index 00000000..4fa125da --- /dev/null +++ b/webui/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/webui/index.html b/webui/index.html new file mode 100644 index 00000000..c0b1721b --- /dev/null +++ b/webui/index.html @@ -0,0 +1,13 @@ + + + + + + + webui + + +
+ + + diff --git a/webui/package-lock.json b/webui/package-lock.json new file mode 100644 index 00000000..688c1074 --- /dev/null +++ b/webui/package-lock.json @@ -0,0 +1,4242 @@ +{ + "name": "webui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webui", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-joystick-component": "^6.2.1", + "react-router-dom": "^7.9.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "autoprefixer": "^10.4.22", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.14", + "vite": "^7.2.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.0.tgz", + "integrity": "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.253", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.253.tgz", + "integrity": "sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-joystick-component": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/react-joystick-component/-/react-joystick-component-6.2.1.tgz", + "integrity": "sha512-0G5Y5aX4hNuXB3xJCwz6Q+nYQOtC6kprNGKmZxmfoPvhepNYUiid0DbLEGZxmr/UKip3S/LUbcQUobtRCuB8IQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0.2", + "react-dom": ">=17.0.2" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", + "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/webui/package.json b/webui/package.json new file mode 100644 index 00000000..1a07cda8 --- /dev/null +++ b/webui/package.json @@ -0,0 +1,32 @@ +{ + "name": "webui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-joystick-component": "^6.2.1", + "react-router-dom": "^7.9.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "autoprefixer": "^10.4.22", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.14", + "vite": "^7.2.2" + } +} diff --git a/webui/postcss.config.js b/webui/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/webui/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/webui/public/vite.svg b/webui/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/webui/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webui/src/App.css b/webui/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/webui/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/webui/src/App.jsx b/webui/src/App.jsx new file mode 100644 index 00000000..f276b3f3 --- /dev/null +++ b/webui/src/App.jsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from 'react'; +import TelemetryPanel from './components/TelemetryPanel.jsx'; +import DrivePanel from './components/DrivePanel.jsx'; +import AlertFeed from './components/AlertFeed.jsx'; +import AdminPanel from './components/AdminPanel.jsx'; +import MobileControls, { MobileJoystick, AuxMotorControls } from './components/MobileControls.jsx'; +import { DriveControlProvider } from './context/DriveControlContext.jsx'; +import RoomCameraPanel from './components/RoomCameraPanel.jsx'; +import LogPanel from './components/LogPanel.jsx'; +import AuthPanel from './components/AuthPanel.jsx'; +import DriverVideoPanel from './components/DriverVideoPanel.jsx'; +import RightPaneTabs from './components/RightPaneTabs.jsx'; +import ModeGateOverlay from './components/ModeGateOverlay.jsx'; +import SessionSnapshot from './components/SessionSnapshot.jsx'; + +function useLayoutMode() { + const [mode, setMode] = useState(() => { + if (typeof window === 'undefined') return 'desktop'; + return window.innerWidth >= 1024 + ? 'desktop' + : window.innerWidth > window.innerHeight + ? 'mobile-landscape' + : 'mobile-portrait'; + }); + + useEffect(() => { + function updateMode() { + if (typeof window === 'undefined') return; + const { innerWidth, innerHeight } = window; + if (innerWidth >= 1024) { + setMode('desktop'); + } else if (innerWidth > innerHeight) { + setMode('mobile-landscape'); + } else { + setMode('mobile-portrait'); + } + } + updateMode(); + window.addEventListener('resize', updateMode); + return () => window.removeEventListener('resize', updateMode); + }, []); + + return mode; +} + +function DesktopLayout({ layout }) { + return ( +
+
+ + +
+
+ {/* */} + + +
+
+ ); +} + +function MobilePortraitLayout() { + return ( +
+ + + + + + + + +
+ ); +} + +function MobileLandscapeLayout() { + return ( +
+
+
+ +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ); +} + +function App() { + const layout = useLayoutMode(); + const renderedLayout = + layout === 'desktop' + ? + : layout === 'mobile-landscape' + ? + : ; + + return ( +
+ +
{renderedLayout}
+ + +
+
+ ); +} + +export default App; diff --git a/webui/src/assets/react.svg b/webui/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/webui/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webui/src/components/AdminPanel.jsx b/webui/src/components/AdminPanel.jsx new file mode 100644 index 00000000..869bcb70 --- /dev/null +++ b/webui/src/components/AdminPanel.jsx @@ -0,0 +1,104 @@ +import { useMemo, useState } from 'react'; +import { useSession } from '../context/SessionContext.jsx'; + +const MODES = [ + { key: 'open', label: 'Open' }, + { key: 'turns', label: 'Turns' }, + { key: 'admin', label: 'Admin' }, + { key: 'lockdown', label: 'Lockdown' }, +]; + +export default function AdminPanel() { + const { session, lockRover, setMode, requestControl } = useSession(); + const roster = useMemo(() => session?.roster ?? [], [session?.roster]); + const [lockStates, setLockStates] = useState({}); + + const isAdmin = + session?.role === 'admin' || + session?.role === 'lockdown' || + session?.role === 'lockdown-admin'; + + const currentMode = session?.mode ?? 'open'; + + const handleLockToggle = async (roverId, locked) => { + try { + await lockRover(roverId, locked); + setLockStates((prev) => ({ ...prev, [roverId]: locked })); + } catch (err) { + alert(err.message); + } + }; + + const handleModeChange = async (event) => { + const mode = event.target.value; + try { + await setMode(mode); + } catch (err) { + alert(err.message); + } + }; + + const handleForceControl = async (roverId) => { + try { + await requestControl(roverId, { force: true }); + } catch (err) { + alert(err.message); + } + }; + + const lockMap = useMemo(() => { + const map = {}; + roster.forEach((rover) => { + map[rover.id] = lockStates[rover.id] ?? rover.locked; + }); + return map; + }, [roster, lockStates]); + + if (!isAdmin) return null; + + return ( +
+
+ Admin controls + +
+ +
+ {roster.map((rover) => ( +
+
+

{rover.name}

+

{lockMap[rover.id] ? 'locked' : 'unlocked'}

+
+
+ + +
+
+ ))} +
+
+ ); +} diff --git a/webui/src/components/AdvancedSettings.jsx b/webui/src/components/AdvancedSettings.jsx new file mode 100644 index 00000000..d08d3c35 --- /dev/null +++ b/webui/src/components/AdvancedSettings.jsx @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; +import { useDriveControl } from '../context/DriveControlContext.jsx'; +import { useSocket } from '../context/SocketContext.jsx'; +import AuthPanel from './AuthPanel.jsx'; +import AdminPanel from './AdminPanel.jsx'; + +const manualTabs = [ + { key: 'start', label: 'Start OI' }, + { key: 'safe', label: 'Safe' }, + { key: 'full', label: 'Full' }, + { key: 'passive', label: 'Passive' }, + { key: 'dock', label: 'Dock' }, +]; + +export default function AdvancedSettings() { + const { roverId, sendOiCommand } = useDriveControl(); + const socket = useSocket(); + const canControl = Boolean(roverId); + + const sensorButtons = useMemo( + () => [ + { key: 'start', label: 'Enable stream', enable: true }, + { key: 'stop', label: 'Disable stream', enable: false }, + ], + [], + ); + + const handleSensorToggle = (enable) => { + if (!roverId) return; + socket.emit('command', { + roverId, + type: 'sensorStream', + data: { sensorStream: { enable } }, + }); + }; + + return ( +
+
+

Manual OI commands

+
+ {manualTabs.map((tab) => ( + + ))} +
+
+
+

Sensor stream

+
+ {sensorButtons.map((btn) => ( + + ))} +
+ {!canControl &&

Assign a rover to toggle streams.

} +
+ + +
+ ); +} diff --git a/webui/src/components/AlertFeed.jsx b/webui/src/components/AlertFeed.jsx new file mode 100644 index 00000000..952a2e8f --- /dev/null +++ b/webui/src/components/AlertFeed.jsx @@ -0,0 +1,54 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useSession } from '../context/SessionContext.jsx'; + +const LIFETIME_MS = 5000; + +function buildKey(alert) { + if (alert.id) return alert.id; + if (alert.timestamp) return `${alert.timestamp}-${alert.message}`; + return `${alert.title || 'alert'}-${alert.message}`; +} + +export default function AlertFeed() { + const { alerts } = useSession(); + const [now, setNow] = useState(() => Date.now()); + const latest = useMemo(() => alerts.slice(-5).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]); + + useEffect(() => { + if (!latest.length) return undefined; + const interval = setInterval(() => setNow(Date.now()), 200); + return () => clearInterval(interval); + }, [latest.length]); + + const visible = latest + .map((item) => ({ + ...item, + age: now - (item.alert.receivedAt ?? item.alert.timestamp ?? 0), + })) + .filter((item) => item.age <= LIFETIME_MS); + + if (!visible.length) return null; + + return ( +
+ {visible.map((toast) => ( + + ))} +
+ ); +} + +function AlertToast({ alert }) { + const colorClasses = + alert.color === 'error' + ? 'bg-red-500/30 text-red-100' + : alert.color === 'warn' + ? 'bg-amber-500/30 text-amber-100' + : 'bg-emerald-500/30 text-emerald-100'; + return ( +
+

{alert.title || 'Alert'}

+

{alert.message}

+
+ ); +} diff --git a/webui/src/components/AuthPanel.jsx b/webui/src/components/AuthPanel.jsx new file mode 100644 index 00000000..1f0e7727 --- /dev/null +++ b/webui/src/components/AuthPanel.jsx @@ -0,0 +1,67 @@ +import { useState } from 'react'; +import { useSession } from '../context/SessionContext.jsx'; + +export default function AuthPanel() { + const { login, setRole } = useSession(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + + async function handleLogin(event) { + event.preventDefault(); + setLoading(true); + try { + await login(username, password); + setUsername(''); + setPassword(''); + } catch (err) { + alert(err.message); + } finally { + setLoading(false); + } + } + + return ( +
+

Admin login

+ + setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + + +
+ + +
+
+ ); +} diff --git a/webui/src/components/DrivePanel.jsx b/webui/src/components/DrivePanel.jsx new file mode 100644 index 00000000..dcbc77a1 --- /dev/null +++ b/webui/src/components/DrivePanel.jsx @@ -0,0 +1,71 @@ +import { useDriveControl } from '../context/DriveControlContext.jsx'; +import { useTelemetryFrame } from '../context/TelemetryContext.jsx'; + +export default function DrivePanel() { + const { roverId, runStartDockFull, seekDock } = useDriveControl(); + const frame = useTelemetryFrame(roverId); + const sensors = frame?.sensors || {}; + const drivingMode = (sensors.oiMode?.label || '').toLowerCase() === 'full'; + const docked = Boolean(sensors.chargingSources?.homeBase); + const charging = Boolean( + sensors.chargingState?.label && sensors.chargingState.label.toLowerCase() !== 'not charging', + ); + + return ( +
+
+ Drive control + {roverId ? `Rover ${roverId}` : 'unassigned'} +
+
+ + +
+
+ ); +} + +function ActionCard({ title, description, statuses, tone, onClick, disabled, footnote }) { + const baseColor = tone === 'indigo' ? 'bg-indigo-600' : 'bg-emerald-600'; + return ( + + ); +} diff --git a/webui/src/components/DriverVideoPanel.jsx b/webui/src/components/DriverVideoPanel.jsx new file mode 100644 index 00000000..c55c4d8d --- /dev/null +++ b/webui/src/components/DriverVideoPanel.jsx @@ -0,0 +1,35 @@ +import { useSession } from '../context/SessionContext.jsx'; +import { useTelemetryFrame } from '../context/TelemetryContext.jsx'; +import { useVideoRequests } from '../hooks/useVideoRequests.js'; +import VideoTile from './VideoTile.jsx'; + +export default function DriverVideoPanel() { + const { session } = useSession(); + const roverId = session?.assignment?.roverId; + const sources = useVideoRequests(roverId ? [roverId] : []); + const info = roverId ? sources[roverId] : null; + const frame = useTelemetryFrame(roverId); + const batteryRecord = + roverId && session?.roster + ? session.roster.find((item) => String(item.id) === String(roverId)) + : null; + const batteryConfig = batteryRecord?.battery ?? null; + + return ( +
+ {roverId ? ( +
+ +
+ ) : ( +

Assign a rover to view the video feed.

+ )} +
+ ); +} diff --git a/webui/src/components/HelpPanel.jsx b/webui/src/components/HelpPanel.jsx new file mode 100644 index 00000000..3aeb2a08 --- /dev/null +++ b/webui/src/components/HelpPanel.jsx @@ -0,0 +1,32 @@ +export default function HelpPanel({ layout }) { + const presets = { + desktop: [ + 'Use WASD + Shift to drive. Video must stay focused.', + 'Macros on the left prep the rover before moving.', + 'Switch tabs on the right for telemetry, room, and advanced tools.', + ], + 'mobile-portrait': [ + 'Keep joystick centered when not driving.', + 'Auxiliary buttons sit beside the joystick – use them carefully.', + 'Scroll to access drive/telemetry if screen space is limited.', + ], + 'mobile-landscape': [ + 'Joystick and video are side by side for game-style control.', + 'Rotate back to portrait if you need admin controls quickly.', + 'Tabs are desktop-only; mobile shows sections stacked.', + ], + }; + const tips = presets[layout] || presets.desktop; + return ( +
+

Help

+
    + {tips.map((tip) => ( +
  • + {tip} +
  • + ))} +
+
+ ); +} diff --git a/webui/src/components/LogPanel.jsx b/webui/src/components/LogPanel.jsx new file mode 100644 index 00000000..2012fbbc --- /dev/null +++ b/webui/src/components/LogPanel.jsx @@ -0,0 +1,30 @@ +import { useSession } from '../context/SessionContext.jsx'; + +export default function LogPanel() { + const { logs } = useSession(); + return ( +
+
+ Server logs + {logs.length} +
+
+ {logs.length === 0 ? ( +

No logs yet.

+ ) : ( + logs + .slice() + .reverse() + .map((entry) => ( +
+ {entry.timestamp}{' '} + [{entry.level}]{' '} + {entry.label && [{entry.label}]}{' '} + {entry.message} +
+ )) + )} +
+
+ ); +} diff --git a/webui/src/components/MobileControls.jsx b/webui/src/components/MobileControls.jsx new file mode 100644 index 00000000..20e4acfc --- /dev/null +++ b/webui/src/components/MobileControls.jsx @@ -0,0 +1,132 @@ +import { useCallback } from 'react'; +import { Joystick } from 'react-joystick-component'; +import { useDriveControl } from '../context/DriveControlContext.jsx'; + +function clampUnit(value = 0) { + if (typeof value !== 'number' || Number.isNaN(value)) return 0; + return Math.max(-1, Math.min(1, value)); +} + +function MobileJoystick() { + const { roverId, driveWithVector, stopMotors } = useDriveControl(); + const disabled = !roverId; + + const handleMove = useCallback( + (event = {}) => { + if (disabled) return; + const x = clampUnit(event.x ?? 0); + const y = clampUnit(event.y ?? 0); + driveWithVector({ x, y }); + }, + [disabled, driveWithVector], + ); + + const handleStop = useCallback(() => { + if (disabled) return; + driveWithVector({ x: 0, y: 0 }); + }, [disabled, driveWithVector]); + + return ( +
+
Joystick
+
+ +
+

+ Drag to drive. Release to stop sending drive commands. +

+ +
+ ); +} + +function AuxMotorControls() { + const { roverId, setAuxMotors } = useDriveControl(); + const disabled = !roverId; + + const runAllForward = () => { + if (disabled) return; + setAuxMotors({ main: 127, side: 127, vacuum: 127 }); + }; + + const stopAll = () => { + if (disabled) return; + setAuxMotors({ main: 0, side: 0, vacuum: 0 }); + }; + + const auxButtons = [ + { label: 'Main +', values: { main: 127 } }, + { label: 'Main -', values: { main: -127 } }, + { label: 'Side +', values: { side: 127 } }, + { label: 'Side -', values: { side: -127 } }, + { label: 'Vacuum Max', values: { vacuum: 127 } }, + { label: 'Vacuum Off', values: { vacuum: 0 } }, + ]; + + return ( +
+
Aux motors
+
+ + +
+
+ {auxButtons.map((btn) => ( + + ))} +
+
+ ); +} + +export default function MobileControlsStack() { + return ( +
+
+ +
+
+ +
+
+ ); +} + +export { MobileJoystick, AuxMotorControls }; diff --git a/webui/src/components/ModeGateOverlay.jsx b/webui/src/components/ModeGateOverlay.jsx new file mode 100644 index 00000000..3c5f2c62 --- /dev/null +++ b/webui/src/components/ModeGateOverlay.jsx @@ -0,0 +1,52 @@ +import AuthPanel from './AuthPanel.jsx'; +import { useSession } from '../context/SessionContext.jsx'; + +const PRIVILEGED_ROLES = new Set(['admin', 'lockdown', 'lockdown-admin']); +const RESTRICTED_MODES = new Set(['admin', 'lockdown']); + +function getModeDetails(mode = 'admin') { + if (mode === 'lockdown') { + return { + title: 'Lockdown in effect', + description: + 'Only the lockdown admin can remain connected right now. If you have credentials, log in below. Otherwise please stand by until the server re-opens.', + }; + } + return { + title: 'Admin mode active', + description: + 'The server is temporarily restricted to administrators. Log in below if you are authorized, or wait for admin mode to end.', + }; +} + +export default function ModeGateOverlay() { + const { session } = useSession(); + const mode = session?.mode; + const role = session?.role; + const restricted = RESTRICTED_MODES.has(mode); + const privileged = PRIVILEGED_ROLES.has(role); + + if (!restricted || privileged) { + return null; + } + + const details = getModeDetails(mode); + + return ( +
+
+
+

{details.title}

+

{details.description}

+
+
+ +
+

+ Your controls are paused until access is granted. You will automatically regain the interface once the mode + changes or after a successful login. +

+
+
+ ); +} diff --git a/webui/src/components/RightPaneTabs.jsx b/webui/src/components/RightPaneTabs.jsx new file mode 100644 index 00000000..95ff6f27 --- /dev/null +++ b/webui/src/components/RightPaneTabs.jsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import TelemetryPanel from './TelemetryPanel.jsx'; +import DrivePanel from './DrivePanel.jsx'; +import RoomCameraPanel from './RoomCameraPanel.jsx'; +import AdvancedSettings from './AdvancedSettings.jsx'; +import HelpPanel from './HelpPanel.jsx'; + +function TabButton({ active, onClick, children }) { + return ( + + ); +} + +function RoomControlsPlaceholder() { + return ( +
+

Room controls

+

Coming soon: lighting, docks, and environmental toggles.

+
+ ); +} + +export default function RightPaneTabs({ layout }) { + const [active, setActive] = useState('telemetry'); + + return ( +
+
+ setActive('telemetry')}> + Rover Controls + + setActive('room')}> + Room Controls + + setActive('advanced')}> + Advanced + + setActive('help')}> + Help + +
+
+ {active === 'telemetry' && ( +
+ + +
+ )} + {active === 'room' && ( +
+ + +
+ )} + {active === 'advanced' && } + {active === 'help' && } +
+
+ ); +} diff --git a/webui/src/components/RoomCameraPanel.jsx b/webui/src/components/RoomCameraPanel.jsx new file mode 100644 index 00000000..39b3e7fe --- /dev/null +++ b/webui/src/components/RoomCameraPanel.jsx @@ -0,0 +1,8 @@ +export default function RoomCameraPanel() { + return ( +
+

Room camera

+

Feed placeholder. Wire upcoming room cam here.

+
+ ); +} diff --git a/webui/src/components/SessionSnapshot.jsx b/webui/src/components/SessionSnapshot.jsx new file mode 100644 index 00000000..02d1adeb --- /dev/null +++ b/webui/src/components/SessionSnapshot.jsx @@ -0,0 +1,13 @@ +import { useMemo } from 'react'; +import { useSession } from '../context/SessionContext.jsx'; + +export default function SessionSnapshot() { + const { session } = useSession(); + const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]); + return ( +
+

Session snapshot

+
{payload}
+
+ ); +} diff --git a/webui/src/components/TelemetryPanel.jsx b/webui/src/components/TelemetryPanel.jsx new file mode 100644 index 00000000..e4b1fe2d --- /dev/null +++ b/webui/src/components/TelemetryPanel.jsx @@ -0,0 +1,145 @@ +import { useMemo, useState } from 'react'; +import { useSession } from '../context/SessionContext.jsx'; +import { useTelemetryFrame } from '../context/TelemetryContext.jsx'; + +function formatMetric(value, fallback = '--') { + if (value == null || value === '') return fallback; + return value; +} + +export default function TelemetryPanel() { + const { connected, session, requestControl } = useSession(); + const roverId = session?.assignment?.roverId; + const frame = useTelemetryFrame(roverId); + const sensors = frame?.sensors || {}; + const voltage = sensors.voltageMv != null ? `${(sensors.voltageMv / 1000).toFixed(2)} V` : null; + const current = sensors.currentMa != null ? `${sensors.currentMa} mA` : null; + const charge = sensors.batteryChargeMah; + const capacity = sensors.batteryCapacityMah; + const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null; + // const rawSnippet = frame?.raw ? (frame.raw.length > 80 ? `${frame.raw.slice(0, 80)}…` : frame.raw) : null; + const rawSnippet = frame?.raw ? frame.raw : null; + const roster = session?.roster ?? []; + const activeDriverId = roverId ? session?.activeDrivers?.[roverId] : null; + const driverLabel = !roverId + ? 'n/a' + : activeDriverId + ? activeDriverId === session?.socketId + ? 'You' + : activeDriverId.slice(0, 6) + : 'Available'; + const isAdmin = useMemo( + () => session?.role === 'admin' || session?.role === 'lockdown' || session?.role === 'lockdown-admin', + [session?.role], + ); + const [pending, setPending] = useState({}); + + async function handleRequest(roverId) { + if (!roverId) return; + setPending((prev) => ({ ...prev, [roverId]: true })); + try { + await requestControl(roverId); + } catch (err) { + alert(err.message); + } finally { + setPending((prev) => ({ ...prev, [roverId]: false })); + } + } + + return ( +
+
+ {connected ? 'online' : 'offline'} + · role {session?.role || 'unknown'} + · mode {session?.mode || '--'} + {updated && · sensors {updated}} + · driver {driverLabel} +
+ {!roverId ? ( +

Assign a rover to view sensors.

+ ) : !frame ? ( +

Waiting for sensor frames…

+ ) : ( + <> +
+ + + + + + + +
+
+ + + + +
+ + + )} + + + + {rawSnippet && ( + // console.log('rawSnippet:', rawSnippet), +
+          {rawSnippet}
+        
+ )} +
+

Rovers

+ {roster.length === 0 ? ( + // if there ARE NO rovers +

No roster data.

+ ) : ( + // if there ARE rovers +
    + {roster.map((rover) => ( +
  • +
    +

    {rover.name}

    +

    + {rover.locked ? 'locked' : 'free'} · {rover.lastSeen ? 'seen' : 'unknown'} +

    +
    + {isAdmin && ( + + )} +
  • + ))} +
+ )} +
+
+ ); +} + +function CliffBar({ value }) { + // Render a small vertical bar; value is clamped to 0-100 for display + // const pct = value == null ? 0 : Math.max(0, Math.min(100, value)); + // goes from 0 to 4095. calculate percentage for height + const pct = value == null ? 0 : Math.max(0, Math.min(100, (value / 4095) * 100)); + const height = `${pct}%`; + return ( +
+
+
+ ); +} + +function Metric({ label, value }) { + return ( +
+ {label}: {value} +
+ ); +} diff --git a/webui/src/components/VideoTile.jsx b/webui/src/components/VideoTile.jsx new file mode 100644 index 00000000..a32cb304 --- /dev/null +++ b/webui/src/components/VideoTile.jsx @@ -0,0 +1,251 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { WhepPlayer } from '../lib/whepPlayer.js'; + +const RESTART_DELAY_MS = 2000; +const UNMUTE_RETRY_MS = 3000; + +export default function VideoTile({ sessionInfo, label, forceMute = false, telemetryFrame, batteryConfig }) { + const videoRef = useRef(null); + const restartTimer = useRef(null); + const unmuteTimer = useRef(null); + const [status, setStatus] = useState('idle'); + const [detail, setDetail] = useState(null); + const [restartToken, setRestartToken] = useState(0); + const [muted, setMuted] = useState(true); + const sensors = telemetryFrame?.sensors; + const batteryCharge = sensors?.batteryChargeMah ?? null; + // console.log('[BatteryBarDebug]', { + // frameSensors: sensors, + // batteryCharge, + // batteryCapacity, + // config: batteryConfig, + // }); + const wheelOvercurrents = sensors?.wheelOvercurrents || null; + const overcurrentActive = Boolean( + wheelOvercurrents && Object.values(wheelOvercurrents).some((value) => Boolean(value)), + ); + + const scheduleRestart = useCallback(() => { + clearTimeout(restartTimer.current); + restartTimer.current = setTimeout(() => setRestartToken(Date.now()), RESTART_DELAY_MS); + }, []); + + const attemptUnmute = useCallback( + (delay = 0) => { + if (forceMute) return; + clearTimeout(unmuteTimer.current); + + const scheduleRetry = () => { + clearTimeout(unmuteTimer.current); + unmuteTimer.current = setTimeout(() => { + if (!forceMute) { + tryPlay(); + } + }, UNMUTE_RETRY_MS); + }; + + const tryPlay = async () => { + const video = videoRef.current; + if (!video) return; + try { + video.muted = false; + await video.play(); + setMuted(false); + } catch { + video.muted = true; + setMuted(true); + scheduleRetry(); + } + }; + + unmuteTimer.current = setTimeout(tryPlay, delay); + }, + [forceMute], + ); + + useEffect( + () => () => { + clearTimeout(restartTimer.current); + clearTimeout(unmuteTimer.current); + }, + [], + ); + + useEffect(() => { + if (status === 'playing') { + attemptUnmute(0); + } + }, [status, attemptUnmute]); + + useEffect(() => { + if (!sessionInfo?.url || !videoRef.current) { + return undefined; + } + let active = true; + let player; + const resetMuteId = setTimeout(() => setMuted(true), 0); + const handleStatus = (nextStatus, info) => { + if (!active) return; + setStatus(nextStatus); + setDetail(info || null); + if (['error', 'failed', 'disconnected', 'closed'].includes(nextStatus)) { + scheduleRestart(); + } + }; + + player = new WhepPlayer({ + url: sessionInfo.url, + token: sessionInfo.token, + video: videoRef.current, + onStatus: handleStatus, + }); + + player.start().catch((err) => { + if (!active) return; + setStatus('error'); + setDetail(err.message); + scheduleRestart(); + }); + + return () => { + active = false; + clearTimeout(resetMuteId); + player?.stop(); + }; + }, [sessionInfo?.url, sessionInfo?.token, restartToken, scheduleRestart]); + + useEffect(() => { + if (status === 'stopped' && sessionInfo?.url) { + scheduleRestart(); + } + }, [status, sessionInfo?.url, scheduleRestart]); + + const renderedStatus = !sessionInfo?.url + ? 'waiting' + : status === 'error' + ? `Error: ${detail || 'unknown'}` + : detail + ? `${status} (${detail})` + : status; + + return ( +
+
+
+ +
+ ); +} + +function BatteryBar({ charge, config, label, status }) { + const full = config?.Full; + const warn = config?.Warn; + const urgent = config?.Urgent ?? null; + if (charge == null || full == null || warn == null) { + return ( +
+
+ {label} + {status} +
+

Battery telemetry unavailable

+
+ ); + } + + const span = full - warn; + if (span <= 0) return null; + const normalized = (charge - warn) / span; + const percent = Math.min(1, Math.max(0, normalized)); + const percentDisplay = Math.round(percent * 100); + const percentText = `${percentDisplay}%`; + const depleted = normalized <= 0; + const warnTriggered = urgent != null && charge <= urgent; + const barClass = depleted ? 'bg-red-500 animate-pulse' : warnTriggered ? 'bg-amber-400' : 'bg-emerald-500'; + return ( +
+
+ {label} + {status} +
+
+
+ + {percentText} + +
+
+ ); +} + +function HudOverlay({ frame }) { + const sensors = frame?.sensors; + const bumps = sensors?.bumpsAndWheelDrops || {}; + const [now, setNow] = useState(() => Date.now()); + + useEffect(() => { + const interval = setInterval(() => setNow(Date.now()), 150); + return () => clearInterval(interval); + }, []); + + const pulse = frame?.receivedAt ? now - frame.receivedAt < 200 : false; + + const bumperBadges = [ + { label: 'B-L', active: bumps.bumpLeft }, + { label: 'B-R', active: bumps.bumpRight }, + ]; + const wheelBadges = [ + { label: 'Drop L', active: bumps.wheelDropLeft }, + { label: 'Drop R', active: bumps.wheelDropRight }, + ]; + + return ( +
+
+
+ + sensor +
+
+ {bumperBadges.map((badge) => ( + + ))} +
+
+
+ {wheelBadges.map((badge) => ( + + ))} +
+
+ ); +} + +function HudBadge({ label, active }) { + return ( + + {label} + + ); +} + +function OvercurrentOverlay({ active }) { + if (!active) return null; + return ( +
+
Overcurrent
+
+ ); +} diff --git a/webui/src/context/DriveControlContext.jsx b/webui/src/context/DriveControlContext.jsx new file mode 100644 index 00000000..121d63ba --- /dev/null +++ b/webui/src/context/DriveControlContext.jsx @@ -0,0 +1,19 @@ +import { createContext, useContext } from 'react'; +import { useDriveControls as useDriveControlsHook } from '../hooks/useDriveControls.js'; + +/* eslint-disable react-refresh/only-export-components */ + +const DriveControlContext = createContext(null); + +export function DriveControlProvider({ children }) { + const controls = useDriveControlsHook(); + return {children}; +} + +export function useDriveControl() { + const context = useContext(DriveControlContext); + if (!context) { + throw new Error('useDriveControl must be used within DriveControlProvider'); + } + return context; +} diff --git a/webui/src/context/SessionContext.jsx b/webui/src/context/SessionContext.jsx new file mode 100644 index 00000000..8027f6eb --- /dev/null +++ b/webui/src/context/SessionContext.jsx @@ -0,0 +1,112 @@ +/* eslint-disable react-refresh/only-export-components */ + +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useSocket } from './SocketContext.jsx'; + +const SessionContext = createContext({ + connected: false, + session: null, + logs: [], + login: async () => {}, + setRole: async () => {}, + requestControl: async () => {}, + releaseControl: async () => {}, + subscribeAll: async () => {}, +}); + +function useAckEmitter(socket) { + return useCallback( + (event, payload = {}) => + new Promise((resolve, reject) => { + socket.emit(event, payload, (resp = {}) => { + if (resp.error) { + reject(new Error(resp.error)); + } else { + resolve(resp); + } + }); + }), + [socket], + ); +} + +export function SessionProvider({ children }) { + const socket = useSocket(); + const emitWithAck = useAckEmitter(socket); + const [session, setSession] = useState(null); + const [logs, setLogs] = useState([]); + const [alerts, setAlerts] = useState([]); + const [connected, setConnected] = useState(socket.connected); + + useEffect(() => { + const onConnect = () => setConnected(true); + const onDisconnect = () => setConnected(false); + socket.on('connect', onConnect); + socket.on('disconnect', onDisconnect); + return () => { + socket.off('connect', onConnect); + socket.off('disconnect', onDisconnect); + }; + }, [socket]); + + useEffect(() => { + function handleSession(payload) { + setSession(payload); + } + function handleLogInit(entries = []) { + setLogs(entries); + } + function handleLogEntry(entry) { + setLogs((prev) => [...prev.slice(-199), entry]); + } + socket.on('session:sync', handleSession); + socket.on('log:init', handleLogInit); + socket.on('log:entry', handleLogEntry); + socket.on('alert:new', (payload = {}) => { + setAlerts((prev) => [ + ...prev.slice(-49), + { + ...payload, + receivedAt: Date.now(), + }, + ]); + }); + return () => { + socket.off('session:sync', handleSession); + socket.off('log:init', handleLogInit); + socket.off('log:entry', handleLogEntry); + socket.off('alert:new'); + }; + }, [socket]); + + const actions = useMemo( + () => ({ + login: (username, password) => emitWithAck('auth:login', { username, password }), + setRole: (role) => emitWithAck('session:setRole', { role }), + requestControl: (roverId, options = {}) => + emitWithAck('session:requestControl', { roverId, ...options }), + releaseControl: (roverId) => emitWithAck('session:releaseControl', { roverId }), + subscribeAll: () => emitWithAck('session:subscribeAll'), + lockRover: (roverId, locked) => emitWithAck('session:lockRover', { roverId, locked }), + setMode: (mode) => emitWithAck('setMode', { mode }), + }), + [emitWithAck], + ); + + const value = useMemo( + () => ({ + connected, + session, + logs, + alerts, + ...actions, + }), + [actions, alerts, connected, logs, session], + ); + + return {children}; +} + +export function useSession() { + return useContext(SessionContext); +} diff --git a/webui/src/context/SocketContext.jsx b/webui/src/context/SocketContext.jsx new file mode 100644 index 00000000..fd96a846 --- /dev/null +++ b/webui/src/context/SocketContext.jsx @@ -0,0 +1,11 @@ +/* eslint-disable react-refresh/only-export-components */ + +// src/context/SocketContext.jsx +import { createContext, useContext } from "react"; +import { socket } from "../lib/socket"; + +const SocketContext = createContext(socket); +export const SocketProvider = ({ children }) => ( + {children} +); +export const useSocket = () => useContext(SocketContext); diff --git a/webui/src/context/TelemetryContext.jsx b/webui/src/context/TelemetryContext.jsx new file mode 100644 index 00000000..0a37f312 --- /dev/null +++ b/webui/src/context/TelemetryContext.jsx @@ -0,0 +1,44 @@ +/* eslint-disable react-refresh/only-export-components */ + +import { createContext, useContext, useMemo, useState, useEffect } from 'react'; +import { useSocket } from './SocketContext.jsx'; + +const TelemetryContext = createContext({ frames: {} }); + +export function TelemetryProvider({ children }) { + const socket = useSocket(); + const [frames, setFrames] = useState({}); + + useEffect(() => { + function handleSensorFrame({ roverId, sensors = {}, frame = {} }) { + if (!roverId) return; + setFrames((prev) => ({ + ...prev, + [roverId]: { + roverId, + sensors, + raw: frame?.data || null, + receivedAt: Date.now(), + }, + })); + } + + socket.on('sensorFrame', handleSensorFrame); + return () => { + socket.off('sensorFrame', handleSensorFrame); + }; + }, [socket]); + + const value = useMemo(() => ({ frames }), [frames]); + return {children}; +} + +export function useTelemetryFrames() { + return useContext(TelemetryContext).frames; +} + +export function useTelemetryFrame(roverId) { + const frames = useTelemetryFrames(); + if (!roverId) return null; + return frames[roverId] ?? null; +} diff --git a/webui/src/hooks/useDriveControls.js b/webui/src/hooks/useDriveControls.js new file mode 100644 index 00000000..79a578ef --- /dev/null +++ b/webui/src/hooks/useDriveControls.js @@ -0,0 +1,253 @@ +/* global Buffer */ + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useSocket } from '../context/SocketContext.jsx'; +import { useSession } from '../context/SessionContext.jsx'; + +const OI_COMMANDS = { + start: [128], + safe: [131], + full: [132], + passive: [128], + dock: [143], +}; + +const AUX_LIMITS = { + main: [-127, 127], + side: [-127, 127], + vacuum: [0, 127], +}; + +const COMMAND_DELAY_MS = 200; + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} + +function computeSpeeds(keys) { + const forward = keys.has('w'); + const backward = keys.has('s'); + const left = keys.has('a'); + const right = keys.has('d'); + const fast = keys.has('shift'); + const base = fast ? 300 : 150; + let leftSpeed = 0; + let rightSpeed = 0; + + if (forward && !backward) { + leftSpeed += base; + rightSpeed += base; + } else if (backward && !forward) { + leftSpeed -= base; + rightSpeed -= base; + } + + if (left && !right) { + leftSpeed -= base; + rightSpeed += base; + } else if (right && !left) { + leftSpeed += base; + rightSpeed -= base; + } + + if (!forward && !backward && (left || right)) { + leftSpeed = left ? -base : base; + rightSpeed = left ? base : -base; + } + + return { + left: clamp(leftSpeed, -500, 500), + right: clamp(rightSpeed, -500, 500), + }; +} + +function clampUnit(value) { + if (typeof value !== 'number' || Number.isNaN(value)) return 0; + return Math.max(-1, Math.min(1, value)); +} + +function shouldIgnoreEvent(event) { + const target = event.target; + if (!target) return false; + const tag = target.tagName; + return ( + tag === 'INPUT' || + tag === 'TEXTAREA' || + target.isContentEditable || + (tag === 'SELECT' && ['w', 'a', 's', 'd', 'Shift'].includes(event.key)) + ); +} + +function bytesToBase64(bytes) { + const binary = String.fromCharCode(...bytes); + if (typeof btoa === 'function') return btoa(binary); + if (typeof Buffer !== 'undefined') { + return Buffer.from(bytes).toString('base64'); + } + throw new Error('No base64 encoder available'); +} + +export function useDriveControls() { + const socket = useSocket(); + const { session } = useSession(); + const roverId = session?.assignment?.roverId; + const keysRef = useRef(new Set()); + const lastSpeedsRef = useRef({ left: 0, right: 0 }); + const [currentSpeeds, setCurrentSpeeds] = useState(() => ({ left: 0, right: 0 })); + + const emitCommand = useCallback( + (payload, cb) => { + if (!roverId) return; + socket.emit('command', { roverId, ...payload }, cb); + }, + [socket, roverId], + ); + + const enableSensorStream = useCallback(() => { + if (!roverId) return; + emitCommand({ + type: 'sensorStream', + data: { sensorStream: { enable: true } }, + }); + }, [emitCommand, roverId]); + + const sendDriveUpdate = useCallback(() => { + if (!roverId) return; + const speeds = computeSpeeds(keysRef.current); + const last = lastSpeedsRef.current; + if (speeds.left === last.left && speeds.right === last.right) return; + lastSpeedsRef.current = speeds; + setCurrentSpeeds(speeds); + emitCommand({ + type: 'drive', + data: { driveDirect: speeds }, + }); + }, [emitCommand, roverId]); + + const driveWithVector = useCallback( + ({ x = 0, y = 0, boost = false } = {}) => { + if (!roverId) return; + const base = boost ? 400 : 250; + const forward = clampUnit(y) * base; + const turn = clampUnit(x) * base; + const speeds = { + left: clamp(Math.round(forward + turn), -500, 500), + right: clamp(Math.round(forward - turn), -500, 500), + }; + lastSpeedsRef.current = speeds; + setCurrentSpeeds(speeds); + emitCommand({ + type: 'drive', + data: { driveDirect: speeds }, + }); + }, + [emitCommand, roverId], + ); + + const sendMotorPwm = useCallback( + ({ main = 0, side = 0, vacuum = 0 } = {}) => { + if (!roverId) return; + const payload = { + main: clamp(main, AUX_LIMITS.main[0], AUX_LIMITS.main[1]), + side: clamp(side, AUX_LIMITS.side[0], AUX_LIMITS.side[1]), + vacuum: clamp(vacuum, AUX_LIMITS.vacuum[0], AUX_LIMITS.vacuum[1]), + }; + emitCommand({ + type: 'motors', + data: { motorPwm: payload }, + }); + }, + [emitCommand, roverId], + ); + + const stopMotors = useCallback(() => { + if (!roverId) return; + keysRef.current.clear(); + lastSpeedsRef.current = { left: 0, right: 0 }; + setCurrentSpeeds(lastSpeedsRef.current); + sendMotorPwm({ main: 0, side: 0, vacuum: 0 }); + }, [roverId, sendMotorPwm]); + + const sendOiCommand = useCallback( + (key) => { + if (!roverId) return; + const bytes = OI_COMMANDS[key]; + if (!bytes) return; + emitCommand({ + type: 'raw', + data: { raw: bytesToBase64(bytes) }, + }); + enableSensorStream(); + }, + [emitCommand, enableSensorStream, roverId], + ); + + const runStartDockFull = useCallback(async () => { + if (!roverId) return; + for (const key of ['start', 'dock', 'full']) { + sendOiCommand(key); + // brief pause so the commands aren't collapsed by the rover + await sleep(COMMAND_DELAY_MS); + } + }, [roverId, sendOiCommand]); + + const seekDock = useCallback(() => { + sendOiCommand('dock'); + }, [sendOiCommand]); + + const setAuxMotors = useCallback( + (values) => { + if (!roverId) return; + sendMotorPwm(values); + }, + [roverId, sendMotorPwm], + ); + + useEffect(() => { + if (!roverId) { + keysRef.current.clear(); + lastSpeedsRef.current = { left: 0, right: 0 }; + setCurrentSpeeds(lastSpeedsRef.current); + return undefined; + } + + function onKeyDown(event) { + if (shouldIgnoreEvent(event)) return; + const key = event.key.toLowerCase(); + if (!['w', 'a', 's', 'd', 'shift'].includes(key)) return; + if (keysRef.current.has(key)) return; + keysRef.current.add(key); + sendDriveUpdate(); + } + + function onKeyUp(event) { + const key = event.key.toLowerCase(); + if (!keysRef.current.has(key)) return; + keysRef.current.delete(key); + sendDriveUpdate(); + } + + window.addEventListener('keydown', onKeyDown); + window.addEventListener('keyup', onKeyUp); + + enableSensorStream(); + + return () => { + window.removeEventListener('keydown', onKeyDown); + window.removeEventListener('keyup', onKeyUp); + }; + }, [roverId, sendDriveUpdate, enableSensorStream]); + + return { + roverId, + speeds: currentSpeeds, + stopMotors, + sendOiCommand, + runStartDockFull, + seekDock, + setAuxMotors, + driveWithVector, + }; +} diff --git a/webui/src/hooks/useSpectatorMode.js b/webui/src/hooks/useSpectatorMode.js new file mode 100644 index 00000000..5e892b0c --- /dev/null +++ b/webui/src/hooks/useSpectatorMode.js @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react'; +import { useSession } from '../context/SessionContext.jsx'; + +export function useSpectatorMode() { + const { session, setRole, subscribeAll } = useSession(); + const [ready, setReady] = useState(false); + + useEffect(() => { + let cancelled = false; + async function ensureSpectator() { + try { + if (session?.role !== 'spectator') { + await setRole('spectator'); + } + await subscribeAll(); + if (!cancelled) { + setReady(true); + } + } catch (err) { + console.error('Failed to enter spectator mode', err); + } + } + ensureSpectator(); + return () => { + cancelled = true; + }; + }, [session?.role, setRole, subscribeAll]); + + return ready; +} diff --git a/webui/src/hooks/useVideoRequests.js b/webui/src/hooks/useVideoRequests.js new file mode 100644 index 00000000..1c4d9cda --- /dev/null +++ b/webui/src/hooks/useVideoRequests.js @@ -0,0 +1,43 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useSocket } from '../context/SocketContext.jsx'; + +export function useVideoRequests(roverIds = []) { + const socket = useSocket(); + const [sources, setSources] = useState({}); + const normalizedKey = Array.isArray(roverIds) ? roverIds.filter(Boolean).join('|') : ''; + + useEffect(() => { + if (!normalizedKey) { + return undefined; + } + const ids = normalizedKey.split('|'); + let cancelled = false; + ids.forEach((roverId) => { + socket.emit('video:request', { roverId }, (resp = {}) => { + if (cancelled) return; + setSources((prev) => ({ + ...prev, + [roverId]: resp, + })); + }); + }); + + return () => { + cancelled = true; + }; + }, [socket, normalizedKey]); + + const filtered = useMemo(() => { + if (!normalizedKey) return {}; + const ids = normalizedKey.split('|'); + const next = {}; + ids.forEach((id) => { + if (sources[id]) { + next[id] = sources[id]; + } + }); + return next; + }, [normalizedKey, sources]); + + return filtered; +} diff --git a/webui/src/index.css b/webui/src/index.css new file mode 100644 index 00000000..39ef9bfd --- /dev/null +++ b/webui/src/index.css @@ -0,0 +1,16 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + sans-serif; + color: #0f172a; + background-color: #f8fafc; +} + +body { + margin: 0; + min-height: 100vh; + background-color: #0f172a; +} diff --git a/webui/src/lib/socket.js b/webui/src/lib/socket.js new file mode 100644 index 00000000..e12e63d1 --- /dev/null +++ b/webui/src/lib/socket.js @@ -0,0 +1,10 @@ +import { io } from 'socket.io-client'; + +const configured = import.meta.env.VITE_ROVERD_URL?.trim(); +const resolvedUrl = configured && configured.length > 0 ? configured : window.location.origin; +console.info('[socket] connecting to', resolvedUrl); +export const socket = io(resolvedUrl, { + transports: ['polling', 'websocket'], + timeout: 15000, +}); +socket.on('connect_error', (err) => console.error('connect_error', err.code, err.message, err.data)); diff --git a/webui/src/lib/whepPlayer.js b/webui/src/lib/whepPlayer.js new file mode 100644 index 00000000..69121798 --- /dev/null +++ b/webui/src/lib/whepPlayer.js @@ -0,0 +1,139 @@ +/* global Buffer */ + +const RTC_CONFIG = { + iceServers: [ + { urls: 'stun:stun.l.google.com:19302' }, + { urls: 'turn:your.turn.server:3478', username: 'user', credential: 'pass' }, + ], + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', +}; + + +function encodeBase64(value) { + if (typeof btoa === 'function') { + return btoa(value); + } + if (typeof Buffer !== 'undefined') { + return Buffer.from(value).toString('base64'); + } + throw new Error('No base64 encoder available'); +} + +function buildAuthHeader(token) { + if (!token) return {}; + const credential = `${token}:${token}`; + const encoded = encodeBase64(credential); + return { Authorization: `Basic ${encoded}` }; +} + +export class WhepPlayer { + constructor({ url, token, video, onStatus }) { + this.url = url; + this.token = token; + this.video = video; + this.pc = null; + this.abortController = null; + this.onStatus = onStatus; + } + + notify(status, detail) { + if (typeof this.onStatus === 'function') { + this.onStatus(status, detail); + } + } + + configureVideoElement() { + if (!this.video) return; + this.video.playsInline = true; + this.video.autoplay = true; + this.video.disableRemotePlayback = true; + if ('latencyHint' in HTMLMediaElement.prototype) { + this.video.latencyHint = 'interactive'; + } + } + + async start() { + if (!this.url || !this.video) { + throw new Error('Video target missing'); + } + this.stop(); + this.notify('connecting'); + this.configureVideoElement(); + this.abortController = new AbortController(); + const pc = new RTCPeerConnection(RTC_CONFIG); + this.pc = pc; + const stream = new MediaStream(); + pc.ontrack = (event) => { + event.streams[0]?.getTracks().forEach((mediaTrack) => stream.addTrack(mediaTrack)); + this.video.srcObject = stream; + if (event.track?.kind === 'video' && 'playoutDelayHint' in event.receiver) { + event.receiver.playoutDelayHint = 0; + } + }; + pc.addTransceiver('video', { direction: 'recvonly' }); + pc.addTransceiver('audio', { direction: 'recvonly' }); + + pc.onconnectionstatechange = () => { + const state = pc.connectionState; + this.notify(state); + }; + pc.oniceconnectionstatechange = () => { + const state = pc.iceConnectionState; + if (state === 'failed' || state === 'disconnected') { + this.notify(state); + } + }; + + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + await pc.setLocalDescription(offer); + + const response = await fetch(this.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/sdp', + ...buildAuthHeader(this.token), + }, + body: offer.sdp, + signal: this.abortController.signal, + }); + if (!response.ok) { + throw new Error(`WHEP request failed: ${response.status}`); + } + const answerSdp = await response.text(); + await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp }); + pc.getReceivers().forEach((receiver) => { + if ('playoutDelayHint' in receiver) { + receiver.playoutDelayHint = 0; + } + }); + await this.video.play().catch(() => {}); + this.notify('playing'); + } catch (err) { + this.notify('error', err.message); + this.stop(); + throw err; + } + } + + stop() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + if (this.pc) { + this.pc.getSenders().forEach((sender) => sender.track?.stop()); + this.pc.getReceivers().forEach((receiver) => receiver.track?.stop()); + this.pc.close(); + this.pc = null; + } + if (this.video) { + this.video.srcObject = null; + } + this.notify('stopped'); + } +} diff --git a/webui/src/main.jsx b/webui/src/main.jsx new file mode 100644 index 00000000..16163c0f --- /dev/null +++ b/webui/src/main.jsx @@ -0,0 +1,26 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import './index.css' +import App from './App.jsx' +import { SocketProvider } from './context/SocketContext.jsx' +import { SessionProvider } from './context/SessionContext.jsx' +import { TelemetryProvider } from './context/TelemetryContext.jsx' +import SpectatorApp from './spectate/SpectatorApp.jsx' + +createRoot(document.getElementById('root')).render( + + + + + + + } /> + } /> + + + + + + , +) diff --git a/webui/src/spectate/SpectatorApp.jsx b/webui/src/spectate/SpectatorApp.jsx new file mode 100644 index 00000000..d79a579c --- /dev/null +++ b/webui/src/spectate/SpectatorApp.jsx @@ -0,0 +1,127 @@ +import { useMemo } from 'react'; +import { useSession } from '../context/SessionContext.jsx'; +import { useSpectatorMode } from '../hooks/useSpectatorMode.js'; +import { useTelemetryFrames } from '../context/TelemetryContext.jsx'; +import { useVideoRequests } from '../hooks/useVideoRequests.js'; +import VideoTile from '../components/VideoTile.jsx'; + +function SpectatorStatus({ connected, ready, rosterCount }) { + return ( +
+ + {connected ? 'Connected' : 'Disconnected'} + + + Spectator mode {ready ? 'active' : 'pending…'} + + + Rovers: {rosterCount} + +
+ ); +} + +function LogStream({ logs }) { + return ( +
+
+

Server logs

+ latest {logs.length} +
+
+ {logs.length === 0 ? ( +

No log entries yet.

+ ) : ( + logs + .slice() + .reverse() + .map((entry) => ( +
+ {entry.timestamp}{' '} + [{entry.level}]{' '} + {entry.label && [{entry.label}]}{' '} + {entry.message} +
+ )) + )} +
+
+ ); +} + +function formatSensors(sensors = {}) { + return Object.entries(sensors).map(([key, value]) => ({ + key, + value: typeof value === 'object' ? JSON.stringify(value) : value, + })); +} + +function RoverSpectatorCard({ rover, frame, videoInfo }) { + const sensorList = formatSensors(frame?.sensors); + return ( +
+
+

{rover.name}

+

+ {frame?.receivedAt ? `Updated ${new Date(frame.receivedAt).toLocaleTimeString()}` : 'Waiting for telemetry…'} +

+
+ +
+

Telemetry

+ {sensorList.length === 0 ? ( +

No sensor data yet.

+ ) : ( +
+ {sensorList.map(({ key, value }) => ( +
+
{key}
+
{value}
+
+ ))} +
+ )} +
+
+ ); +} + +export default function SpectatorApp() { + const { connected, session, logs } = useSession(); + const spectatorReady = useSpectatorMode(); + const frames = useTelemetryFrames(); + const roster = session?.roster ?? []; + const videoSources = useVideoRequests(roster.map((rover) => rover.id)); + + const status = useMemo( + () => ({ connected, ready: spectatorReady, rosterCount: roster.length }), + [connected, spectatorReady, roster.length], + ); + + return ( +
+
+
+

Spectator Console

+

Live rover telemetry & logs. This view is read-only.

+ +
+
+ {roster.length === 0 ? ( +

No rovers registered.

+ ) : ( + roster.map((rover) => ( + + )) + )} +
+ +
+
+ ); +} diff --git a/webui/tailwind.config.js b/webui/tailwind.config.js new file mode 100644 index 00000000..dca8ba02 --- /dev/null +++ b/webui/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/webui/vite.config.js b/webui/vite.config.js new file mode 100644 index 00000000..16e25075 --- /dev/null +++ b/webui/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../server/public', + emptyOutDir: true, + }, +})