From caf3922ecd0b0b03199eabb83fca5b587c670ad6 Mon Sep 17 00:00:00 2001
From: Adam Shiervani <adam.shiervani@gmail.com>
Date: Mon, 24 Mar 2025 12:07:31 +0100
Subject: [PATCH 01/17] refactor(WebRTCVideo): improve mouse event handling and
 video playback logic (#282)

---
 ui/src/components/WebRTCVideo.tsx | 223 ++++++++++++++++++------------
 ui/src/routes/devices.$id.tsx     |   4 +
 2 files changed, 136 insertions(+), 91 deletions(-)

diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx
index 29c72d1..de36e37 100644
--- a/ui/src/components/WebRTCVideo.tsx
+++ b/ui/src/components/WebRTCVideo.tsx
@@ -94,7 +94,7 @@ export default function WebRTCVideo() {
   );
 
   // Mouse-related
-  const calcDelta = (pos: number) => Math.abs(pos) < 10 ? pos * 2 : pos;
+  const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
   const sendRelMouseMovement = useCallback(
     (x: number, y: number, buttons: number) => {
       if (settings.mouseMode !== "relative") return;
@@ -168,7 +168,14 @@ export default function WebRTCVideo() {
       const { buttons } = e;
       sendAbsMouseMovement(x, y, buttons);
     },
-    [sendAbsMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight, settings.mouseMode],
+    [
+      sendAbsMouseMovement,
+      videoClientHeight,
+      videoClientWidth,
+      videoWidth,
+      videoHeight,
+      settings.mouseMode,
+    ],
   );
 
   const trackpadSensitivity = useDeviceSettingsStore(state => state.trackpadSensitivity);
@@ -355,28 +362,6 @@ export default function WebRTCVideo() {
     ],
   );
 
-  // Effect hooks
-  useEffect(
-    function setupKeyboardEvents() {
-      const abortController = new AbortController();
-      const signal = abortController.signal;
-
-      document.addEventListener("keydown", keyDownHandler, { signal });
-      document.addEventListener("keyup", keyUpHandler, { signal });
-
-      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-      // @ts-expect-error
-      window.clearKeys = () => sendKeyboardEvent([], []);
-      window.addEventListener("blur", resetKeyboardState, { signal });
-      document.addEventListener("visibilitychange", resetKeyboardState, { signal });
-
-      return () => {
-        abortController.abort();
-      };
-    },
-    [keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
-  );
-
   const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
     // In fullscreen mode in chrome & safari, the space key is used to pause/play the video
     // there is no way to prevent this, so we need to simply force play the video when it's paused.
@@ -389,71 +374,6 @@ export default function WebRTCVideo() {
     }
   }, []);
 
-  useEffect(
-    function setupVideoEventListeners() {
-      let videoElmRefValue = null;
-      if (!videoElm.current) return;
-      videoElmRefValue = videoElm.current;
-      const abortController = new AbortController();
-      const signal = abortController.signal;
-
-      videoElmRefValue.addEventListener("mousemove", absMouseMoveHandler, { signal });
-      videoElmRefValue.addEventListener("pointerdown", absMouseMoveHandler, { signal });
-      videoElmRefValue.addEventListener("pointerup", absMouseMoveHandler, { signal });
-      videoElmRefValue.addEventListener("keyup", videoKeyUpHandler, { signal });
-      videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
-        signal,
-        passive: true,
-      });
-      videoElmRefValue.addEventListener(
-        "contextmenu",
-        (e: MouseEvent) => e.preventDefault(),
-        { signal },
-      );
-      videoElmRefValue.addEventListener("playing", onVideoPlaying, { signal });
-
-      const local = resetMousePosition;
-      window.addEventListener("blur", local, { signal });
-      document.addEventListener("visibilitychange", local, { signal });
-
-      return () => {
-        if (videoElmRefValue) abortController.abort();
-      };
-    },
-    [
-      absMouseMoveHandler,
-      resetMousePosition,
-      onVideoPlaying,
-      mouseWheelHandler,
-      videoKeyUpHandler,
-    ],
-  );
-
-  useEffect(
-    function setupRelativeMouseEventListeners() {
-      if (settings.mouseMode !== "relative") return;
-
-      const abortController = new AbortController();
-      const signal = abortController.signal;
-
-      // bind to body to capture all mouse events
-      const body = document.querySelector("body");
-      if (!body) return;
-
-      body.addEventListener("mousemove", relMouseMoveHandler, { signal });
-      body.addEventListener("pointerdown", relMouseMoveHandler, { signal });
-      body.addEventListener("pointerup", relMouseMoveHandler, { signal });
-
-      return () => {
-        abortController.abort();
-
-        body.removeEventListener("mousemove", relMouseMoveHandler);
-        body.removeEventListener("pointerdown", relMouseMoveHandler);
-        body.removeEventListener("pointerup", relMouseMoveHandler);
-      };
-    }, [settings.mouseMode, relMouseMoveHandler],
-  )
-
   useEffect(
     function updateVideoStream() {
       if (!mediaStream) return;
@@ -476,6 +396,120 @@ export default function WebRTCVideo() {
     ],
   );
 
+  // Setup Keyboard Events
+  useEffect(
+    function setupKeyboardEvents() {
+      const abortController = new AbortController();
+      const signal = abortController.signal;
+
+      document.addEventListener("keydown", keyDownHandler, { signal });
+      document.addEventListener("keyup", keyUpHandler, { signal });
+
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-expect-error
+      window.clearKeys = () => sendKeyboardEvent([], []);
+      window.addEventListener("blur", resetKeyboardState, { signal });
+      document.addEventListener("visibilitychange", resetKeyboardState, { signal });
+
+      return () => {
+        abortController.abort();
+      };
+    },
+    [keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
+  );
+
+  useEffect(
+    function setupVideoEventListeners() {
+      const videoElmRefValue = videoElm.current;
+      if (!videoElmRefValue) return;
+
+      const abortController = new AbortController();
+      const signal = abortController.signal;
+
+      // To prevent the video from being paused when the user presses a space in fullscreen mode
+      videoElmRefValue.addEventListener("keyup", videoKeyUpHandler, { signal });
+
+      // We need to know when the video is playing to update state and video size
+      videoElmRefValue.addEventListener("playing", onVideoPlaying, { signal });
+
+      return () => {
+        abortController.abort();
+      };
+    },
+    [
+      absMouseMoveHandler,
+      resetMousePosition,
+      onVideoPlaying,
+      mouseWheelHandler,
+      videoKeyUpHandler,
+    ],
+  );
+
+  // Setup Absolute Mouse Events
+  useEffect(
+    function setAbsoluteMouseModeEventListeners() {
+      const videoElmRefValue = videoElm.current;
+      if (!videoElmRefValue) return;
+
+      if (settings.mouseMode !== "absolute") return;
+
+      const abortController = new AbortController();
+      const signal = abortController.signal;
+
+      videoElmRefValue.addEventListener("mousemove", absMouseMoveHandler, { signal });
+      videoElmRefValue.addEventListener("pointerdown", absMouseMoveHandler, { signal });
+      videoElmRefValue.addEventListener("pointerup", absMouseMoveHandler, { signal });
+      videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
+        signal,
+        passive: true,
+      });
+
+      // Reset the mouse position when the window is blurred or the document is hidden
+      const local = resetMousePosition;
+      window.addEventListener("blur", local, { signal });
+      document.addEventListener("visibilitychange", local, { signal });
+
+      return () => {
+        abortController.abort();
+      };
+    },
+    [absMouseMoveHandler, mouseWheelHandler, resetMousePosition, settings.mouseMode],
+  );
+
+  // Setup Relative Mouse Events
+  const containerRef = useRef<HTMLDivElement>(null);
+  useEffect(
+    function setupRelativeMouseEventListeners() {
+      if (settings.mouseMode !== "relative") return;
+
+      const abortController = new AbortController();
+      const signal = abortController.signal;
+
+      // We bind to the larger container in relative mode because of delta between the acceleration of the local
+      // mouse and the mouse movement of the remote mouse. This simply makes it a bit less painful to use.
+      // When we get Pointer Lock support, we can remove this.
+      const containerElm = containerRef.current;
+      if (!containerElm) return;
+
+      containerElm.addEventListener("mousemove", relMouseMoveHandler, { signal });
+      containerElm.addEventListener("pointerdown", relMouseMoveHandler, { signal });
+      containerElm.addEventListener("pointerup", relMouseMoveHandler, { signal });
+
+      containerElm.addEventListener("wheel", mouseWheelHandler, {
+        signal,
+        passive: true,
+      });
+
+      const preventContextMenu = (e: MouseEvent) => e.preventDefault();
+      containerElm.addEventListener("contextmenu", preventContextMenu, { signal });
+
+      return () => {
+        abortController.abort();
+      };
+    },
+    [settings.mouseMode, relMouseMoveHandler, mouseWheelHandler],
+  );
+
   return (
     <div className="grid h-full w-full grid-rows-layout">
       <div className="min-h-[39.5px]">
@@ -490,7 +524,12 @@ export default function WebRTCVideo() {
         </fieldset>
       </div>
 
-      <div className="h-full overflow-hidden">
+      <div
+        ref={containerRef}
+        className={cx("h-full overflow-hidden", {
+          "cursor-none": settings.mouseMode === "relative" && settings.isCursorHidden,
+        })}
+      >
         <div className="relative h-full">
           <div
             className={cx(
@@ -519,7 +558,9 @@ export default function WebRTCVideo() {
                         className={cx(
                           "outline-50 max-h-full max-w-full object-contain transition-all duration-1000",
                           {
-                            "cursor-none": settings.isCursorHidden,
+                            "cursor-none":
+                              settings.mouseMode === "absolute" &&
+                              settings.isCursorHidden,
                             "opacity-0": isLoading || isConnectionError || hdmiError,
                             "animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
                               isPlaying,
diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx
index 24a6428..d25b848 100644
--- a/ui/src/routes/devices.$id.tsx
+++ b/ui/src/routes/devices.$id.tsx
@@ -516,6 +516,10 @@ export default function KvmIdRoute() {
 
       <div
         className="isolate"
+        // onMouseMove={e => e.stopPropagation()}
+        // onMouseDown={e => e.stopPropagation()}
+        // onMouseUp={e => e.stopPropagation()}
+        // onPointerMove={e => e.stopPropagation()}
         onKeyUp={e => e.stopPropagation()}
         onKeyDown={e => {
           e.stopPropagation();

From 204e6c7fafab1f5519be3f94943a04f2434bffd8 Mon Sep 17 00:00:00 2001
From: Adam Shiervani <adam.shiervani@gmail.com>
Date: Mon, 24 Mar 2025 12:32:12 +0100
Subject: [PATCH 02/17] feat(UsbDeviceSetting): integrate remote virtual media
 state management and improve USB config handlingt

---
 ui/src/components/UsbDeviceSetting.tsx | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/ui/src/components/UsbDeviceSetting.tsx b/ui/src/components/UsbDeviceSetting.tsx
index 605ae4d..07125e6 100644
--- a/ui/src/components/UsbDeviceSetting.tsx
+++ b/ui/src/components/UsbDeviceSetting.tsx
@@ -9,7 +9,6 @@ import { Button } from "./Button";
 import { SelectMenuBasic } from "./SelectMenuBasic";
 import { SettingsSectionHeader } from "./SettingsSectionHeader";
 import Fieldset from "./Fieldset";
-
 export interface USBConfig {
   vendor_id: string;
   product_id: string;
@@ -119,13 +118,12 @@ export function UsbDeviceSetting() {
 
   const onUsbConfigItemChange = useCallback(
     (key: keyof UsbDeviceConfig) => (e: React.ChangeEvent<HTMLInputElement>) => {
-      setUsbDeviceConfig(val => {
-        val[key] = e.target.checked;
-        handleUsbConfigChange(val);
-        return val;
-      });
+      setUsbDeviceConfig(prev => ({
+        ...prev,
+        [key]: e.target.checked,
+      }));
     },
-    [handleUsbConfigChange],
+    [],
   );
 
   const handlePresetChange = useCallback(

From ab03aded74f63e0dfd37ef7f2d50cb47a472d472 Mon Sep 17 00:00:00 2001
From: Siyuan Miao <i@xswan.net>
Date: Mon, 24 Mar 2025 23:10:06 +0100
Subject: [PATCH 03/17] chore: create empty resource directory to avoid static
 type check fail

---
 .github/workflows/golangci-lint.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 8c828cf..7ec9229 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -27,6 +27,9 @@ jobs:
         uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
         with:
           go-version: 1.23.x
+      - name: Create empty resource directory
+        run: |
+          mkdir -p static && touch static/.gitkeep
       - name: Lint
         uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
         with:

From 1b8954e9f32935f1cb8fe8ac4f7c8e2458bf30c4 Mon Sep 17 00:00:00 2001
From: Siyuan Miao <i@xswan.net>
Date: Mon, 24 Mar 2025 23:20:08 +0100
Subject: [PATCH 04/17] chore: fix linting issues of web_tls.go

---
 web_tls.go | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/web_tls.go b/web_tls.go
index fff9253..b1bb60e 100644
--- a/web_tls.go
+++ b/web_tls.go
@@ -38,7 +38,7 @@ func RunWebSecureServer() {
 		TLSConfig: &tls.Config{
 			// TODO: cache certificate in persistent storage
 			GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
-				hostname := WebSecureSelfSignedDefaultDomain
+				var hostname string
 				if info.ServerName != "" {
 					hostname = info.ServerName
 				} else {
@@ -58,7 +58,6 @@ func RunWebSecureServer() {
 	if err != nil {
 		panic(err)
 	}
-	return
 }
 
 func createSelfSignedCert(hostname string) *tls.Certificate {

From 5d7d4db4aa609d19bc18cae437eb1b7108666b85 Mon Sep 17 00:00:00 2001
From: Adam Shiervani <adam.shiervani@gmail.com>
Date: Mon, 24 Mar 2025 23:31:23 +0100
Subject: [PATCH 05/17] Improve connection error handling (#284)

* feat(WebRTC): enhance connection management with connection failures after X attempts or a certain time

* refactor(WebRTC): simplify WebRTCVideo component and enhance connection error handling

* fix(WebRTC): extend connection timeout from 1 second to 60 seconds for improved error handling
---
 ui/src/components/WebRTCVideo.tsx |   2 +-
 ui/src/routes/devices.$id.tsx     | 110 ++++++++++++++++++++++++++----
 2 files changed, 98 insertions(+), 14 deletions(-)

diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx
index de36e37..505ce73 100644
--- a/ui/src/components/WebRTCVideo.tsx
+++ b/ui/src/components/WebRTCVideo.tsx
@@ -47,7 +47,7 @@ export default function WebRTCVideo() {
   const hdmiState = useVideoStore(state => state.hdmiState);
   const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
   const isLoading = !hdmiError && !isPlaying;
-  const isConnectionError = ["error", "failed", "disconnected"].includes(
+  const isConnectionError = ["error", "failed", "disconnected", "closed"].includes(
     peerConnectionState || "",
   );
 
diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx
index d25b848..b454f73 100644
--- a/ui/src/routes/devices.$id.tsx
+++ b/ui/src/routes/devices.$id.tsx
@@ -130,10 +130,68 @@ export default function KvmIdRoute() {
   const setDiskChannel = useRTCStore(state => state.setDiskChannel);
   const setRpcDataChannel = useRTCStore(state => state.setRpcDataChannel);
   const setTransceiver = useRTCStore(state => state.setTransceiver);
+  const location = useLocation();
+
+  const [connectionAttempts, setConnectionAttempts] = useState(0);
+
+  const [startedConnectingAt, setStartedConnectingAt] = useState<Date | null>(null);
+  const [connectedAt, setConnectedAt] = useState<Date | null>(null);
+
+  const [connectionFailed, setConnectionFailed] = useState(false);
 
   const navigate = useNavigate();
   const { otaState, setOtaState, setModalView } = useUpdateStore();
 
+  const closePeerConnection = useCallback(
+    function closePeerConnection() {
+      peerConnection?.close();
+      // "closed" is a valid RTCPeerConnection state according to the WebRTC spec
+      // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState#closed
+      // However, the onconnectionstatechange event doesn't fire when close() is called manually
+      // So we need to explicitly update our state to maintain consistency
+      // I don't know why this is happening, but this is the best way I can think of to handle it
+      setPeerConnectionState("closed");
+    },
+    [peerConnection, setPeerConnectionState],
+  );
+
+  useEffect(() => {
+    const connectionAttemptsThreshold = 30;
+    if (connectionAttempts > connectionAttemptsThreshold) {
+      console.log(`Connection failed after ${connectionAttempts} attempts.`);
+      setConnectionFailed(true);
+      closePeerConnection();
+    }
+  }, [connectionAttempts, closePeerConnection]);
+
+  useEffect(() => {
+    // Skip if already connected
+    if (connectedAt) return;
+
+    // Skip if connection is declared as failed
+    if (connectionFailed) return;
+
+    const interval = setInterval(() => {
+      console.log("Checking connection status");
+
+      // Skip if connection hasn't started
+      if (!startedConnectingAt) return;
+
+      const elapsedTime = Math.floor(
+        new Date().getTime() - startedConnectingAt.getTime(),
+      );
+
+      // Fail connection if it's been over X seconds since we started connecting
+      if (elapsedTime > 60 * 1000) {
+        console.error(`Connection failed after ${elapsedTime} ms.`);
+        setConnectionFailed(true);
+        closePeerConnection();
+      }
+    }, 1000);
+
+    return () => clearInterval(interval);
+  }, [closePeerConnection, connectedAt, connectionFailed, startedConnectingAt]);
+
   const sdp = useCallback(
     async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => {
       if (!pc) return;
@@ -169,7 +227,7 @@ export default function KvmIdRoute() {
           // - In device mode, the device api would timeout, the fetch would throw an error, therefore the catch block would be hit
           // Regardless, we should close the peer connection and let the useInterval handle reconnecting
           if (!res.ok) {
-            pc?.close();
+            closePeerConnection();
             console.error(`Error setting SDP - Status: ${res.status}}`, json);
             return;
           }
@@ -180,14 +238,20 @@ export default function KvmIdRoute() {
         ).catch(e => console.log(`Error setting remote description: ${e}`));
       } catch (error) {
         console.error(`Error setting SDP: ${error}`);
-        pc?.close();
+        closePeerConnection();
       }
     },
-    [navigate, params.id],
+    [closePeerConnection, navigate, params.id],
   );
 
   const connectWebRTC = useCallback(async () => {
     console.log("Attempting to connect WebRTC");
+
+    // Track connection status to detect failures and show error overlay
+    setConnectionAttempts(x => x + 1);
+    setStartedConnectingAt(new Date());
+    setConnectedAt(null);
+
     const pc = new RTCPeerConnection({
       // We only use STUN or TURN servers if we're in the cloud
       ...(isInCloud && iceConfig?.iceServers
@@ -197,6 +261,11 @@ export default function KvmIdRoute() {
 
     // Set up event listeners and data channels
     pc.onconnectionstatechange = () => {
+      // If the connection state is connected, we reset the connection attempts.
+      if (pc.connectionState === "connected") {
+        setConnectionAttempts(0);
+        setConnectedAt(new Date());
+      }
       setPeerConnectionState(pc.connectionState);
     };
 
@@ -236,16 +305,35 @@ export default function KvmIdRoute() {
     setTransceiver,
   ]);
 
-  // WebRTC connection management
-  useInterval(() => {
+  useEffect(() => {
+    console.log("Attempting to connect WebRTC");
+
+    // If we're in an other session, we don't need to connect
+    if (location.pathname.includes("other-session")) return;
+
+    // If we're already connected or connecting, we don't need to connect
     if (
       ["connected", "connecting", "new"].includes(peerConnection?.connectionState ?? "")
     ) {
       return;
     }
-    if (location.pathname.includes("other-session")) return;
-    connectWebRTC();
-  }, 3000);
+
+    // In certain cases, we want to never connect again. This happens when we've tried for a long time and failed
+    if (connectionFailed) {
+      console.log("Connection failed. We won't attempt to connect again.");
+      return;
+    }
+
+    const interval = setInterval(() => {
+      connectWebRTC();
+    }, 3000);
+    return () => clearInterval(interval);
+  }, [
+    connectWebRTC,
+    connectionFailed,
+    location.pathname,
+    peerConnection?.connectionState,
+  ]);
 
   // On boot, if the connection state is undefined, we connect to the WebRTC
   useEffect(() => {
@@ -431,7 +519,6 @@ export default function KvmIdRoute() {
   }, [kvmTerminal, peerConnection, serialConsole]);
 
   const outlet = useOutlet();
-  const location = useLocation();
   const onModalClose = useCallback(() => {
     if (location.pathname !== "/other-session") navigateTo("/");
   }, [navigateTo, location.pathname]);
@@ -516,10 +603,6 @@ export default function KvmIdRoute() {
 
       <div
         className="isolate"
-        // onMouseMove={e => e.stopPropagation()}
-        // onMouseDown={e => e.stopPropagation()}
-        // onMouseUp={e => e.stopPropagation()}
-        // onPointerMove={e => e.stopPropagation()}
         onKeyUp={e => e.stopPropagation()}
         onKeyDown={e => {
           e.stopPropagation();
@@ -527,6 +610,7 @@ export default function KvmIdRoute() {
         }}
       >
         <Modal open={outlet !== null} onClose={onModalClose}>
+          {/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
           <Outlet context={{ connectWebRTC }} />
         </Modal>
       </div>

From 9d511d7f581a9ddefcd1f5bfc6ee1e75a0b71caf Mon Sep 17 00:00:00 2001
From: Adam Shiervani <adam.shiervani@gmail.com>
Date: Mon, 24 Mar 2025 23:32:13 +0100
Subject: [PATCH 06/17] Autoplay permission handling (#285)

* feat(WebRTC): enhance connection management with connection failures after X attempts or a certain time

* refactor(WebRTC): simplify WebRTCVideo component and enhance connection error handling

* fix(WebRTC): extend connection timeout from 1 second to 60 seconds for improved error handling

* feat(VideoOverlay): add NoAutoplayPermissionsOverlay component and improve HDMIErrorOverlay content

* feat(VideoOverlay): update NoAutoplayPermissionsOverlay styling and improve user instructions

* Remove unused PlayIcon import to clean up code
---
 ui/src/components/VideoOverlay.tsx | 74 ++++++++++++++++++++++++++----
 ui/src/components/WebRTCVideo.tsx  | 19 +++++++-
 2 files changed, 81 insertions(+), 12 deletions(-)

diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx
index 97d097b..a8560cb 100644
--- a/ui/src/components/VideoOverlay.tsx
+++ b/ui/src/components/VideoOverlay.tsx
@@ -1,10 +1,11 @@
 import React from "react";
 import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
 import { ArrowRightIcon } from "@heroicons/react/16/solid";
-import { LinkButton } from "@components/Button";
+import { Button, LinkButton } from "@components/Button";
 import LoadingSpinner from "@components/LoadingSpinner";
 import { GridCard } from "@components/Card";
 import { motion, AnimatePresence } from "motion/react";
+import { LuPlay } from "react-icons/lu";
 
 interface OverlayContentProps {
   children: React.ReactNode;
@@ -34,7 +35,7 @@ export function LoadingOverlay({ show }: LoadingOverlayProps) {
           exit={{ opacity: 0 }}
           transition={{
             duration: show ? 0.3 : 0.1,
-            ease: "easeInOut"
+            ease: "easeInOut",
           }}
         >
           <OverlayContent>
@@ -68,7 +69,7 @@ export function ConnectionErrorOverlay({ show }: ConnectionErrorOverlayProps) {
           exit={{ opacity: 0 }}
           transition={{
             duration: 0.3,
-            ease: "easeInOut"
+            ease: "easeInOut",
           }}
         >
           <OverlayContent>
@@ -118,25 +119,27 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
       <AnimatePresence>
         {show && isNoSignal && (
           <motion.div
-            className="absolute inset-0 w-full h-full aspect-video"
+            className="absolute inset-0 aspect-video h-full w-full"
             initial={{ opacity: 0 }}
             animate={{ opacity: 1 }}
             exit={{ opacity: 0 }}
             transition={{
               duration: 0.3,
-              ease: "easeInOut"
+              ease: "easeInOut",
             }}
           >
             <OverlayContent>
               <div className="flex flex-col items-start gap-y-1">
-                <ExclamationTriangleIcon className="w-12 h-12 text-yellow-500" />
-                <div className="text-sm text-left text-slate-700 dark:text-slate-300">
+                <ExclamationTriangleIcon className="h-12 w-12 text-yellow-500" />
+                <div className="text-left text-sm text-slate-700 dark:text-slate-300">
                   <div className="space-y-4">
                     <div className="space-y-2 text-black dark:text-white">
                       <h2 className="text-xl font-bold">No HDMI signal detected.</h2>
                       <ul className="list-disc space-y-2 pl-4 text-left">
                         <li>Ensure the HDMI cable securely connected at both ends</li>
-                        <li>Ensure source device is powered on and outputting a signal</li>
+                        <li>
+                          Ensure source device is powered on and outputting a signal
+                        </li>
                         <li>
                           If using an adapter, it&apos;s compatible and functioning
                           correctly
@@ -169,7 +172,7 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
             exit={{ opacity: 0 }}
             transition={{
               duration: 0.3,
-              ease: "easeInOut"
+              ease: "easeInOut",
             }}
           >
             <OverlayContent>
@@ -187,7 +190,7 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
                     </div>
                     <div>
                       <LinkButton
-                        to={"/help/hdmi-error"}
+                        to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
                         theme="light"
                         text="Learn more"
                         TrailingIcon={ArrowRightIcon}
@@ -204,3 +207,54 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
     </>
   );
 }
+
+interface NoAutoplayPermissionsOverlayProps {
+  show: boolean;
+  onPlayClick: () => void;
+}
+
+export function NoAutoplayPermissionsOverlay({
+  show,
+  onPlayClick,
+}: NoAutoplayPermissionsOverlayProps) {
+  return (
+    <AnimatePresence>
+      {show && (
+        <motion.div
+          className="absolute inset-0 z-10 aspect-video h-full w-full"
+          initial={{ opacity: 0 }}
+          animate={{ opacity: 1 }}
+          exit={{ opacity: 0 }}
+          transition={{
+            duration: 0.3,
+            ease: "easeInOut",
+          }}
+        >
+          <OverlayContent>
+            <div className="space-y-4">
+              <h2 className="text-2xl font-extrabold text-black dark:text-white">
+                Autoplay permissions required
+              </h2>
+
+              <div className="space-y-2 text-center">
+                <div>
+                  <Button
+                    size="MD"
+                    theme="primary"
+                    LeadingIcon={LuPlay}
+                    text="Manually start stream"
+                    onClick={onPlayClick}
+                  />
+                </div>
+
+                <div className="text-xs text-slate-600 dark:text-slate-400">
+                  Please adjust browser settings to enable autoplay
+                </div>
+              </div>
+            </div>
+          </OverlayContent>
+        </motion.div>
+      )}
+    </AnimatePresence>
+  );
+}
diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx
index 505ce73..4cd56f6 100644
--- a/ui/src/components/WebRTCVideo.tsx
+++ b/ui/src/components/WebRTCVideo.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import {
   useDeviceSettingsStore,
   useHidStore,
@@ -15,7 +15,7 @@ import Actionbar from "@components/ActionBar";
 import InfoBar from "@components/InfoBar";
 import useKeyboard from "@/hooks/useKeyboard";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
-import { HDMIErrorOverlay } from "./VideoOverlay";
+import { HDMIErrorOverlay, NoAutoplayPermissionsOverlay } from "./VideoOverlay";
 import { ConnectionErrorOverlay } from "./VideoOverlay";
 import { LoadingOverlay } from "./VideoOverlay";
 
@@ -418,6 +418,7 @@ export default function WebRTCVideo() {
     [keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
   );
 
+  // Setup Video Event Listeners
   useEffect(
     function setupVideoEventListeners() {
       const videoElmRefValue = videoElm.current;
@@ -510,6 +511,14 @@ export default function WebRTCVideo() {
     [settings.mouseMode, relMouseMoveHandler, mouseWheelHandler],
   );
 
+  const hasNoAutoPlayPermissions = useMemo(() => {
+    if (peerConnectionState !== "connected") return false;
+    if (isPlaying) return false;
+    if (hdmiError) return false;
+    if (videoHeight === 0 || videoWidth === 0) return false;
+    return true;
+  }, [peerConnectionState, isPlaying, hdmiError, videoHeight, videoWidth]);
+
   return (
     <div className="grid h-full w-full grid-rows-layout">
       <div className="min-h-[39.5px]">
@@ -575,6 +584,12 @@ export default function WebRTCVideo() {
                           <LoadingOverlay show={isLoading} />
                           <ConnectionErrorOverlay show={isConnectionError} />
                           <HDMIErrorOverlay show={hdmiError} hdmiState={hdmiState} />
+                          <NoAutoplayPermissionsOverlay
+                            show={hasNoAutoPlayPermissions}
+                            onPlayClick={() => {
+                              videoElm.current?.play();
+                            }}
+                          />
                         </div>
                       </div>
                     </div>

From 3b711db7819f81e6dbdc39fdb0d338c065a0ab42 Mon Sep 17 00:00:00 2001
From: Adam Shiervani <adam.shiervani@gmail.com>
Date: Tue, 25 Mar 2025 11:56:24 +0100
Subject: [PATCH 07/17] Apply and Upgrade Eslint (#288)

* Upgrade ESLINT and fix issues

* feat: add frontend linting job to GitHub Actions workflow

* Move UI linting to separate file

* More linting fixes

* Remove pull_request trigger from UI linting workflow

* Update UI linting workflow

* Rename frontend-lint workflow to ui-lint for clarity
---
 .github/workflows/build.yml                   |    6 +-
 .github/workflows/ui-lint.yml                 |   34 +
 ui/.eslintrc.cjs                              |   42 +
 ui/.prettierrc                                |   10 +-
 ui/package-lock.json                          | 1955 +++++++++--------
 ui/package.json                               |   48 +-
 ui/src/components/ActionBar.tsx               |   19 +-
 ui/src/components/AuthLayout.tsx              |   18 +-
 ui/src/components/Button.tsx                  |    7 +-
 ui/src/components/Card.tsx                    |    5 +-
 ui/src/components/CardHeader.tsx              |    4 +-
 ui/src/components/Checkbox.tsx                |    5 +-
 ui/src/components/Container.tsx               |    2 +
 ui/src/components/CustomTooltip.tsx           |    4 +-
 ui/src/components/EmptyCard.tsx               |   22 +-
 ui/src/components/ExtLink.tsx                 |    1 +
 ui/src/components/FeatureFlag.tsx             |    1 +
 ui/src/components/FieldLabel.tsx              |    5 +-
 ui/src/components/Fieldset.tsx                |    2 +-
 ui/src/components/Header.tsx                  |    7 +-
 ui/src/components/InfoBar.tsx                 |    3 +-
 ui/src/components/InputField.tsx              |    5 +-
 ui/src/components/KvmCard.tsx                 |    5 +-
 ui/src/components/Modal.tsx                   |    1 +
 ui/src/components/NotFoundPage.tsx            |    1 +
 .../components/PeerConnectionStatusCard.tsx   |    6 +-
 ui/src/components/SelectMenuBasic.tsx         |    7 +-
 ui/src/components/SimpleNavbar.tsx            |    5 +-
 ui/src/components/StatChart.tsx               |    1 +
 ui/src/components/StatusCards.tsx             |    1 +
 ui/src/components/StepCounter.tsx             |    5 +-
 ui/src/components/Terminal.tsx                |    8 +-
 ui/src/components/TextArea.tsx                |    3 +-
 ui/src/components/USBStateStatus.tsx          |   13 +-
 .../components/UpdateInProgressStatusCard.tsx |    4 +-
 ui/src/components/UsbDeviceSetting.tsx        |    4 +-
 ui/src/components/UsbInfoSetting.tsx          |    8 +-
 ui/src/components/VideoOverlay.tsx            |    5 +-
 ui/src/components/VirtualKeyboard.tsx         |   10 +-
 ui/src/components/WebRTCVideo.tsx             |   15 +-
 .../components/extensions/ATXPowerControl.tsx |   12 +-
 .../components/extensions/DCPowerControl.tsx  |   13 +-
 .../components/extensions/SerialConsole.tsx   |    7 +-
 .../components/popovers/ExtensionPopover.tsx  |   19 +-
 ui/src/components/popovers/MountPopover.tsx   |   17 +-
 ui/src/components/popovers/PasteModal.tsx     |   22 +-
 .../popovers/WakeOnLan/AddDeviceForm.tsx      |   12 +-
 .../popovers/WakeOnLan/DeviceList.tsx         |   24 +-
 .../popovers/WakeOnLan/EmptyStateCard.tsx     |   13 +-
 .../components/popovers/WakeOnLan/Index.tsx   |    8 +-
 ui/src/components/sidebar/connectionStats.tsx |   21 +-
 ui/src/hooks/useAppNavigation.ts              |    3 +-
 ui/src/hooks/useFeatureFlag.ts                |    3 +-
 ui/src/hooks/useJsonRpc.ts                    |    3 +-
 ui/src/hooks/useKeyboard.ts                   |    1 +
 ui/src/hooks/useResizeObserver.ts             |   13 +-
 ui/src/main.tsx                               |   24 +-
 ui/src/notifications.tsx                      |    3 +-
 ui/src/providers/FeatureFlagContext.tsx       |   10 +
 ui/src/providers/FeatureFlagProvider.tsx      |   11 +-
 ui/src/routes/adopt.tsx                       |    4 +-
 ui/src/routes/devices.$id.deregister.tsx      |    4 +-
 ui/src/routes/devices.$id.mount.tsx           |   38 +-
 ui/src/routes/devices.$id.other-session.tsx   |    1 +
 ui/src/routes/devices.$id.rename.tsx          |   13 +-
 ui/src/routes/devices.$id.settings._index.tsx |    1 +
 .../devices.$id.settings.access._index.tsx    |   30 +-
 ...devices.$id.settings.access.local-auth.tsx |    6 +-
 .../routes/devices.$id.settings.advanced.tsx  |   11 +-
 .../devices.$id.settings.appearance.tsx       |    2 +
 .../devices.$id.settings.general._index.tsx   |   10 +-
 .../devices.$id.settings.general.update.tsx   |    5 +-
 .../routes/devices.$id.settings.hardware.tsx  |    7 +-
 ui/src/routes/devices.$id.settings.mouse.tsx  |    7 +-
 ui/src/routes/devices.$id.settings.tsx        |    6 +-
 ui/src/routes/devices.$id.settings.video.tsx  |   10 +-
 ui/src/routes/devices.$id.setup.tsx           |   14 +-
 ui/src/routes/devices.$id.tsx                 |   41 +-
 ui/src/routes/devices.tsx                     |    8 +-
 ui/src/routes/login-local.tsx                 |   47 +-
 ui/src/routes/login.tsx                       |    3 +-
 ui/src/routes/signup.tsx                      |    3 +-
 ui/src/routes/welcome-local.mode.tsx          |   12 +-
 ui/src/routes/welcome-local.password.tsx      |   13 +-
 ui/src/routes/welcome-local.tsx               |   10 +-
 ui/src/utils.ts                               |    6 +-
 86 files changed, 1627 insertions(+), 1231 deletions(-)
 create mode 100644 .github/workflows/ui-lint.yml
 create mode 100644 ui/src/providers/FeatureFlagContext.tsx

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 68e1cb5..84bc4b1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,12 +19,12 @@ jobs:
         uses: actions/setup-node@v4
         with:
           node-version: v21.1.0
-          cache: 'npm'
-          cache-dependency-path: '**/package-lock.json'
+          cache: "npm"
+          cache-dependency-path: "**/package-lock.json"
       - name: Set up Golang
         uses: actions/setup-go@v4
         with:
-          go-version: '1.24.0'
+          go-version: "1.24.0"
       - name: Build frontend
         run: |
           make frontend
diff --git a/.github/workflows/ui-lint.yml b/.github/workflows/ui-lint.yml
new file mode 100644
index 0000000..492a5fe
--- /dev/null
+++ b/.github/workflows/ui-lint.yml
@@ -0,0 +1,34 @@
+---
+name: ui-lint
+on:
+  push:
+    paths:
+      - "ui/**"
+      - "package.json"
+      - "package-lock.json"
+      - ".github/workflows/ui-lint.yml"
+
+permissions:
+  contents: read
+
+jobs:
+  ui-lint:
+    name: UI Lint
+    runs-on: buildjet-4vcpu-ubuntu-2204
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+      - name: Set up Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: v21.1.0
+          cache: "npm"
+          cache-dependency-path: "ui/package-lock.json"
+      - name: Install dependencies
+        run: |
+          cd ui
+          npm ci
+      - name: Lint UI
+        run: |
+          cd ui
+          npm run lint
diff --git a/ui/.eslintrc.cjs b/ui/.eslintrc.cjs
index 671054c..568fbd9 100644
--- a/ui/.eslintrc.cjs
+++ b/ui/.eslintrc.cjs
@@ -8,6 +8,8 @@ module.exports = {
     "plugin:react-hooks/recommended",
     "plugin:react/recommended",
     "plugin:react/jsx-runtime",
+    "plugin:import/recommended",
+    "prettier",
   ],
   ignorePatterns: ["dist", ".eslintrc.cjs", "tailwind.config.js", "postcss.config.js"],
   parser: "@typescript-eslint/parser",
@@ -20,5 +22,45 @@ module.exports = {
   },
   rules: {
     "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
+    "import/order": [
+      "error",
+      {
+        /**
+         * @description
+         *
+         * This keeps imports separate from one another, ensuring that imports are separated
+         * by their relative groups. As you move through the groups, imports become closer
+         * to the current file.
+         *
+         * @example
+         * ```
+         * import fs from 'fs';
+         *
+         * import package from 'npm-package';
+         *
+         * import xyz from '~/project-file';
+         *
+         * import index from '../';
+         *
+         * import sibling from './foo';
+         * ```
+         */
+        groups: ["builtin", "external", "internal", "parent", "sibling"],
+        "newlines-between": "always",
+      },
+    ],
+  },
+  settings: {
+    "import/resolver": {
+      alias: {
+        map: [
+          ["@components", "./src/components"],
+          ["@routes", "./src/routes"],
+          ["@assets", "./src/assets"],
+          ["@", "./src"],
+        ],
+        extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
+      },
+    },
   },
 };
diff --git a/ui/.prettierrc b/ui/.prettierrc
index 0fa9a7c..65b362d 100644
--- a/ui/.prettierrc
+++ b/ui/.prettierrc
@@ -5,11 +5,7 @@
   "useTabs": false,
   "arrowParens": "avoid",
   "singleQuote": false,
-  "plugins": [
-    "prettier-plugin-tailwindcss"
-  ],
-  "tailwindFunctions": [
-    "clsx"
-  ],
+  "plugins": ["prettier-plugin-tailwindcss"],
+  "tailwindFunctions": ["clsx"],
   "printWidth": 90
-}
\ No newline at end of file
+}
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 14ed59b..e9caa20 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -9,7 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "@headlessui/react": "^2.2.0",
-        "@headlessui/tailwindcss": "^0.2.2",
+        "@headlessui/tailwindcss": "^0.2.1",
         "@heroicons/react": "^2.2.0",
         "@xterm/addon-clipboard": "^0.1.0",
         "@xterm/addon-fit": "^0.10.0",
@@ -18,47 +18,50 @@
         "@xterm/addon-webgl": "^0.18.0",
         "@xterm/xterm": "^5.5.0",
         "cva": "^1.0.0-beta.1",
+        "eslint-import-resolver-alias": "^1.1.2",
         "focus-trap-react": "^10.2.3",
         "framer-motion": "^11.15.0",
         "lodash.throttle": "^4.1.1",
         "mini-svg-data-uri": "^1.4.4",
-        "motion": "^12.4.7",
         "react": "^18.2.0",
         "react-animate-height": "^3.2.3",
         "react-dom": "^18.2.0",
-        "react-hot-toast": "^2.5.2",
-        "react-icons": "^5.5.0",
+        "react-hot-toast": "^2.4.1",
+        "react-icons": "^5.4.0",
         "react-router-dom": "^6.22.3",
         "react-simple-keyboard": "^3.7.112",
         "react-xtermjs": "^1.0.9",
-        "recharts": "^2.15.1",
-        "semver": "^7.7.1",
+        "recharts": "^2.15.0",
         "tailwind-merge": "^2.5.5",
-        "usehooks-ts": "^3.1.1",
+        "usehooks-ts": "^3.1.0",
         "validator": "^13.12.0",
+        "xterm": "^5.3.0",
         "zustand": "^4.5.2"
       },
       "devDependencies": {
-        "@tailwindcss/forms": "^0.5.10",
-        "@tailwindcss/typography": "^0.5.16",
+        "@tailwindcss/forms": "^0.5.9",
+        "@tailwindcss/typography": "^0.5.15",
         "@types/react": "^18.2.66",
         "@types/react-dom": "^18.3.0",
+        "@types/semver": "^7.5.8",
         "@types/validator": "^13.12.2",
-        "@typescript-eslint/eslint-plugin": "^7.2.0",
-        "@typescript-eslint/parser": "^7.2.0",
-        "@vitejs/plugin-react-swc": "^3.8.0",
+        "@typescript-eslint/eslint-plugin": "^8.25.0",
+        "@typescript-eslint/parser": "^8.25.0",
+        "@vitejs/plugin-react-swc": "^3.7.2",
         "autoprefixer": "^10.4.20",
-        "eslint": "^8.57.0",
-        "eslint-plugin-react": "^7.34.1",
-        "eslint-plugin-react-hooks": "^4.6.0",
-        "eslint-plugin-react-refresh": "^0.4.6",
-        "postcss": "^8.5.3",
-        "prettier": "^3.5.2",
-        "prettier-plugin-tailwindcss": "^0.5.13",
+        "eslint": "^8.20.0",
+        "eslint-config-prettier": "^10.0.1",
+        "eslint-plugin-import": "^2.31.0",
+        "eslint-plugin-react": "^7.37.4",
+        "eslint-plugin-react-hooks": "^5.1.0",
+        "eslint-plugin-react-refresh": "^0.4.19",
+        "postcss": "^8.4.49",
+        "prettier": "^3.4.2",
+        "prettier-plugin-tailwindcss": "^0.6.11",
         "tailwindcss": "^3.4.17",
-        "typescript": "^5.7.3",
+        "typescript": "^5.7.2",
         "vite": "^5.2.0",
-        "vite-tsconfig-paths": "^4.3.2"
+        "vite-tsconfig-paths": "^5.1.4"
       },
       "engines": {
         "node": "21.1.0"
@@ -68,7 +71,6 @@
       "version": "1.2.6",
       "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
       "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -467,7 +469,6 @@
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
       "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
-      "dev": true,
       "dependencies": {
         "eslint-visitor-keys": "^3.3.0"
       },
@@ -482,7 +483,6 @@
       "version": "4.10.0",
       "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
       "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
-      "dev": true,
       "engines": {
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
       }
@@ -491,7 +491,6 @@
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
       "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
-      "dev": true,
       "dependencies": {
         "ajv": "^6.12.4",
         "debug": "^4.3.2",
@@ -514,7 +513,6 @@
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
       "dependencies": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
@@ -524,7 +522,6 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
       "dependencies": {
         "brace-expansion": "^1.1.7"
       },
@@ -536,7 +533,6 @@
       "version": "8.57.0",
       "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
       "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
-      "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
@@ -630,7 +626,6 @@
       "version": "0.11.14",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
       "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
-      "dev": true,
       "dependencies": {
         "@humanwhocodes/object-schema": "^2.0.2",
         "debug": "^4.3.1",
@@ -644,7 +639,6 @@
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
       "dependencies": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
@@ -654,7 +648,6 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
       "dependencies": {
         "brace-expansion": "^1.1.7"
       },
@@ -666,7 +659,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
       "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
-      "dev": true,
       "engines": {
         "node": ">=12.22"
       },
@@ -678,8 +670,7 @@
     "node_modules/@humanwhocodes/object-schema": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
-      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
-      "dev": true
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="
     },
     "node_modules/@isaacs/cliui": {
       "version": "8.0.2",
@@ -1086,6 +1077,11 @@
         "win32"
       ]
     },
+    "node_modules/@rtsao/scc": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+      "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="
+    },
     "node_modules/@swc/core": {
       "version": "1.11.4",
       "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.4.tgz",
@@ -1432,11 +1428,10 @@
       "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
       "dev": true
     },
-    "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
+    "node_modules/@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
     },
     "node_modules/@types/prop-types": {
       "version": "15.7.12",
@@ -1476,79 +1471,69 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz",
-      "integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==",
+      "version": "8.28.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz",
+      "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==",
       "dev": true,
       "dependencies": {
-        "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "7.5.0",
-        "@typescript-eslint/type-utils": "7.5.0",
-        "@typescript-eslint/utils": "7.5.0",
-        "@typescript-eslint/visitor-keys": "7.5.0",
-        "debug": "^4.3.4",
+        "@eslint-community/regexpp": "^4.10.0",
+        "@typescript-eslint/scope-manager": "8.28.0",
+        "@typescript-eslint/type-utils": "8.28.0",
+        "@typescript-eslint/utils": "8.28.0",
+        "@typescript-eslint/visitor-keys": "8.28.0",
         "graphemer": "^1.4.0",
-        "ignore": "^5.2.4",
+        "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
-        "semver": "^7.5.4",
-        "ts-api-utils": "^1.0.1"
+        "ts-api-utils": "^2.0.1"
       },
       "engines": {
-        "node": "^18.18.0 || >=20.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "@typescript-eslint/parser": "^7.0.0",
-        "eslint": "^8.56.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+        "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz",
-      "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==",
+      "version": "8.28.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz",
+      "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.5.0",
-        "@typescript-eslint/types": "7.5.0",
-        "@typescript-eslint/typescript-estree": "7.5.0",
-        "@typescript-eslint/visitor-keys": "7.5.0",
+        "@typescript-eslint/scope-manager": "8.28.0",
+        "@typescript-eslint/types": "8.28.0",
+        "@typescript-eslint/typescript-estree": "8.28.0",
+        "@typescript-eslint/visitor-keys": "8.28.0",
         "debug": "^4.3.4"
       },
       "engines": {
-        "node": "^18.18.0 || >=20.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^8.56.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz",
-      "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==",
+      "version": "8.28.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz",
+      "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.5.0",
-        "@typescript-eslint/visitor-keys": "7.5.0"
+        "@typescript-eslint/types": "8.28.0",
+        "@typescript-eslint/visitor-keys": "8.28.0"
       },
       "engines": {
-        "node": "^18.18.0 || >=20.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "type": "opencollective",
@@ -1556,39 +1541,35 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz",
-      "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==",
+      "version": "8.28.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz",
+      "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.5.0",
-        "@typescript-eslint/utils": "7.5.0",
+        "@typescript-eslint/typescript-estree": "8.28.0",
+        "@typescript-eslint/utils": "8.28.0",
         "debug": "^4.3.4",
-        "ts-api-utils": "^1.0.1"
+        "ts-api-utils": "^2.0.1"
       },
       "engines": {
-        "node": "^18.18.0 || >=20.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^8.56.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz",
-      "integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==",
+      "version": "8.28.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz",
+      "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==",
       "dev": true,
       "engines": {
-        "node": "^18.18.0 || >=20.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "type": "opencollective",
@@ -1596,80 +1577,102 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz",
-      "integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==",
+      "version": "8.28.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz",
+      "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.5.0",
-        "@typescript-eslint/visitor-keys": "7.5.0",
+        "@typescript-eslint/types": "8.28.0",
+        "@typescript-eslint/visitor-keys": "8.28.0",
         "debug": "^4.3.4",
-        "globby": "^11.1.0",
+        "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
-        "minimatch": "9.0.3",
-        "semver": "^7.5.4",
-        "ts-api-utils": "^1.0.1"
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^2.0.1"
       },
       "engines": {
-        "node": "^18.18.0 || >=20.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@typescript-eslint/utils": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz",
-      "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==",
-      "dev": true,
-      "dependencies": {
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "@types/json-schema": "^7.0.12",
-        "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "7.5.0",
-        "@typescript-eslint/types": "7.5.0",
-        "@typescript-eslint/typescript-estree": "7.5.0",
-        "semver": "^7.5.4"
-      },
-      "engines": {
-        "node": "^18.18.0 || >=20.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "eslint": "^8.56.0"
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/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,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.28.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz",
+      "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@typescript-eslint/scope-manager": "8.28.0",
+        "@typescript-eslint/types": "8.28.0",
+        "@typescript-eslint/typescript-estree": "8.28.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.5.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz",
-      "integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==",
+      "version": "8.28.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz",
+      "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.5.0",
-        "eslint-visitor-keys": "^3.4.1"
+        "@typescript-eslint/types": "8.28.0",
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
-        "node": "^18.18.0 || >=20.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "dev": true,
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
     "node_modules/@ungap/structured-clone": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
-      "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
-      "dev": true
+      "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
     },
     "node_modules/@vitejs/plugin-react-swc": {
       "version": "3.8.0",
@@ -1735,7 +1738,6 @@
       "version": "8.11.3",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
       "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
-      "dev": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -1747,7 +1749,6 @@
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
       "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
-      "dev": true,
       "peerDependencies": {
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
@@ -1756,7 +1757,6 @@
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
       "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -1815,17 +1815,15 @@
     "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
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
     "node_modules/array-buffer-byte-length": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
-      "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
-      "dev": true,
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+      "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
       "dependencies": {
-        "call-bind": "^1.0.5",
-        "is-array-buffer": "^3.0.4"
+        "call-bound": "^1.0.3",
+        "is-array-buffer": "^3.0.5"
       },
       "engines": {
         "node": ">= 0.4"
@@ -1838,7 +1836,6 @@
       "version": "3.1.8",
       "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
       "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
-      "dev": true,
       "dependencies": {
         "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
@@ -1854,15 +1851,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/array-union": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
-      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/array.prototype.findlast": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
@@ -1883,11 +1871,30 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/array.prototype.findlastindex": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+      "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.9",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "es-shim-unscopables": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/array.prototype.flat": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
       "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
-      "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
         "define-properties": "^1.2.0",
@@ -1902,15 +1909,14 @@
       }
     },
     "node_modules/array.prototype.flatmap": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
-      "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
-      "dev": true,
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+      "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "es-shim-unscopables": "^1.0.0"
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -1919,45 +1925,34 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/array.prototype.toreversed": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz",
-      "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "es-shim-unscopables": "^1.0.0"
-      }
-    },
     "node_modules/array.prototype.tosorted": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz",
-      "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+      "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
+        "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.3",
-        "es-errors": "^1.1.0",
+        "es-abstract": "^1.23.3",
+        "es-errors": "^1.3.0",
         "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/arraybuffer.prototype.slice": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
-      "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
-      "dev": true,
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+      "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
       "dependencies": {
         "array-buffer-byte-length": "^1.0.1",
-        "call-bind": "^1.0.5",
+        "call-bind": "^1.0.8",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.3",
-        "es-errors": "^1.2.1",
-        "get-intrinsic": "^1.2.3",
-        "is-array-buffer": "^3.0.4",
-        "is-shared-array-buffer": "^1.0.2"
+        "es-abstract": "^1.23.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "is-array-buffer": "^3.0.4"
       },
       "engines": {
         "node": ">= 0.4"
@@ -1966,6 +1961,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/async-function": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+      "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.20",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -2007,7 +2010,6 @@
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
       "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
-      "dev": true,
       "dependencies": {
         "possible-typed-array-names": "^1.0.0"
       },
@@ -2086,16 +2088,41 @@
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
-      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
-      "dev": true,
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
       "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
         "es-define-property": "^1.0.0",
-        "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2",
         "get-intrinsic": "^1.2.4",
-        "set-function-length": "^1.2.1"
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -2108,7 +2135,6 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
       "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
@@ -2145,7 +2171,6 @@
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
       "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
       "dependencies": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
@@ -2226,8 +2251,7 @@
     "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
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
     },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
@@ -2388,14 +2412,13 @@
       }
     },
     "node_modules/data-view-buffer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
-      "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
-      "dev": true,
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+      "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bound": "^1.0.3",
         "es-errors": "^1.3.0",
-        "is-data-view": "^1.0.1"
+        "is-data-view": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -2405,29 +2428,27 @@
       }
     },
     "node_modules/data-view-byte-length": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
-      "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
-      "dev": true,
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+      "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bound": "^1.0.3",
         "es-errors": "^1.3.0",
-        "is-data-view": "^1.0.1"
+        "is-data-view": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/inspect-js"
       }
     },
     "node_modules/data-view-byte-offset": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
-      "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
-      "dev": true,
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+      "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bound": "^1.0.2",
         "es-errors": "^1.3.0",
         "is-data-view": "^1.0.1"
       },
@@ -2442,7 +2463,6 @@
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
       "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
-      "dev": true,
       "dependencies": {
         "ms": "2.1.2"
       },
@@ -2463,14 +2483,12 @@
     "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
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
     },
     "node_modules/define-data-property": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
       "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
-      "dev": true,
       "dependencies": {
         "es-define-property": "^1.0.0",
         "es-errors": "^1.3.0",
@@ -2487,7 +2505,6 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
       "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
-      "dev": true,
       "dependencies": {
         "define-data-property": "^1.0.1",
         "has-property-descriptors": "^1.0.0",
@@ -2505,18 +2522,6 @@
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
       "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
     },
-    "node_modules/dir-glob": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
-      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
-      "dev": true,
-      "dependencies": {
-        "path-type": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/dlv": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -2526,7 +2531,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
       "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
-      "dev": true,
       "dependencies": {
         "esutils": "^2.0.2"
       },
@@ -2543,6 +2547,19 @@
         "csstype": "^3.0.2"
       }
     },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2560,57 +2577,61 @@
       "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
     },
     "node_modules/es-abstract": {
-      "version": "1.23.3",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
-      "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
-      "dev": true,
+      "version": "1.23.9",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
+      "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
       "dependencies": {
-        "array-buffer-byte-length": "^1.0.1",
-        "arraybuffer.prototype.slice": "^1.0.3",
+        "array-buffer-byte-length": "^1.0.2",
+        "arraybuffer.prototype.slice": "^1.0.4",
         "available-typed-arrays": "^1.0.7",
-        "call-bind": "^1.0.7",
-        "data-view-buffer": "^1.0.1",
-        "data-view-byte-length": "^1.0.1",
-        "data-view-byte-offset": "^1.0.0",
-        "es-define-property": "^1.0.0",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "data-view-buffer": "^1.0.2",
+        "data-view-byte-length": "^1.0.2",
+        "data-view-byte-offset": "^1.0.1",
+        "es-define-property": "^1.0.1",
         "es-errors": "^1.3.0",
         "es-object-atoms": "^1.0.0",
-        "es-set-tostringtag": "^2.0.3",
-        "es-to-primitive": "^1.2.1",
-        "function.prototype.name": "^1.1.6",
-        "get-intrinsic": "^1.2.4",
-        "get-symbol-description": "^1.0.2",
-        "globalthis": "^1.0.3",
-        "gopd": "^1.0.1",
+        "es-set-tostringtag": "^2.1.0",
+        "es-to-primitive": "^1.3.0",
+        "function.prototype.name": "^1.1.8",
+        "get-intrinsic": "^1.2.7",
+        "get-proto": "^1.0.0",
+        "get-symbol-description": "^1.1.0",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
         "has-property-descriptors": "^1.0.2",
-        "has-proto": "^1.0.3",
-        "has-symbols": "^1.0.3",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
         "hasown": "^2.0.2",
-        "internal-slot": "^1.0.7",
-        "is-array-buffer": "^3.0.4",
+        "internal-slot": "^1.1.0",
+        "is-array-buffer": "^3.0.5",
         "is-callable": "^1.2.7",
-        "is-data-view": "^1.0.1",
-        "is-negative-zero": "^2.0.3",
-        "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.3",
-        "is-string": "^1.0.7",
-        "is-typed-array": "^1.1.13",
-        "is-weakref": "^1.0.2",
-        "object-inspect": "^1.13.1",
+        "is-data-view": "^1.0.2",
+        "is-regex": "^1.2.1",
+        "is-shared-array-buffer": "^1.0.4",
+        "is-string": "^1.1.1",
+        "is-typed-array": "^1.1.15",
+        "is-weakref": "^1.1.0",
+        "math-intrinsics": "^1.1.0",
+        "object-inspect": "^1.13.3",
         "object-keys": "^1.1.1",
-        "object.assign": "^4.1.5",
-        "regexp.prototype.flags": "^1.5.2",
-        "safe-array-concat": "^1.1.2",
-        "safe-regex-test": "^1.0.3",
-        "string.prototype.trim": "^1.2.9",
-        "string.prototype.trimend": "^1.0.8",
+        "object.assign": "^4.1.7",
+        "own-keys": "^1.0.1",
+        "regexp.prototype.flags": "^1.5.3",
+        "safe-array-concat": "^1.1.3",
+        "safe-push-apply": "^1.0.0",
+        "safe-regex-test": "^1.1.0",
+        "set-proto": "^1.0.0",
+        "string.prototype.trim": "^1.2.10",
+        "string.prototype.trimend": "^1.0.9",
         "string.prototype.trimstart": "^1.0.8",
-        "typed-array-buffer": "^1.0.2",
-        "typed-array-byte-length": "^1.0.1",
-        "typed-array-byte-offset": "^1.0.2",
-        "typed-array-length": "^1.0.6",
-        "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.15"
+        "typed-array-buffer": "^1.0.3",
+        "typed-array-byte-length": "^1.0.3",
+        "typed-array-byte-offset": "^1.0.4",
+        "typed-array-length": "^1.0.7",
+        "unbox-primitive": "^1.1.0",
+        "which-typed-array": "^1.1.18"
       },
       "engines": {
         "node": ">= 0.4"
@@ -2620,13 +2641,9 @@
       }
     },
     "node_modules/es-define-property": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
-      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
-      "dev": true,
-      "dependencies": {
-        "get-intrinsic": "^1.2.4"
-      },
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
       "engines": {
         "node": ">= 0.4"
       }
@@ -2635,41 +2652,41 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
       "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/es-iterator-helpers": {
-      "version": "1.0.18",
-      "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz",
-      "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
+      "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.23.0",
+        "es-abstract": "^1.23.6",
         "es-errors": "^1.3.0",
         "es-set-tostringtag": "^2.0.3",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.4",
-        "globalthis": "^1.0.3",
+        "get-intrinsic": "^1.2.6",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
         "has-property-descriptors": "^1.0.2",
-        "has-proto": "^1.0.3",
-        "has-symbols": "^1.0.3",
-        "internal-slot": "^1.0.7",
-        "iterator.prototype": "^1.1.2",
-        "safe-array-concat": "^1.1.2"
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "internal-slot": "^1.1.0",
+        "iterator.prototype": "^1.1.4",
+        "safe-array-concat": "^1.1.3"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/es-object-atoms": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
-      "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
-      "dev": true,
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
       "dependencies": {
         "es-errors": "^1.3.0"
       },
@@ -2678,37 +2695,38 @@
       }
     },
     "node_modules/es-set-tostringtag": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
-      "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
-      "dev": true,
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
       "dependencies": {
-        "get-intrinsic": "^1.2.4",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
         "has-tostringtag": "^1.0.2",
-        "hasown": "^2.0.1"
+        "hasown": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/es-shim-unscopables": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
-      "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+      "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
       "dependencies": {
-        "hasown": "^2.0.0"
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/es-to-primitive": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
-      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
-      "dev": true,
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+      "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
       "dependencies": {
-        "is-callable": "^1.1.4",
-        "is-date-object": "^1.0.1",
-        "is-symbol": "^1.0.2"
+        "is-callable": "^1.2.7",
+        "is-date-object": "^1.0.5",
+        "is-symbol": "^1.0.4"
       },
       "engines": {
         "node": ">= 0.4"
@@ -2768,7 +2786,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
       "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "dev": true,
       "engines": {
         "node": ">=10"
       },
@@ -2780,7 +2797,6 @@
       "version": "8.57.0",
       "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
       "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
-      "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
@@ -2831,57 +2847,220 @@
         "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/eslint-plugin-react": {
-      "version": "7.34.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz",
-      "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==",
+    "node_modules/eslint-config-prettier": {
+      "version": "10.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz",
+      "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==",
       "dev": true,
+      "bin": {
+        "eslint-config-prettier": "bin/cli.js"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.0.0"
+      }
+    },
+    "node_modules/eslint-import-resolver-alias": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz",
+      "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==",
+      "engines": {
+        "node": ">= 4"
+      },
+      "peerDependencies": {
+        "eslint-plugin-import": ">=1.4.0"
+      }
+    },
+    "node_modules/eslint-import-resolver-node": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+      "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
       "dependencies": {
-        "array-includes": "^3.1.7",
-        "array.prototype.findlast": "^1.2.4",
+        "debug": "^3.2.7",
+        "is-core-module": "^2.13.0",
+        "resolve": "^1.22.4"
+      }
+    },
+    "node_modules/eslint-import-resolver-node/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-import-resolver-node/node_modules/resolve": {
+      "version": "1.22.10",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+      "dependencies": {
+        "is-core-module": "^2.16.0",
+        "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/eslint-module-utils": {
+      "version": "2.12.0",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
+      "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
+      "dependencies": {
+        "debug": "^3.2.7"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependenciesMeta": {
+        "eslint": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-module-utils/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-plugin-import": {
+      "version": "2.31.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
+      "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
+      "dependencies": {
+        "@rtsao/scc": "^1.1.0",
+        "array-includes": "^3.1.8",
+        "array.prototype.findlastindex": "^1.2.5",
+        "array.prototype.flat": "^1.3.2",
         "array.prototype.flatmap": "^1.3.2",
-        "array.prototype.toreversed": "^1.1.2",
-        "array.prototype.tosorted": "^1.1.3",
+        "debug": "^3.2.7",
         "doctrine": "^2.1.0",
-        "es-iterator-helpers": "^1.0.17",
-        "estraverse": "^5.3.0",
-        "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+        "eslint-import-resolver-node": "^0.3.9",
+        "eslint-module-utils": "^2.12.0",
+        "hasown": "^2.0.2",
+        "is-core-module": "^2.15.1",
+        "is-glob": "^4.0.3",
         "minimatch": "^3.1.2",
-        "object.entries": "^1.1.7",
-        "object.fromentries": "^2.0.7",
-        "object.hasown": "^1.1.3",
-        "object.values": "^1.1.7",
-        "prop-types": "^15.8.1",
-        "resolve": "^2.0.0-next.5",
+        "object.fromentries": "^2.0.8",
+        "object.groupby": "^1.0.3",
+        "object.values": "^1.2.0",
         "semver": "^6.3.1",
-        "string.prototype.matchall": "^4.0.10"
+        "string.prototype.trimend": "^1.0.8",
+        "tsconfig-paths": "^3.15.0"
       },
       "engines": {
         "node": ">=4"
       },
       "peerDependencies": {
-        "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+        "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/eslint-plugin-react": {
+      "version": "7.37.4",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz",
+      "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==",
+      "dev": true,
+      "dependencies": {
+        "array-includes": "^3.1.8",
+        "array.prototype.findlast": "^1.2.5",
+        "array.prototype.flatmap": "^1.3.3",
+        "array.prototype.tosorted": "^1.1.4",
+        "doctrine": "^2.1.0",
+        "es-iterator-helpers": "^1.2.1",
+        "estraverse": "^5.3.0",
+        "hasown": "^2.0.2",
+        "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+        "minimatch": "^3.1.2",
+        "object.entries": "^1.1.8",
+        "object.fromentries": "^2.0.8",
+        "object.values": "^1.2.1",
+        "prop-types": "^15.8.1",
+        "resolve": "^2.0.0-next.5",
+        "semver": "^6.3.1",
+        "string.prototype.matchall": "^4.0.12",
+        "string.prototype.repeat": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
       }
     },
     "node_modules/eslint-plugin-react-hooks": {
-      "version": "4.6.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
-      "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+      "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
       "dev": true,
       "engines": {
         "node": ">=10"
       },
       "peerDependencies": {
-        "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+        "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.6",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz",
-      "integrity": "sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==",
+      "version": "0.4.19",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz",
+      "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==",
       "dev": true,
       "peerDependencies": {
-        "eslint": ">=7"
+        "eslint": ">=8.40"
       }
     },
     "node_modules/eslint-plugin-react/node_modules/brace-expansion": {
@@ -2931,7 +3110,6 @@
       "version": "7.2.2",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
       "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
-      "dev": true,
       "dependencies": {
         "esrecurse": "^4.3.0",
         "estraverse": "^5.2.0"
@@ -2947,7 +3125,6 @@
       "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,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
@@ -2959,7 +3136,6 @@
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
       "dependencies": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
@@ -2969,7 +3145,6 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
       "dependencies": {
         "brace-expansion": "^1.1.7"
       },
@@ -2981,7 +3156,6 @@
       "version": "9.6.1",
       "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
       "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
-      "dev": true,
       "dependencies": {
         "acorn": "^8.9.0",
         "acorn-jsx": "^5.3.2",
@@ -2998,7 +3172,6 @@
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
       "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
-      "dev": true,
       "dependencies": {
         "estraverse": "^5.1.0"
       },
@@ -3010,7 +3183,6 @@
       "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,
       "dependencies": {
         "estraverse": "^5.2.0"
       },
@@ -3022,7 +3194,6 @@
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
       "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-      "dev": true,
       "engines": {
         "node": ">=4.0"
       }
@@ -3031,7 +3202,6 @@
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
       "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -3044,8 +3214,7 @@
     "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
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
     },
     "node_modules/fast-equals": {
       "version": "5.2.2",
@@ -3084,14 +3253,12 @@
     "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
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
     },
     "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
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
     },
     "node_modules/fastq": {
       "version": "1.17.1",
@@ -3105,7 +3272,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
       "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
-      "dev": true,
       "dependencies": {
         "flat-cache": "^3.0.4"
       },
@@ -3128,7 +3294,6 @@
       "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,
       "dependencies": {
         "locate-path": "^6.0.0",
         "path-exists": "^4.0.0"
@@ -3144,7 +3309,6 @@
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
       "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
-      "dev": true,
       "dependencies": {
         "flatted": "^3.2.9",
         "keyv": "^4.5.3",
@@ -3157,8 +3321,7 @@
     "node_modules/flatted": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
-      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
-      "dev": true
+      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
     },
     "node_modules/focus-trap": {
       "version": "7.5.4",
@@ -3183,12 +3346,17 @@
       }
     },
     "node_modules/for-each": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
-      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
-      "dev": true,
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+      "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
       "dependencies": {
-        "is-callable": "^1.1.3"
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/foreground-child": {
@@ -3248,8 +3416,7 @@
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
-      "dev": true
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
     },
     "node_modules/fsevents": {
       "version": "2.3.3",
@@ -3273,15 +3440,16 @@
       }
     },
     "node_modules/function.prototype.name": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
-      "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
-      "dev": true,
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+      "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "functions-have-names": "^1.2.3"
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "functions-have-names": "^1.2.3",
+        "hasown": "^2.0.2",
+        "is-callable": "^1.2.7"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3294,22 +3462,25 @@
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
       "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
-      "dev": true,
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
-      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
-      "dev": true,
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
       "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
         "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
         "function-bind": "^1.1.2",
-        "has-proto": "^1.0.1",
-        "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0"
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3318,15 +3489,26 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/get-symbol-description": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
-      "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
-      "dev": true,
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
       "dependencies": {
-        "call-bind": "^1.0.5",
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/get-symbol-description": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+      "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
         "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.4"
+        "get-intrinsic": "^1.2.6"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3339,7 +3521,6 @@
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-      "dev": true,
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
@@ -3370,7 +3551,6 @@
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
       "dependencies": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
@@ -3380,7 +3560,6 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
       "dependencies": {
         "brace-expansion": "^1.1.7"
       },
@@ -3392,7 +3571,6 @@
       "version": "13.24.0",
       "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
       "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
-      "dev": true,
       "dependencies": {
         "type-fest": "^0.20.2"
       },
@@ -3404,12 +3582,12 @@
       }
     },
     "node_modules/globalthis": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
-      "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
-      "dev": true,
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+      "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
       "dependencies": {
-        "define-properties": "^1.1.3"
+        "define-properties": "^1.2.1",
+        "gopd": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3418,26 +3596,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/globby": {
-      "version": "11.1.0",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
-      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
-      "dev": true,
-      "dependencies": {
-        "array-union": "^2.1.0",
-        "dir-glob": "^3.0.1",
-        "fast-glob": "^3.2.9",
-        "ignore": "^5.2.0",
-        "merge2": "^1.4.1",
-        "slash": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/globrex": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
@@ -3453,12 +3611,11 @@
       }
     },
     "node_modules/gopd": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
-      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
-      "dev": true,
-      "dependencies": {
-        "get-intrinsic": "^1.1.3"
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3467,14 +3624,15 @@
     "node_modules/graphemer": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
-      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-      "dev": true
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
     },
     "node_modules/has-bigints": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
-      "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+      "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -3483,7 +3641,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
       "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -3492,7 +3649,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
       "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
-      "dev": true,
       "dependencies": {
         "es-define-property": "^1.0.0"
       },
@@ -3501,10 +3657,12 @@
       }
     },
     "node_modules/has-proto": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
-      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
-      "dev": true,
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+      "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+      "dependencies": {
+        "dunder-proto": "^1.0.0"
+      },
       "engines": {
         "node": ">= 0.4"
       },
@@ -3513,10 +3671,9 @@
       }
     },
     "node_modules/has-symbols": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
-      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
       "engines": {
         "node": ">= 0.4"
       },
@@ -3528,7 +3685,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
       "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
-      "dev": true,
       "dependencies": {
         "has-symbols": "^1.0.3"
       },
@@ -3554,7 +3710,6 @@
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
       "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
-      "dev": true,
       "engines": {
         "node": ">= 4"
       }
@@ -3563,7 +3718,6 @@
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
       "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "dev": true,
       "dependencies": {
         "parent-module": "^1.0.0",
         "resolve-from": "^4.0.0"
@@ -3579,7 +3733,6 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
       "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
-      "dev": true,
       "engines": {
         "node": ">=0.8.19"
       }
@@ -3588,7 +3741,6 @@
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
-      "dev": true,
       "dependencies": {
         "once": "^1.3.0",
         "wrappy": "1"
@@ -3597,18 +3749,16 @@
     "node_modules/inherits": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     "node_modules/internal-slot": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
-      "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+      "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
       "dependencies": {
         "es-errors": "^1.3.0",
-        "hasown": "^2.0.0",
-        "side-channel": "^1.0.4"
+        "hasown": "^2.0.2",
+        "side-channel": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3623,13 +3773,13 @@
       }
     },
     "node_modules/is-array-buffer": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
-      "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
-      "dev": true,
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+      "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1"
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3639,12 +3789,15 @@
       }
     },
     "node_modules/is-async-function": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
-      "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
-      "dev": true,
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+      "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "async-function": "^1.0.0",
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.1",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3654,12 +3807,14 @@
       }
     },
     "node_modules/is-bigint": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
-      "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+      "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
       "dependencies": {
-        "has-bigints": "^1.0.1"
+        "has-bigints": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3677,13 +3832,12 @@
       }
     },
     "node_modules/is-boolean-object": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
-      "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
-      "dev": true,
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+      "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3696,7 +3850,6 @@
       "version": "1.2.7",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
       "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -3705,22 +3858,26 @@
       }
     },
     "node_modules/is-core-module": {
-      "version": "2.13.1",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
-      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
       "dependencies": {
-        "hasown": "^2.0.0"
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/is-data-view": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
-      "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
-      "dev": true,
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+      "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
       "dependencies": {
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
         "is-typed-array": "^1.1.13"
       },
       "engines": {
@@ -3731,12 +3888,12 @@
       }
     },
     "node_modules/is-date-object": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
-      "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+      "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3754,12 +3911,14 @@
       }
     },
     "node_modules/is-finalizationregistry": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
-      "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
-      "dev": true,
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+      "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
       "dependencies": {
-        "call-bind": "^1.0.2"
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3774,12 +3933,14 @@
       }
     },
     "node_modules/is-generator-function": {
-      "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
-      "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
+      "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.0",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3803,19 +3964,6 @@
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
       "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/is-negative-zero": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
-      "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -3832,12 +3980,12 @@
       }
     },
     "node_modules/is-number-object": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
-      "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
-      "dev": true,
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+      "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3850,19 +3998,19 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
       "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/is-regex": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
-      "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
-      "dev": true,
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3875,7 +4023,6 @@
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
       "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -3884,12 +4031,11 @@
       }
     },
     "node_modules/is-shared-array-buffer": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
-      "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
-      "dev": true,
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+      "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
       "dependencies": {
-        "call-bind": "^1.0.7"
+        "call-bound": "^1.0.3"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3899,12 +4045,12 @@
       }
     },
     "node_modules/is-string": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
-      "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
-      "dev": true,
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+      "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3914,12 +4060,13 @@
       }
     },
     "node_modules/is-symbol": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
-      "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
-      "dev": true,
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+      "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
       "dependencies": {
-        "has-symbols": "^1.0.2"
+        "call-bound": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "safe-regex-test": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3929,12 +4076,11 @@
       }
     },
     "node_modules/is-typed-array": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
-      "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
-      "dev": true,
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+      "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
       "dependencies": {
-        "which-typed-array": "^1.1.14"
+        "which-typed-array": "^1.1.16"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3947,7 +4093,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
       "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
@@ -3956,25 +4101,26 @@
       }
     },
     "node_modules/is-weakref": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
-      "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
-      "dev": true,
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+      "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
       "dependencies": {
-        "call-bind": "^1.0.2"
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/is-weakset": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
-      "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
-      "dev": true,
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+      "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
       "dependencies": {
-        "call-bind": "^1.0.7",
-        "get-intrinsic": "^1.2.4"
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
       },
       "engines": {
         "node": ">= 0.4"
@@ -3986,8 +4132,7 @@
     "node_modules/isarray": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
-      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
-      "dev": true
+      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
     },
     "node_modules/isexe": {
       "version": "2.0.0",
@@ -3995,16 +4140,20 @@
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
     },
     "node_modules/iterator.prototype": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
-      "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+      "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
       "dev": true,
       "dependencies": {
-        "define-properties": "^1.2.1",
-        "get-intrinsic": "^1.2.1",
-        "has-symbols": "^1.0.3",
-        "reflect.getprototypeof": "^1.0.4",
-        "set-function-name": "^2.0.1"
+        "define-data-property": "^1.1.4",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.6",
+        "get-proto": "^1.0.0",
+        "has-symbols": "^1.1.0",
+        "set-function-name": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/jackspeak": {
@@ -4046,7 +4195,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
       "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-      "dev": true,
       "dependencies": {
         "argparse": "^2.0.1"
       },
@@ -4057,20 +4205,28 @@
     "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
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
     },
     "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
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
     },
     "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
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
+    },
+    "node_modules/json5": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+      "dependencies": {
+        "minimist": "^1.2.0"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      }
     },
     "node_modules/jsx-ast-utils": {
       "version": "3.3.5",
@@ -4091,7 +4247,6 @@
       "version": "4.5.4",
       "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
       "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
-      "dev": true,
       "dependencies": {
         "json-buffer": "3.0.1"
       }
@@ -4100,7 +4255,6 @@
       "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,
       "dependencies": {
         "prelude-ls": "^1.2.1",
         "type-check": "~0.4.0"
@@ -4129,7 +4283,6 @@
       "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,
       "dependencies": {
         "p-locate": "^5.0.0"
       },
@@ -4165,8 +4318,7 @@
     "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
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
     },
     "node_modules/lodash.throttle": {
       "version": "4.1.1",
@@ -4184,6 +4336,14 @@
         "loose-envify": "cli.js"
       }
     },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -4226,6 +4386,14 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/minipass": {
       "version": "7.0.4",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
@@ -4234,31 +4402,6 @@
         "node": ">=16 || 14 >=14.17"
       }
     },
-    "node_modules/motion": {
-      "version": "12.4.7",
-      "resolved": "https://registry.npmjs.org/motion/-/motion-12.4.7.tgz",
-      "integrity": "sha512-mhegHAbf1r80fr+ytC6OkjKvIUegRNXKLWNPrCN2+GnixlNSPwT03FtKqp9oDny1kNcLWZvwbmEr+JqVryFrcg==",
-      "dependencies": {
-        "framer-motion": "^12.4.7",
-        "tslib": "^2.4.0"
-      },
-      "peerDependencies": {
-        "@emotion/is-prop-valid": "*",
-        "react": "^18.0.0 || ^19.0.0",
-        "react-dom": "^18.0.0 || ^19.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@emotion/is-prop-valid": {
-          "optional": true
-        },
-        "react": {
-          "optional": true
-        },
-        "react-dom": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/motion-dom": {
       "version": "11.14.3",
       "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
@@ -4269,50 +4412,10 @@
       "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
       "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ=="
     },
-    "node_modules/motion/node_modules/framer-motion": {
-      "version": "12.4.7",
-      "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.7.tgz",
-      "integrity": "sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==",
-      "dependencies": {
-        "motion-dom": "^12.4.5",
-        "motion-utils": "^12.0.0",
-        "tslib": "^2.4.0"
-      },
-      "peerDependencies": {
-        "@emotion/is-prop-valid": "*",
-        "react": "^18.0.0 || ^19.0.0",
-        "react-dom": "^18.0.0 || ^19.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@emotion/is-prop-valid": {
-          "optional": true
-        },
-        "react": {
-          "optional": true
-        },
-        "react-dom": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/motion/node_modules/motion-dom": {
-      "version": "12.4.5",
-      "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.4.5.tgz",
-      "integrity": "sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==",
-      "dependencies": {
-        "motion-utils": "^12.0.0"
-      }
-    },
-    "node_modules/motion/node_modules/motion-utils": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
-      "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA=="
-    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "node_modules/mz": {
       "version": "2.7.0",
@@ -4344,8 +4447,7 @@
     "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
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
     },
     "node_modules/node-releases": {
       "version": "2.0.19",
@@ -4387,10 +4489,12 @@
       }
     },
     "node_modules/object-inspect": {
-      "version": "1.13.1",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
-      "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
-      "dev": true,
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -4399,20 +4503,20 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
       "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-      "dev": true,
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/object.assign": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
-      "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
-      "dev": true,
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+      "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
       "dependencies": {
-        "call-bind": "^1.0.5",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
         "define-properties": "^1.2.1",
-        "has-symbols": "^1.0.3",
+        "es-object-atoms": "^1.0.0",
+        "has-symbols": "^1.1.0",
         "object-keys": "^1.1.1"
       },
       "engines": {
@@ -4440,7 +4544,6 @@
       "version": "2.0.8",
       "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
       "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
-      "dev": true,
       "dependencies": {
         "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
@@ -4454,30 +4557,26 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/object.hasown": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz",
-      "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==",
-      "dev": true,
+    "node_modules/object.groupby": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+      "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
       "dependencies": {
+        "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.23.2",
-        "es-object-atoms": "^1.0.0"
+        "es-abstract": "^1.23.2"
       },
       "engines": {
         "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/object.values": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
-      "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==",
-      "dev": true,
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+      "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
         "define-properties": "^1.2.1",
         "es-object-atoms": "^1.0.0"
       },
@@ -4492,7 +4591,6 @@
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "dev": true,
       "dependencies": {
         "wrappy": "1"
       }
@@ -4501,7 +4599,6 @@
       "version": "0.9.3",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
       "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
-      "dev": true,
       "dependencies": {
         "@aashutoshrathi/word-wrap": "^1.2.3",
         "deep-is": "^0.1.3",
@@ -4514,11 +4611,26 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/own-keys": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+      "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+      "dependencies": {
+        "get-intrinsic": "^1.2.6",
+        "object-keys": "^1.1.1",
+        "safe-push-apply": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "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,
       "dependencies": {
         "yocto-queue": "^0.1.0"
       },
@@ -4533,7 +4645,6 @@
       "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,
       "dependencies": {
         "p-limit": "^3.0.2"
       },
@@ -4548,7 +4659,6 @@
       "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,
       "dependencies": {
         "callsites": "^3.0.0"
       },
@@ -4560,7 +4670,6 @@
       "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,
       "engines": {
         "node": ">=8"
       }
@@ -4569,7 +4678,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
       "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -4610,15 +4718,6 @@
         "node": "14 || >=16.14"
       }
     },
-    "node_modules/path-type": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/picocolors": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -4652,10 +4751,9 @@
       }
     },
     "node_modules/possible-typed-array-names": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
-      "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+      "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
       "engines": {
         "node": ">= 0.4"
       }
@@ -4816,7 +4914,6 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
       "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8.0"
       }
@@ -4837,9 +4934,9 @@
       }
     },
     "node_modules/prettier-plugin-tailwindcss": {
-      "version": "0.5.13",
-      "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.13.tgz",
-      "integrity": "sha512-2tPWHCFNC+WRjAC4SIWQNSOdcL1NNkydXim8w7TDqlZi+/ulZYz2OouAI6qMtkggnPt7lGamboj6LcTMwcCvoQ==",
+      "version": "0.6.11",
+      "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz",
+      "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==",
       "dev": true,
       "engines": {
         "node": ">=14.21.3"
@@ -4849,13 +4946,14 @@
         "@prettier/plugin-pug": "*",
         "@shopify/prettier-plugin-liquid": "*",
         "@trivago/prettier-plugin-sort-imports": "*",
-        "@zackad/prettier-plugin-twig-melody": "*",
+        "@zackad/prettier-plugin-twig": "*",
         "prettier": "^3.0",
         "prettier-plugin-astro": "*",
         "prettier-plugin-css-order": "*",
         "prettier-plugin-import-sort": "*",
         "prettier-plugin-jsdoc": "*",
         "prettier-plugin-marko": "*",
+        "prettier-plugin-multiline-arrays": "*",
         "prettier-plugin-organize-attributes": "*",
         "prettier-plugin-organize-imports": "*",
         "prettier-plugin-sort-imports": "*",
@@ -4875,7 +4973,7 @@
         "@trivago/prettier-plugin-sort-imports": {
           "optional": true
         },
-        "@zackad/prettier-plugin-twig-melody": {
+        "@zackad/prettier-plugin-twig": {
           "optional": true
         },
         "prettier-plugin-astro": {
@@ -4893,6 +4991,9 @@
         "prettier-plugin-marko": {
           "optional": true
         },
+        "prettier-plugin-multiline-arrays": {
+          "optional": true
+        },
         "prettier-plugin-organize-attributes": {
           "optional": true
         },
@@ -4924,7 +5025,6 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
       "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
@@ -5143,18 +5243,18 @@
       "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
     },
     "node_modules/reflect.getprototypeof": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
-      "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==",
-      "dev": true,
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+      "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.23.1",
+        "es-abstract": "^1.23.9",
         "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.4",
-        "globalthis": "^1.0.3",
-        "which-builtin-type": "^1.1.3"
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.7",
+        "get-proto": "^1.0.1",
+        "which-builtin-type": "^1.2.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5169,15 +5269,16 @@
       "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
     },
     "node_modules/regexp.prototype.flags": {
-      "version": "1.5.2",
-      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
-      "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
-      "dev": true,
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+      "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bind": "^1.0.8",
         "define-properties": "^1.2.1",
         "es-errors": "^1.3.0",
-        "set-function-name": "^2.0.1"
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "set-function-name": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5207,7 +5308,6 @@
       "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,
       "engines": {
         "node": ">=4"
       }
@@ -5225,7 +5325,6 @@
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-      "dev": true,
       "dependencies": {
         "glob": "^7.1.3"
       },
@@ -5293,14 +5392,14 @@
       }
     },
     "node_modules/safe-array-concat": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
-      "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
-      "dev": true,
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+      "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
       "dependencies": {
-        "call-bind": "^1.0.7",
-        "get-intrinsic": "^1.2.4",
-        "has-symbols": "^1.0.3",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "has-symbols": "^1.1.0",
         "isarray": "^2.0.5"
       },
       "engines": {
@@ -5310,15 +5409,29 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/safe-regex-test": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
-      "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
-      "dev": true,
+    "node_modules/safe-push-apply": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+      "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
       "dependencies": {
-        "call-bind": "^1.0.6",
         "es-errors": "^1.3.0",
-        "is-regex": "^1.1.4"
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-regex-test": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+      "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-regex": "^1.2.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5339,6 +5452,7 @@
       "version": "7.7.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
       "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "dev": true,
       "bin": {
         "semver": "bin/semver.js"
       },
@@ -5350,7 +5464,6 @@
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
       "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
-      "dev": true,
       "dependencies": {
         "define-data-property": "^1.1.4",
         "es-errors": "^1.3.0",
@@ -5367,7 +5480,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
       "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
-      "dev": true,
       "dependencies": {
         "define-data-property": "^1.1.4",
         "es-errors": "^1.3.0",
@@ -5378,6 +5490,19 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/set-proto": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+      "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -5398,15 +5523,65 @@
       }
     },
     "node_modules/side-channel": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
-      "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
       "dependencies": {
-        "call-bind": "^1.0.7",
         "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.4",
-        "object-inspect": "^1.13.1"
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5426,15 +5601,6 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/slash": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
-      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/source-map-js": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5504,23 +5670,24 @@
       }
     },
     "node_modules/string.prototype.matchall": {
-      "version": "4.0.11",
-      "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
-      "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==",
+      "version": "4.0.12",
+      "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+      "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.23.2",
+        "es-abstract": "^1.23.6",
         "es-errors": "^1.3.0",
         "es-object-atoms": "^1.0.0",
-        "get-intrinsic": "^1.2.4",
-        "gopd": "^1.0.1",
-        "has-symbols": "^1.0.3",
-        "internal-slot": "^1.0.7",
-        "regexp.prototype.flags": "^1.5.2",
+        "get-intrinsic": "^1.2.6",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "internal-slot": "^1.1.0",
+        "regexp.prototype.flags": "^1.5.3",
         "set-function-name": "^2.0.2",
-        "side-channel": "^1.0.6"
+        "side-channel": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5529,16 +5696,28 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/string.prototype.trim": {
-      "version": "1.2.9",
-      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
-      "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
+    "node_modules/string.prototype.repeat": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+      "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.5"
+      }
+    },
+    "node_modules/string.prototype.trim": {
+      "version": "1.2.10",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+      "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-data-property": "^1.1.4",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.23.0",
-        "es-object-atoms": "^1.0.0"
+        "es-abstract": "^1.23.5",
+        "es-object-atoms": "^1.0.0",
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5548,15 +5727,18 @@
       }
     },
     "node_modules/string.prototype.trimend": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
-      "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
-      "dev": true,
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+      "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
         "define-properties": "^1.2.1",
         "es-object-atoms": "^1.0.0"
       },
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
@@ -5565,7 +5747,6 @@
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
       "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
-      "dev": true,
       "dependencies": {
         "call-bind": "^1.0.7",
         "define-properties": "^1.2.1",
@@ -5601,11 +5782,18 @@
         "node": ">=8"
       }
     },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "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,
       "engines": {
         "node": ">=8"
       },
@@ -5659,7 +5847,6 @@
       "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,
       "dependencies": {
         "has-flag": "^4.0.0"
       },
@@ -5747,8 +5934,7 @@
     "node_modules/text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
     },
     "node_modules/thenify": {
       "version": "3.3.1",
@@ -5786,15 +5972,15 @@
       }
     },
     "node_modules/ts-api-utils": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
-      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+      "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
       "dev": true,
       "engines": {
-        "node": ">=16"
+        "node": ">=18.12"
       },
       "peerDependencies": {
-        "typescript": ">=4.2.0"
+        "typescript": ">=4.8.4"
       }
     },
     "node_modules/ts-interface-checker": {
@@ -5822,6 +6008,17 @@
         }
       }
     },
+    "node_modules/tsconfig-paths": {
+      "version": "3.15.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+      "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+      "dependencies": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      }
+    },
     "node_modules/tslib": {
       "version": "2.6.2",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -5831,7 +6028,6 @@
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
       "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
-      "dev": true,
       "dependencies": {
         "prelude-ls": "^1.2.1"
       },
@@ -5843,7 +6039,6 @@
       "version": "0.20.2",
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
       "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
-      "dev": true,
       "engines": {
         "node": ">=10"
       },
@@ -5852,30 +6047,28 @@
       }
     },
     "node_modules/typed-array-buffer": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
-      "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
-      "dev": true,
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+      "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bound": "^1.0.3",
         "es-errors": "^1.3.0",
-        "is-typed-array": "^1.1.13"
+        "is-typed-array": "^1.1.14"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/typed-array-byte-length": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
-      "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
-      "dev": true,
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+      "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
         "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
-        "has-proto": "^1.0.3",
-        "is-typed-array": "^1.1.13"
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.14"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5885,17 +6078,17 @@
       }
     },
     "node_modules/typed-array-byte-offset": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
-      "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
-      "dev": true,
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+      "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
       "dependencies": {
         "available-typed-arrays": "^1.0.7",
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
         "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
-        "has-proto": "^1.0.3",
-        "is-typed-array": "^1.1.13"
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.15",
+        "reflect.getprototypeof": "^1.0.9"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5905,17 +6098,16 @@
       }
     },
     "node_modules/typed-array-length": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
-      "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
-      "dev": true,
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+      "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
       "dependencies": {
         "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
         "gopd": "^1.0.1",
-        "has-proto": "^1.0.3",
         "is-typed-array": "^1.1.13",
-        "possible-typed-array-names": "^1.0.0"
+        "possible-typed-array-names": "^1.0.0",
+        "reflect.getprototypeof": "^1.0.6"
       },
       "engines": {
         "node": ">= 0.4"
@@ -5938,15 +6130,17 @@
       }
     },
     "node_modules/unbox-primitive": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
-      "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
-      "dev": true,
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+      "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
       "dependencies": {
-        "call-bind": "^1.0.2",
+        "call-bound": "^1.0.3",
         "has-bigints": "^1.0.2",
-        "has-symbols": "^1.0.3",
-        "which-boxed-primitive": "^1.0.2"
+        "has-symbols": "^1.1.0",
+        "which-boxed-primitive": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -5986,7 +6180,6 @@
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-      "dev": true,
       "dependencies": {
         "punycode": "^2.1.0"
       }
@@ -6103,9 +6296,9 @@
       }
     },
     "node_modules/vite-tsconfig-paths": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz",
-      "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",
+      "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==",
       "dev": true,
       "dependencies": {
         "debug": "^4.1.1",
@@ -6136,39 +6329,41 @@
       }
     },
     "node_modules/which-boxed-primitive": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
-      "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
-      "dev": true,
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+      "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
       "dependencies": {
-        "is-bigint": "^1.0.1",
-        "is-boolean-object": "^1.1.0",
-        "is-number-object": "^1.0.4",
-        "is-string": "^1.0.5",
-        "is-symbol": "^1.0.3"
+        "is-bigint": "^1.1.0",
+        "is-boolean-object": "^1.2.1",
+        "is-number-object": "^1.1.1",
+        "is-string": "^1.1.1",
+        "is-symbol": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/which-builtin-type": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
-      "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
-      "dev": true,
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+      "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
       "dependencies": {
-        "function.prototype.name": "^1.1.5",
-        "has-tostringtag": "^1.0.0",
+        "call-bound": "^1.0.2",
+        "function.prototype.name": "^1.1.6",
+        "has-tostringtag": "^1.0.2",
         "is-async-function": "^2.0.0",
-        "is-date-object": "^1.0.5",
-        "is-finalizationregistry": "^1.0.2",
+        "is-date-object": "^1.1.0",
+        "is-finalizationregistry": "^1.1.0",
         "is-generator-function": "^1.0.10",
-        "is-regex": "^1.1.4",
+        "is-regex": "^1.2.1",
         "is-weakref": "^1.0.2",
         "isarray": "^2.0.5",
-        "which-boxed-primitive": "^1.0.2",
-        "which-collection": "^1.0.1",
-        "which-typed-array": "^1.1.9"
+        "which-boxed-primitive": "^1.1.0",
+        "which-collection": "^1.0.2",
+        "which-typed-array": "^1.1.16"
       },
       "engines": {
         "node": ">= 0.4"
@@ -6181,7 +6376,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
       "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
-      "dev": true,
       "dependencies": {
         "is-map": "^2.0.3",
         "is-set": "^2.0.3",
@@ -6196,15 +6390,16 @@
       }
     },
     "node_modules/which-typed-array": {
-      "version": "1.1.15",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
-      "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
-      "dev": true,
+      "version": "1.1.19",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+      "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
       "dependencies": {
         "available-typed-arrays": "^1.0.7",
-        "call-bind": "^1.0.7",
-        "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "for-each": "^0.3.5",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
         "has-tostringtag": "^1.0.2"
       },
       "engines": {
@@ -6304,8 +6499,13 @@
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-      "dev": true
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+    },
+    "node_modules/xterm": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
+      "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
+      "deprecated": "This package is now deprecated. Move to @xterm/xterm instead."
     },
     "node_modules/yaml": {
       "version": "2.4.1",
@@ -6322,7 +6522,6 @@
       "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,
       "engines": {
         "node": ">=10"
       },
diff --git a/ui/package.json b/ui/package.json
index fbeebc1..f8f1c7a 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -13,12 +13,13 @@
     "build:device": "tsc && vite build --mode=device --emptyOutDir",
     "build:staging": "tsc && vite build --mode=cloud-staging",
     "build:prod": "tsc && vite build --mode=cloud-production",
-    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+    "lint": "eslint './src/**/*.{ts,tsx}'",
+    "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix",
     "preview": "vite preview"
   },
   "dependencies": {
     "@headlessui/react": "^2.2.0",
-    "@headlessui/tailwindcss": "^0.2.2",
+    "@headlessui/tailwindcss": "^0.2.1",
     "@heroicons/react": "^2.2.0",
     "@xterm/addon-clipboard": "^0.1.0",
     "@xterm/addon-fit": "^0.10.0",
@@ -27,46 +28,49 @@
     "@xterm/addon-webgl": "^0.18.0",
     "@xterm/xterm": "^5.5.0",
     "cva": "^1.0.0-beta.1",
+    "eslint-import-resolver-alias": "^1.1.2",
     "focus-trap-react": "^10.2.3",
     "framer-motion": "^11.15.0",
     "lodash.throttle": "^4.1.1",
     "mini-svg-data-uri": "^1.4.4",
-    "motion": "^12.4.7",
     "react": "^18.2.0",
     "react-animate-height": "^3.2.3",
     "react-dom": "^18.2.0",
-    "react-hot-toast": "^2.5.2",
-    "react-icons": "^5.5.0",
+    "react-hot-toast": "^2.4.1",
+    "react-icons": "^5.4.0",
     "react-router-dom": "^6.22.3",
     "react-simple-keyboard": "^3.7.112",
     "react-xtermjs": "^1.0.9",
-    "recharts": "^2.15.1",
-    "semver": "^7.7.1",
+    "recharts": "^2.15.0",
     "tailwind-merge": "^2.5.5",
-    "usehooks-ts": "^3.1.1",
+    "usehooks-ts": "^3.1.0",
     "validator": "^13.12.0",
+    "xterm": "^5.3.0",
     "zustand": "^4.5.2"
   },
   "devDependencies": {
-    "@tailwindcss/forms": "^0.5.10",
-    "@tailwindcss/typography": "^0.5.16",
+    "@tailwindcss/forms": "^0.5.9",
+    "@tailwindcss/typography": "^0.5.15",
     "@types/react": "^18.2.66",
     "@types/react-dom": "^18.3.0",
+    "@types/semver": "^7.5.8",
     "@types/validator": "^13.12.2",
-    "@typescript-eslint/eslint-plugin": "^7.2.0",
-    "@typescript-eslint/parser": "^7.2.0",
-    "@vitejs/plugin-react-swc": "^3.8.0",
+    "@typescript-eslint/eslint-plugin": "^8.25.0",
+    "@typescript-eslint/parser": "^8.25.0",
+    "@vitejs/plugin-react-swc": "^3.7.2",
     "autoprefixer": "^10.4.20",
-    "eslint": "^8.57.0",
-    "eslint-plugin-react": "^7.34.1",
-    "eslint-plugin-react-hooks": "^4.6.0",
-    "eslint-plugin-react-refresh": "^0.4.6",
-    "postcss": "^8.5.3",
-    "prettier": "^3.5.2",
-    "prettier-plugin-tailwindcss": "^0.5.13",
+    "eslint": "^8.20.0",
+    "eslint-config-prettier": "^10.0.1",
+    "eslint-plugin-import": "^2.31.0",
+    "eslint-plugin-react": "^7.37.4",
+    "eslint-plugin-react-hooks": "^5.1.0",
+    "eslint-plugin-react-refresh": "^0.4.19",
+    "postcss": "^8.4.49",
+    "prettier": "^3.4.2",
+    "prettier-plugin-tailwindcss": "^0.6.11",
     "tailwindcss": "^3.4.17",
-    "typescript": "^5.7.3",
+    "typescript": "^5.7.2",
     "vite": "^5.2.0",
-    "vite-tsconfig-paths": "^4.3.2"
+    "vite-tsconfig-paths": "^5.1.4"
   }
 }
diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx
index f8c97d7..1afef63 100644
--- a/ui/src/components/ActionBar.tsx
+++ b/ui/src/components/ActionBar.tsx
@@ -1,3 +1,10 @@
+import { MdOutlineContentPasteGo } from "react-icons/md";
+import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
+import { FaKeyboard } from "react-icons/fa6";
+import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
+import { Fragment, useCallback, useRef } from "react";
+import { CommandLineIcon } from "@heroicons/react/20/solid";
+
 import { Button } from "@components/Button";
 import {
   useHidStore,
@@ -5,19 +12,13 @@ import {
   useSettingsStore,
   useUiStore,
 } from "@/hooks/stores";
-import { MdOutlineContentPasteGo } from "react-icons/md";
 import Container from "@components/Container";
-import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
 import { cx } from "@/cva.config";
 import PasteModal from "@/components/popovers/PasteModal";
-import { FaKeyboard } from "react-icons/fa6";
 import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
-import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
-import MountPopopover from "./popovers/MountPopover";
-import { Fragment, useCallback, useRef } from "react";
-import { CommandLineIcon } from "@heroicons/react/20/solid";
-import ExtensionPopover from "./popovers/ExtensionPopover";
-import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
+import MountPopopover from "@/components/popovers/MountPopover";
+import ExtensionPopover from "@/components/popovers/ExtensionPopover";
+import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
 
 export default function Actionbar({
   requestFullscreen,
diff --git a/ui/src/components/AuthLayout.tsx b/ui/src/components/AuthLayout.tsx
index 5ed427a..b1e9cbc 100644
--- a/ui/src/components/AuthLayout.tsx
+++ b/ui/src/components/AuthLayout.tsx
@@ -1,21 +1,22 @@
+import { useLocation, useNavigation, useSearchParams } from "react-router-dom";
+
 import { Button, LinkButton } from "@components/Button";
 import { GoogleIcon } from "@components/Icons";
 import SimpleNavbar from "@components/SimpleNavbar";
 import Container from "@components/Container";
-import { useLocation, useNavigation, useSearchParams } from "react-router-dom";
 import Fieldset from "@components/Fieldset";
 import GridBackground from "@components/GridBackground";
 import StepCounter from "@components/StepCounter";
 import { CLOUD_API } from "@/ui.config";
 
-type AuthLayoutProps = {
+interface AuthLayoutProps {
   title: string;
   description: string;
   action: string;
   cta: string;
   ctaHref: string;
   showCounter?: boolean;
-};
+}
 
 export default function AuthLayout({
   title,
@@ -46,8 +47,8 @@ export default function AuthLayout({
           }
         />
         <Container>
-          <div className="flex items-center justify-center w-full h-full isolate">
-            <div className="max-w-2xl -mt-16 space-y-8">
+          <div className="isolate flex h-full w-full items-center justify-center">
+            <div className="-mt-16 max-w-2xl space-y-8">
               {showCounter ? (
                 <div className="text-center">
                   <StepCounter currStepIdx={0} nSteps={2} />
@@ -61,11 +62,8 @@ export default function AuthLayout({
               </div>
 
               <Fieldset className="space-y-12">
-                <div className="max-w-sm mx-auto space-y-4">
-                  <form
-                    action={`${CLOUD_API}/oidc/google`}
-                    method="POST"
-                  >
+                <div className="mx-auto max-w-sm space-y-4">
+                  <form action={`${CLOUD_API}/oidc/google`} method="POST">
                     {/*This could be the KVM ID*/}
                     {deviceId ? (
                       <input type="hidden" name="deviceId" value={deviceId} />
diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx
index 2e9a1fe..3b7ac95 100644
--- a/ui/src/components/Button.tsx
+++ b/ui/src/components/Button.tsx
@@ -1,8 +1,9 @@
 import React from "react";
+import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
+
 import ExtLink from "@/components/ExtLink";
 import LoadingSpinner from "@/components/LoadingSpinner";
 import { cva, cx } from "@/cva.config";
-import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
 
 const sizes = {
   XS: "h-[28px] px-2 text-xs",
@@ -101,7 +102,7 @@ const iconVariants = cva({
   },
 });
 
-type ButtonContentPropsType = {
+interface ButtonContentPropsType {
   text?: string | React.ReactNode;
   LeadingIcon?: React.FC<{ className: string | undefined }> | null;
   TrailingIcon?: React.FC<{ className: string | undefined }> | null;
@@ -111,7 +112,7 @@ type ButtonContentPropsType = {
   size: keyof typeof sizes;
   theme: keyof typeof themes;
   loading?: boolean;
-};
+}
 
 function ButtonContent(props: ButtonContentPropsType) {
   const { text, LeadingIcon, TrailingIcon, fullWidth, className, textAlign, loading } =
diff --git a/ui/src/components/Card.tsx b/ui/src/components/Card.tsx
index 1811bf8..857ed92 100644
--- a/ui/src/components/Card.tsx
+++ b/ui/src/components/Card.tsx
@@ -1,10 +1,11 @@
 import React, { forwardRef } from "react";
+
 import { cx } from "@/cva.config";
 
-type CardPropsType = {
+interface CardPropsType {
   children: React.ReactNode;
   className?: string;
-};
+}
 
 export const GridCard = ({
   children,
diff --git a/ui/src/components/CardHeader.tsx b/ui/src/components/CardHeader.tsx
index 979c24a..c9ed3ce 100644
--- a/ui/src/components/CardHeader.tsx
+++ b/ui/src/components/CardHeader.tsx
@@ -1,10 +1,10 @@
 import React from "react";
 
-type Props = {
+interface Props {
   headline: string;
   description?: string | React.ReactNode;
   Button?: React.ReactNode;
-};
+}
 
 export const CardHeader = ({ headline, description, Button }: Props) => {
   return (
diff --git a/ui/src/components/Checkbox.tsx b/ui/src/components/Checkbox.tsx
index a9c4801..261a425 100644
--- a/ui/src/components/Checkbox.tsx
+++ b/ui/src/components/Checkbox.tsx
@@ -1,7 +1,8 @@
 import type { Ref } from "react";
 import React, { forwardRef } from "react";
-import FieldLabel from "@/components/FieldLabel";
 import clsx from "clsx";
+
+import FieldLabel from "@/components/FieldLabel";
 import { cva, cx } from "@/cva.config";
 
 const sizes = {
@@ -52,7 +53,7 @@ type CheckboxWithLabelProps = React.ComponentProps<typeof FieldLabel> &
 
 const CheckboxWithLabel = forwardRef<HTMLInputElement, CheckboxWithLabelProps>(
   function CheckboxWithLabel(
-    { label, id, description, as, fullWidth, readOnly, ...props },
+    { label, id, description, fullWidth, readOnly, ...props },
     ref: Ref<HTMLInputElement>,
   ) {
     return (
diff --git a/ui/src/components/Container.tsx b/ui/src/components/Container.tsx
index ba02e64..a759ca5 100644
--- a/ui/src/components/Container.tsx
+++ b/ui/src/components/Container.tsx
@@ -1,4 +1,6 @@
+/* eslint-disable react-refresh/only-export-components */
 import React, { ReactNode } from "react";
+
 import { cx } from "@/cva.config";
 
 function Container({ children, className }: { children: ReactNode; className?: string }) {
diff --git a/ui/src/components/CustomTooltip.tsx b/ui/src/components/CustomTooltip.tsx
index 8ca214c..a27f607 100644
--- a/ui/src/components/CustomTooltip.tsx
+++ b/ui/src/components/CustomTooltip.tsx
@@ -1,8 +1,8 @@
 import Card from "@components/Card";
 
-export type CustomTooltipProps = {
+export interface CustomTooltipProps {
   payload: { payload: { date: number; stat: number }; unit: string }[];
-};
+}
 
 export default function CustomTooltip({ payload }: CustomTooltipProps) {
   if (payload?.length) {
diff --git a/ui/src/components/EmptyCard.tsx b/ui/src/components/EmptyCard.tsx
index 0b467b9..d8ba782 100644
--- a/ui/src/components/EmptyCard.tsx
+++ b/ui/src/components/EmptyCard.tsx
@@ -1,14 +1,16 @@
-import { GridCard } from "@/components/Card";
 import React from "react";
+
+import { GridCard } from "@/components/Card";
+
 import { cx } from "../cva.config";
 
-type Props = {
-  IconElm?: React.FC<any>;
+interface Props {
+  IconElm?: React.FC<{ className: string | undefined }>;
   headline: string;
   description?: string | React.ReactNode;
   BtnElm?: React.ReactNode;
   className?: string;
-};
+}
 
 export default function EmptyCard({
   IconElm,
@@ -27,10 +29,16 @@ export default function EmptyCard({
       >
         <div className="max-w-[90%] space-y-1.5 text-center md:max-w-[60%]">
           <div className="space-y-2">
-            {IconElm && <IconElm className="w-6 h-6 mx-auto text-blue-600 dark:text-blue-400" />}
-            <h4 className="text-base font-bold leading-none text-black dark:text-white">{headline}</h4>
+            {IconElm && (
+              <IconElm className="mx-auto h-6 w-6 text-blue-600 dark:text-blue-400" />
+            )}
+            <h4 className="text-base font-bold leading-none text-black dark:text-white">
+              {headline}
+            </h4>
           </div>
-          <p className="mx-auto text-sm text-slate-600 dark:text-slate-400">{description}</p>
+          <p className="mx-auto text-sm text-slate-600 dark:text-slate-400">
+            {description}
+          </p>
         </div>
         {BtnElm}
       </div>
diff --git a/ui/src/components/ExtLink.tsx b/ui/src/components/ExtLink.tsx
index 09c5f4e..79eec8c 100644
--- a/ui/src/components/ExtLink.tsx
+++ b/ui/src/components/ExtLink.tsx
@@ -1,4 +1,5 @@
 import React from "react";
+
 import { cx } from "@/cva.config";
 
 export default function ExtLink({
diff --git a/ui/src/components/FeatureFlag.tsx b/ui/src/components/FeatureFlag.tsx
index 985cec6..cc0c7c5 100644
--- a/ui/src/components/FeatureFlag.tsx
+++ b/ui/src/components/FeatureFlag.tsx
@@ -1,4 +1,5 @@
 import { useEffect } from "react";
+
 import { useFeatureFlag } from "../hooks/useFeatureFlag";
 
 export function FeatureFlag({
diff --git a/ui/src/components/FieldLabel.tsx b/ui/src/components/FieldLabel.tsx
index 687e44c..42e6ede 100644
--- a/ui/src/components/FieldLabel.tsx
+++ b/ui/src/components/FieldLabel.tsx
@@ -1,13 +1,14 @@
 import React from "react";
+
 import { cx } from "@/cva.config";
 
-type Props = {
+interface Props {
   label: string | React.ReactNode;
   id?: string;
   as?: "label" | "span";
   description?: string | React.ReactNode | null;
   disabled?: boolean;
-};
+}
 export default function FieldLabel({
   label,
   id,
diff --git a/ui/src/components/Fieldset.tsx b/ui/src/components/Fieldset.tsx
index edfa823..9a37e79 100644
--- a/ui/src/components/Fieldset.tsx
+++ b/ui/src/components/Fieldset.tsx
@@ -9,7 +9,7 @@ export default function Fieldset({
   disabled,
 }: {
   children: React.ReactNode;
-  fetcher?: FetcherWithComponents<any>;
+  fetcher?: FetcherWithComponents<unknown>;
   className?: string;
   disabled?: boolean;
 }) {
diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx
index efbbfbd..452a19c 100644
--- a/ui/src/components/Header.tsx
+++ b/ui/src/components/Header.tsx
@@ -2,19 +2,22 @@ import { Fragment, useCallback } from "react";
 import { useNavigate } from "react-router-dom";
 import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
 import { Menu, MenuButton } from "@headlessui/react";
+import { LuMonitorSmartphone } from "react-icons/lu";
+
 import Container from "@/components/Container";
 import Card from "@/components/Card";
-import { LuMonitorSmartphone } from "react-icons/lu";
 import { cx } from "@/cva.config";
 import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
 import LogoBlueIcon from "@/assets/logo-blue.svg";
 import LogoWhiteIcon from "@/assets/logo-white.svg";
 import USBStateStatus from "@components/USBStateStatus";
 import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
+import { CLOUD_API, DEVICE_API } from "@/ui.config";
+
 import api from "../api";
 import { isOnDevice } from "../main";
+
 import { Button, LinkButton } from "./Button";
-import { CLOUD_API, DEVICE_API } from "@/ui.config";
 
 interface NavbarProps {
   isLoggedIn: boolean;
diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx
index 7c002f1..aa00da7 100644
--- a/ui/src/components/InfoBar.tsx
+++ b/ui/src/components/InfoBar.tsx
@@ -1,3 +1,5 @@
+import { useEffect } from "react";
+
 import { cx } from "@/cva.config";
 import {
   useHidStore,
@@ -6,7 +8,6 @@ import {
   useSettingsStore,
   useVideoStore,
 } from "@/hooks/stores";
-import { useEffect } from "react";
 import { keys, modifiers } from "@/keyboardMappings";
 
 export default function InfoBar() {
diff --git a/ui/src/components/InputField.tsx b/ui/src/components/InputField.tsx
index 57db7d8..2f580a0 100644
--- a/ui/src/components/InputField.tsx
+++ b/ui/src/components/InputField.tsx
@@ -1,7 +1,8 @@
 import type { Ref } from "react";
 import React, { forwardRef } from "react";
-import FieldLabel from "@/components/FieldLabel";
 import clsx from "clsx";
+
+import FieldLabel from "@/components/FieldLabel";
 import Card from "@/components/Card";
 import { cva } from "@/cva.config";
 
@@ -84,7 +85,7 @@ const InputFieldWithLabel = forwardRef<HTMLInputElement, InputFieldWithLabelProp
         {(label || description) && (
           <FieldLabel label={label} id={id} description={description} />
         )}
-        <InputField ref={ref as any} id={id} {...props} />
+        <InputField ref={ref as never} id={id} {...props} />
       </div>
     );
   },
diff --git a/ui/src/components/KvmCard.tsx b/ui/src/components/KvmCard.tsx
index a0dc1e2..c680a37 100644
--- a/ui/src/components/KvmCard.tsx
+++ b/ui/src/components/KvmCard.tsx
@@ -1,10 +1,11 @@
-import { Button, LinkButton } from "@components/Button";
-import Card from "@components/Card";
 import { MdConnectWithoutContact } from "react-icons/md";
 import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
 import { Link } from "react-router-dom";
 import { LuEllipsisVertical } from "react-icons/lu";
 
+import Card from "@components/Card";
+import { Button, LinkButton } from "@components/Button";
+
 function getRelativeTimeString(date: Date | number, lang = navigator.language): string {
   // Allow dates or times to be passed
   const timeMs = typeof date === "number" ? date : date.getTime();
diff --git a/ui/src/components/Modal.tsx b/ui/src/components/Modal.tsx
index 49fb0c3..039b493 100644
--- a/ui/src/components/Modal.tsx
+++ b/ui/src/components/Modal.tsx
@@ -1,5 +1,6 @@
 import React from "react";
 import { Dialog, DialogBackdrop, DialogPanel } from "@headlessui/react";
+
 import { cx } from "@/cva.config";
 
 const Modal = React.memo(function Modal({
diff --git a/ui/src/components/NotFoundPage.tsx b/ui/src/components/NotFoundPage.tsx
index c89b618..b499b11 100644
--- a/ui/src/components/NotFoundPage.tsx
+++ b/ui/src/components/NotFoundPage.tsx
@@ -1,4 +1,5 @@
 import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
+
 import EmptyCard from "@/components/EmptyCard";
 
 export default function NotFoundPage() {
diff --git a/ui/src/components/PeerConnectionStatusCard.tsx b/ui/src/components/PeerConnectionStatusCard.tsx
index ca0621f..07e91cd 100644
--- a/ui/src/components/PeerConnectionStatusCard.tsx
+++ b/ui/src/components/PeerConnectionStatusCard.tsx
@@ -13,11 +13,9 @@ const PeerConnectionStatusMap = {
 
 export type PeerConnections = keyof typeof PeerConnectionStatusMap;
 
-type StatusProps = {
-  [key in PeerConnections]: {
+type StatusProps = Record<PeerConnections, {
     statusIndicatorClassName: string;
-  };
-};
+  }>;
 
 export default function PeerConnectionStatusCard({
   state,
diff --git a/ui/src/components/SelectMenuBasic.tsx b/ui/src/components/SelectMenuBasic.tsx
index b68bc38..c518bfe 100644
--- a/ui/src/components/SelectMenuBasic.tsx
+++ b/ui/src/components/SelectMenuBasic.tsx
@@ -1,9 +1,12 @@
 import React from "react";
-import FieldLabel from "@/components/FieldLabel";
 import clsx from "clsx";
-import Card from "./Card";
+
+import FieldLabel from "@/components/FieldLabel";
 import { cva } from "@/cva.config";
 
+import Card from "./Card";
+
+
 type SelectMenuProps = Pick<
   JSX.IntrinsicElements["select"],
   "disabled" | "onChange" | "name" | "value"
diff --git a/ui/src/components/SimpleNavbar.tsx b/ui/src/components/SimpleNavbar.tsx
index 86f6520..7652ad0 100644
--- a/ui/src/components/SimpleNavbar.tsx
+++ b/ui/src/components/SimpleNavbar.tsx
@@ -1,10 +1,11 @@
-import Container from "@/components/Container";
 import { Link } from "react-router-dom";
 import React from "react";
+
+import Container from "@/components/Container";
 import LogoBlueIcon from "@/assets/logo-blue.png";
 import LogoWhiteIcon from "@/assets/logo-white.svg";
 
-type Props = { logoHref?: string; actionElement?: React.ReactNode };
+interface Props { logoHref?: string; actionElement?: React.ReactNode }
 
 export default function SimpleNavbar({ logoHref, actionElement }: Props) {
   return (
diff --git a/ui/src/components/StatChart.tsx b/ui/src/components/StatChart.tsx
index 1c188aa..2c403e3 100644
--- a/ui/src/components/StatChart.tsx
+++ b/ui/src/components/StatChart.tsx
@@ -9,6 +9,7 @@ import {
   XAxis,
   YAxis,
 } from "recharts";
+
 import CustomTooltip, { CustomTooltipProps } from "@components/CustomTooltip";
 
 export default function StatChart({
diff --git a/ui/src/components/StatusCards.tsx b/ui/src/components/StatusCards.tsx
index 6bdcf56..8cbe9f3 100644
--- a/ui/src/components/StatusCards.tsx
+++ b/ui/src/components/StatusCards.tsx
@@ -1,4 +1,5 @@
 import React from "react";
+
 import { cx } from "@/cva.config";
 
 interface Props {
diff --git a/ui/src/components/StepCounter.tsx b/ui/src/components/StepCounter.tsx
index 4e39c59..57bcca9 100644
--- a/ui/src/components/StepCounter.tsx
+++ b/ui/src/components/StepCounter.tsx
@@ -1,12 +1,13 @@
 import { CheckIcon } from "@heroicons/react/16/solid";
+
 import { cva, cx } from "@/cva.config";
 import Card from "@/components/Card";
 
-type Props = {
+interface Props {
   nSteps: number;
   currStepIdx: number;
   size?: keyof typeof sizes;
-};
+}
 
 const sizes = {
   SM: "text-xs leading-[12px]",
diff --git a/ui/src/components/Terminal.tsx b/ui/src/components/Terminal.tsx
index 1456e74..7e09c6f 100644
--- a/ui/src/components/Terminal.tsx
+++ b/ui/src/components/Terminal.tsx
@@ -1,8 +1,5 @@
 import "react-simple-keyboard/build/css/index.css";
-import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
-import { Button } from "./Button";
 import { ChevronDownIcon } from "@heroicons/react/16/solid";
-import { cx } from "@/cva.config";
 import { useEffect } from "react";
 import { useXTerm } from "react-xtermjs";
 import { FitAddon } from "@xterm/addon-fit";
@@ -11,6 +8,11 @@ import { WebglAddon } from "@xterm/addon-webgl";
 import { Unicode11Addon } from "@xterm/addon-unicode11";
 import { ClipboardAddon } from "@xterm/addon-clipboard";
 
+import { cx } from "@/cva.config";
+import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
+
+import { Button } from "./Button";
+
 const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
 
 // Terminal theme configuration
diff --git a/ui/src/components/TextArea.tsx b/ui/src/components/TextArea.tsx
index 71b7551..51747a3 100644
--- a/ui/src/components/TextArea.tsx
+++ b/ui/src/components/TextArea.tsx
@@ -1,6 +1,7 @@
 import React from "react";
-import FieldLabel from "@/components/FieldLabel";
 import clsx from "clsx";
+
+import FieldLabel from "@/components/FieldLabel";
 import { FieldError } from "@/components/InputField";
 import Card from "@/components/Card";
 import { cx } from "@/cva.config";
diff --git a/ui/src/components/USBStateStatus.tsx b/ui/src/components/USBStateStatus.tsx
index 7ebb788..d8e86c6 100644
--- a/ui/src/components/USBStateStatus.tsx
+++ b/ui/src/components/USBStateStatus.tsx
@@ -1,23 +1,20 @@
+import React from "react";
+
 import { cx } from "@/cva.config";
 import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
-import React from "react";
 import LoadingSpinner from "@components/LoadingSpinner";
 import StatusCard from "@components/StatusCards";
 import { HidState } from "@/hooks/stores";
 
 type USBStates = HidState["usbState"];
 
-type StatusProps = {
-  [key in USBStates]: {
+type StatusProps = Record<USBStates, {
     icon: React.FC<{ className: string | undefined }>;
     iconClassName: string;
     statusIndicatorClassName: string;
-  };
-};
+  }>;
 
-const USBStateMap: {
-  [key in USBStates]: string;
-} = {
+const USBStateMap: Record<USBStates, string> = {
   configured: "Connected",
   attached: "Connecting",
   addressed: "Connecting",
diff --git a/ui/src/components/UpdateInProgressStatusCard.tsx b/ui/src/components/UpdateInProgressStatusCard.tsx
index a6dc103..764bcdb 100644
--- a/ui/src/components/UpdateInProgressStatusCard.tsx
+++ b/ui/src/components/UpdateInProgressStatusCard.tsx
@@ -1,8 +1,10 @@
 import { cx } from "@/cva.config";
+
+import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
+
 import { Button } from "./Button";
 import { GridCard } from "./Card";
 import LoadingSpinner from "./LoadingSpinner";
-import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
 
 export default function UpdateInProgressStatusCard() {
   const { navigateTo } = useDeviceUiNavigation();
diff --git a/ui/src/components/UsbDeviceSetting.tsx b/ui/src/components/UsbDeviceSetting.tsx
index 07125e6..9ec7d39 100644
--- a/ui/src/components/UsbDeviceSetting.tsx
+++ b/ui/src/components/UsbDeviceSetting.tsx
@@ -1,9 +1,9 @@
-import { useCallback } from "react";
+import { useCallback , useEffect, useState } from "react";
 
-import { useEffect, useState } from "react";
 import { useJsonRpc } from "../hooks/useJsonRpc";
 import notifications from "../notifications";
 import { SettingsItem } from "../routes/devices.$id.settings";
+
 import Checkbox from "./Checkbox";
 import { Button } from "./Button";
 import { SelectMenuBasic } from "./SelectMenuBasic";
diff --git a/ui/src/components/UsbInfoSetting.tsx b/ui/src/components/UsbInfoSetting.tsx
index 4ac93ff..198335c 100644
--- a/ui/src/components/UsbInfoSetting.tsx
+++ b/ui/src/components/UsbInfoSetting.tsx
@@ -1,14 +1,14 @@
-import { useMemo } from "react";
+import { useMemo , useCallback , useEffect, useState } from "react";
 
-import { useCallback } from "react";
 import { Button } from "@components/Button";
-import { InputFieldWithLabel } from "./InputField";
 
-import { useEffect, useState } from "react";
+
 import { UsbConfigState } from "../hooks/stores";
 import { useJsonRpc } from "../hooks/useJsonRpc";
 import notifications from "../notifications";
 import { SettingsItem } from "../routes/devices.$id.settings";
+
+import { InputFieldWithLabel } from "./InputField";
 import { SelectMenuBasic } from "./SelectMenuBasic";
 import Fieldset from "./Fieldset";
 
diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx
index a8560cb..f13b6ce 100644
--- a/ui/src/components/VideoOverlay.tsx
+++ b/ui/src/components/VideoOverlay.tsx
@@ -1,11 +1,12 @@
 import React from "react";
 import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
 import { ArrowRightIcon } from "@heroicons/react/16/solid";
+import { motion, AnimatePresence } from "framer-motion";
+import { LuPlay } from "react-icons/lu";
+
 import { Button, LinkButton } from "@components/Button";
 import LoadingSpinner from "@components/LoadingSpinner";
 import { GridCard } from "@components/Card";
-import { motion, AnimatePresence } from "motion/react";
-import { LuPlay } from "react-icons/lu";
 
 interface OverlayContentProps {
   children: React.ReactNode;
diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx
index a26d683..ec5906c 100644
--- a/ui/src/components/VirtualKeyboard.tsx
+++ b/ui/src/components/VirtualKeyboard.tsx
@@ -1,11 +1,15 @@
 import { useCallback, useEffect, useRef, useState } from "react";
 import Keyboard from "react-simple-keyboard";
-import { Button } from "@components/Button";
-import Card from "@components/Card";
 import { ChevronDownIcon } from "@heroicons/react/16/solid";
+import { motion, AnimatePresence } from "framer-motion";
+
+import Card from "@components/Card";
+// eslint-disable-next-line import/order
+import { Button } from "@components/Button";
+
 import "react-simple-keyboard/build/css/index.css";
+
 import { useHidStore, useUiStore } from "@/hooks/stores";
-import { motion, AnimatePresence } from "motion/react";
 import { cx } from "@/cva.config";
 import { keys, modifiers } from "@/keyboardMappings";
 import useKeyboard from "@/hooks/useKeyboard";
diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx
index 4cd56f6..911c5ea 100644
--- a/ui/src/components/WebRTCVideo.tsx
+++ b/ui/src/components/WebRTCVideo.tsx
@@ -1,4 +1,5 @@
 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+
 import {
   useDeviceSettingsStore,
   useHidStore,
@@ -15,9 +16,13 @@ import Actionbar from "@components/ActionBar";
 import InfoBar from "@components/InfoBar";
 import useKeyboard from "@/hooks/useKeyboard";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
-import { HDMIErrorOverlay, NoAutoplayPermissionsOverlay } from "./VideoOverlay";
-import { ConnectionErrorOverlay } from "./VideoOverlay";
-import { LoadingOverlay } from "./VideoOverlay";
+
+import {
+  HDMIErrorOverlay,
+  NoAutoplayPermissionsOverlay,
+  ConnectionErrorOverlay,
+  LoadingOverlay,
+} from "./VideoOverlay";
 
 export default function WebRTCVideo() {
   // Video and stream related refs and states
@@ -82,13 +87,13 @@ export default function WebRTCVideo() {
 
   const onVideoPlaying = useCallback(() => {
     setIsPlaying(true);
-    videoElm.current && updateVideoSizeStore(videoElm.current);
+    if (videoElm.current) updateVideoSizeStore(videoElm.current);
   }, [updateVideoSizeStore]);
 
   // On mount, get the video size
   useEffect(
     function updateVideoSizeOnMount() {
-      videoElm.current && updateVideoSizeStore(videoElm.current);
+      if (videoElm.current) updateVideoSizeStore(videoElm.current);
     },
     [setVideoClientSize, updateVideoSizeStore, setVideoSize],
   );
diff --git a/ui/src/components/extensions/ATXPowerControl.tsx b/ui/src/components/extensions/ATXPowerControl.tsx
index 62b3bfb..0334a18 100644
--- a/ui/src/components/extensions/ATXPowerControl.tsx
+++ b/ui/src/components/extensions/ATXPowerControl.tsx
@@ -1,11 +1,13 @@
-import { Button } from "@components/Button";
 import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
+import { useEffect, useState } from "react";
+
+import { Button } from "@components/Button";
 import Card from "@components/Card";
 import { SettingsPageHeader } from "@components/SettingsPageheader";
-import { useEffect, useState } from "react";
 import notifications from "@/notifications";
+import LoadingSpinner from "@/components/LoadingSpinner";
+
 import { useJsonRpc } from "../../hooks/useJsonRpc";
-import LoadingSpinner from "../LoadingSpinner";
 
 const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
 
@@ -102,11 +104,11 @@ export function ATXPowerControl() {
 
       {atxState === null ? (
         <Card className="flex h-[120px] items-center justify-center p-3">
-          <LoadingSpinner className="w-6 h-6 text-blue-500 dark:text-blue-400" />
+          <LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
         </Card>
       ) : (
         <Card className="h-[120px] animate-fadeIn opacity-0">
-          <div className="p-3 space-y-4">
+          <div className="space-y-4 p-3">
             {/* Control Buttons */}
             <div className="flex items-center space-x-2">
               <Button
diff --git a/ui/src/components/extensions/DCPowerControl.tsx b/ui/src/components/extensions/DCPowerControl.tsx
index e903939..3fcb7dc 100644
--- a/ui/src/components/extensions/DCPowerControl.tsx
+++ b/ui/src/components/extensions/DCPowerControl.tsx
@@ -1,12 +1,13 @@
-import { Button } from "@components/Button";
 import { LuPower } from "react-icons/lu";
+import { useCallback, useEffect, useState } from "react";
+
+import { Button } from "@components/Button";
 import Card from "@components/Card";
 import { SettingsPageHeader } from "@components/SettingsPageheader";
-import FieldLabel from "../FieldLabel";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
-import { useCallback, useEffect, useState } from "react";
 import notifications from "@/notifications";
-import LoadingSpinner from "../LoadingSpinner";
+import FieldLabel from "@components/FieldLabel";
+import LoadingSpinner from "@components/LoadingSpinner";
 
 interface DCPowerState {
   isOn: boolean;
@@ -59,11 +60,11 @@ export function DCPowerControl() {
 
       {powerState === null ? (
         <Card className="flex h-[160px] justify-center p-3">
-          <LoadingSpinner className="w-6 h-6 text-blue-500 dark:text-blue-400" />
+          <LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
         </Card>
       ) : (
         <Card className="h-[160px] animate-fadeIn opacity-0">
-          <div className="p-3 space-y-4">
+          <div className="space-y-4 p-3">
             {/* Power Controls */}
             <div className="flex items-center space-x-2">
               <Button
diff --git a/ui/src/components/extensions/SerialConsole.tsx b/ui/src/components/extensions/SerialConsole.tsx
index 238a3aa..544d3fd 100644
--- a/ui/src/components/extensions/SerialConsole.tsx
+++ b/ui/src/components/extensions/SerialConsole.tsx
@@ -1,12 +1,13 @@
-import { Button } from "@components/Button";
 import { LuTerminal } from "react-icons/lu";
+import { useEffect, useState } from "react";
+
+import { Button } from "@components/Button";
 import Card from "@components/Card";
 import { SettingsPageHeader } from "@components/SettingsPageheader";
-import { SelectMenuBasic } from "../SelectMenuBasic";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
-import { useEffect, useState } from "react";
 import notifications from "@/notifications";
 import { useUiStore } from "@/hooks/stores";
+import { SelectMenuBasic } from "@components/SelectMenuBasic";
 
 interface SerialSettings {
   baudRate: string;
diff --git a/ui/src/components/popovers/ExtensionPopover.tsx b/ui/src/components/popovers/ExtensionPopover.tsx
index e8141aa..69d0e70 100644
--- a/ui/src/components/popovers/ExtensionPopover.tsx
+++ b/ui/src/components/popovers/ExtensionPopover.tsx
@@ -1,19 +1,20 @@
 import { useEffect, useState } from "react";
+import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
+
 import { useJsonRpc } from "@/hooks/useJsonRpc";
 import Card, { GridCard } from "@components/Card";
 import { SettingsPageHeader } from "@components/SettingsPageheader";
-import { Button } from "../Button";
-import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
 import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
 import { DCPowerControl } from "@components/extensions/DCPowerControl";
 import { SerialConsole } from "@components/extensions/SerialConsole";
-import notifications from "../../notifications";
+import { Button } from "@components/Button";
+import notifications from "@/notifications";
 
 interface Extension {
   id: string;
   name: string;
   description: string;
-  icon: any;
+  icon: React.ElementType;
 }
 
 const AVAILABLE_EXTENSIONS: Extension[] = [
@@ -58,7 +59,9 @@ export default function ExtensionPopover() {
   const handleSetActiveExtension = (extension: Extension | null) => {
     send("setActiveExtension", { extensionId: extension?.id || "" }, resp => {
       if ("error" in resp) {
-        notifications.error(`Failed to set active extension: ${resp.error.data || "Unknown error"}`);
+        notifications.error(
+          `Failed to set active extension: ${resp.error.data || "Unknown error"}`,
+        );
         return;
       }
       setActiveExtension(extension);
@@ -80,7 +83,7 @@ export default function ExtensionPopover() {
 
   return (
     <GridCard>
-      <div className="p-4 py-3 space-y-4">
+      <div className="space-y-4 p-4 py-3">
         <div className="grid h-full grid-rows-headerBody">
           <div className="space-y-4">
             {activeExtension ? (
@@ -89,7 +92,7 @@ export default function ExtensionPopover() {
                 {renderActiveExtension()}
 
                 <div
-                  className="flex items-center justify-end space-x-2 opacity-0 animate-fadeIn"
+                  className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
                   style={{
                     animationDuration: "0.7s",
                     animationDelay: "0.2s",
@@ -110,7 +113,7 @@ export default function ExtensionPopover() {
                   title="Extensions"
                   description="Load and manage your extensions"
                 />
-                <Card className="opacity-0 animate-fadeIn">
+                <Card className="animate-fadeIn opacity-0">
                   <div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
                     {AVAILABLE_EXTENSIONS.map(extension => (
                       <div
diff --git a/ui/src/components/popovers/MountPopover.tsx b/ui/src/components/popovers/MountPopover.tsx
index 41dbbd6..065f547 100644
--- a/ui/src/components/popovers/MountPopover.tsx
+++ b/ui/src/components/popovers/MountPopover.tsx
@@ -1,11 +1,6 @@
-import { Button } from "@components/Button";
 import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
-import Card, { GridCard } from "@components/Card";
 import { PlusCircleIcon } from "@heroicons/react/20/solid";
 import { useMemo, forwardRef, useEffect, useCallback } from "react";
-import { formatters } from "@/utils";
-import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores";
-import { SettingsPageHeader } from "@components/SettingsPageheader";
 import {
   LuArrowUpFromLine,
   LuCheckCheck,
@@ -13,11 +8,17 @@ import {
   LuPlus,
   LuRadioReceiver,
 } from "react-icons/lu";
-import { useJsonRpc } from "@/hooks/useJsonRpc";
-import notifications from "../../notifications";
 import { useClose } from "@headlessui/react";
 import { useLocation } from "react-router-dom";
+
+import { Button } from "@components/Button";
+import Card, { GridCard } from "@components/Card";
+import { formatters } from "@/utils";
+import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores";
+import { SettingsPageHeader } from "@components/SettingsPageheader";
+import { useJsonRpc } from "@/hooks/useJsonRpc";
 import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
+import notifications from "@/notifications";
 
 const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
   const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats);
@@ -194,7 +195,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
     <GridCard>
       <div className="space-y-4 p-4 py-3">
         <div ref={ref} className="grid h-full grid-rows-headerBody">
-          <div className="h-full space-y-4 ">
+          <div className="h-full space-y-4">
             <div className="space-y-4">
               <SettingsPageHeader
                 title="Virtual Media"
diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx
index ce93934..643f55b 100644
--- a/ui/src/components/popovers/PasteModal.tsx
+++ b/ui/src/components/popovers/PasteModal.tsx
@@ -1,15 +1,16 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { LuCornerDownLeft } from "react-icons/lu";
+import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
+import { useClose } from "@headlessui/react";
+
 import { Button } from "@components/Button";
 import { GridCard } from "@components/Card";
 import { TextAreaWithLabel } from "@components/TextArea";
 import { SettingsPageHeader } from "@components/SettingsPageheader";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
 import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores";
-import notifications from "../../notifications";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { LuCornerDownLeft } from "react-icons/lu";
-import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
-import { useClose } from "@headlessui/react";
 import { chars, keys, modifiers } from "@/keyboardMappings";
+import notifications from "@/notifications";
 
 const hidKeyboardPayload = (keys: number[], modifier: number) => {
   return { keys, modifier };
@@ -59,6 +60,7 @@ export default function PasteModal() {
         });
       }
     } catch (error) {
+      console.error(error);
       notifications.error("Failed to paste text");
     }
   }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode]);
@@ -71,7 +73,7 @@ export default function PasteModal() {
 
   return (
     <GridCard>
-      <div className="p-4 py-3 space-y-4">
+      <div className="space-y-4 p-4 py-3">
         <div className="grid h-full grid-rows-headerBody">
           <div className="h-full space-y-4">
             <div className="space-y-4">
@@ -81,7 +83,7 @@ export default function PasteModal() {
               />
 
               <div
-                className="space-y-2 opacity-0 animate-fadeIn"
+                className="animate-fadeIn space-y-2 opacity-0"
                 style={{
                   animationDuration: "0.7s",
                   animationDelay: "0.1s",
@@ -120,8 +122,8 @@ export default function PasteModal() {
                     />
 
                     {invalidChars.length > 0 && (
-                      <div className="flex items-center mt-2 gap-x-2">
-                        <ExclamationCircleIcon className="w-4 h-4 text-red-500 dark:text-red-400" />
+                      <div className="mt-2 flex items-center gap-x-2">
+                        <ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
                         <span className="text-xs text-red-500 dark:text-red-400">
                           The following characters won&apos;t be pasted:{" "}
                           {invalidChars.join(", ")}
@@ -135,7 +137,7 @@ export default function PasteModal() {
           </div>
         </div>
         <div
-          className="flex items-center justify-end opacity-0 animate-fadeIn gap-x-2"
+          className="flex animate-fadeIn items-center justify-end gap-x-2 opacity-0"
           style={{
             animationDuration: "0.7s",
             animationDelay: "0.2s",
diff --git a/ui/src/components/popovers/WakeOnLan/AddDeviceForm.tsx b/ui/src/components/popovers/WakeOnLan/AddDeviceForm.tsx
index b94c350..2b4198c 100644
--- a/ui/src/components/popovers/WakeOnLan/AddDeviceForm.tsx
+++ b/ui/src/components/popovers/WakeOnLan/AddDeviceForm.tsx
@@ -1,8 +1,8 @@
-import { InputFieldWithLabel } from "@components/InputField";
 import { useState, useRef } from "react";
-import { LuPlus } from "react-icons/lu";
-import { Button } from "../../Button";
-import { LuArrowLeft } from "react-icons/lu";
+import { LuPlus, LuArrowLeft } from "react-icons/lu";
+
+import { InputFieldWithLabel } from "@/components/InputField";
+import { Button } from "@/components/Button";
 
 interface AddDeviceFormProps {
   onAddDevice: (name: string, macAddress: string) => void;
@@ -26,7 +26,7 @@ export default function AddDeviceForm({
   return (
     <div className="space-y-4">
       <div
-        className="space-y-4 opacity-0 animate-fadeIn"
+        className="animate-fadeIn space-y-4 opacity-0"
         style={{
           animationDuration: "0.5s",
           animationFillMode: "forwards",
@@ -73,7 +73,7 @@ export default function AddDeviceForm({
         />
       </div>
       <div
-        className="flex items-center justify-end space-x-2 opacity-0 animate-fadeIn"
+        className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
         style={{
           animationDuration: "0.7s",
           animationDelay: "0.2s",
diff --git a/ui/src/components/popovers/WakeOnLan/DeviceList.tsx b/ui/src/components/popovers/WakeOnLan/DeviceList.tsx
index 2f5537a..b46e84a 100644
--- a/ui/src/components/popovers/WakeOnLan/DeviceList.tsx
+++ b/ui/src/components/popovers/WakeOnLan/DeviceList.tsx
@@ -1,8 +1,9 @@
-import { Button } from "@components/Button";
-import Card from "@components/Card";
-import { FieldError } from "@components/InputField";
 import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
 
+import { Button } from "@/components/Button";
+import Card from "@/components/Card";
+import { FieldError } from "@/components/InputField";
+
 export interface StoredDevice {
   name: string;
   macAddress: string;
@@ -27,12 +28,14 @@ export default function DeviceList({
 }: DeviceListProps) {
   return (
     <div className="space-y-4">
-      <Card className="opacity-0 animate-fadeIn">
+      <Card className="animate-fadeIn opacity-0">
         <div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
           {storedDevices.map((device, index) => (
-            <div key={index} className="flex items-center justify-between p-3 gap-x-2">
+            <div key={index} className="flex items-center justify-between gap-x-2 p-3">
               <div className="space-y-0.5">
-                <p className="text-sm font-semibold leading-none text-slate-900 dark:text-slate-100">{device?.name}</p>
+                <p className="text-sm font-semibold leading-none text-slate-900 dark:text-slate-100">
+                  {device?.name}
+                </p>
                 <p className="text-sm text-slate-600 dark:text-slate-400">
                   {device.macAddress?.toLowerCase()}
                 </p>
@@ -60,18 +63,13 @@ export default function DeviceList({
         </div>
       </Card>
       <div
-        className="flex items-center justify-end space-x-2 opacity-0 animate-fadeIn"
+        className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
         style={{
           animationDuration: "0.7s",
           animationDelay: "0.2s",
         }}
       >
-        <Button
-          size="SM"
-          theme="blank"
-          text="Close"
-          onClick={onCancelWakeOnLanModal}
-        />
+        <Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
         <Button
           size="SM"
           theme="primary"
diff --git a/ui/src/components/popovers/WakeOnLan/EmptyStateCard.tsx b/ui/src/components/popovers/WakeOnLan/EmptyStateCard.tsx
index 9c7967d..f6f97db 100644
--- a/ui/src/components/popovers/WakeOnLan/EmptyStateCard.tsx
+++ b/ui/src/components/popovers/WakeOnLan/EmptyStateCard.tsx
@@ -1,7 +1,8 @@
-import Card from "@components/Card";
 import { PlusCircleIcon } from "@heroicons/react/16/solid";
 import { LuPlus } from "react-icons/lu";
-import { Button } from "../../Button";
+
+import Card from "@/components/Card";
+import { Button } from "@/components/Button";
 
 export default function EmptyStateCard({
   onCancelWakeOnLanModal,
@@ -11,15 +12,15 @@ export default function EmptyStateCard({
   setShowAddForm: (show: boolean) => void;
 }) {
   return (
-    <div className="space-y-4 select-none">
-      <Card className="opacity-0 animate-fadeIn">
+    <div className="select-none space-y-4">
+      <Card className="animate-fadeIn opacity-0">
         <div className="flex items-center justify-center py-8 text-center">
           <div className="space-y-3">
             <div className="space-y-1">
               <div className="inline-block">
                 <Card>
                   <div className="p-1">
-                    <PlusCircleIcon className="w-4 h-4 text-blue-700 shrink-0 dark:text-white" />
+                    <PlusCircleIcon className="h-4 w-4 shrink-0 text-blue-700 dark:text-white" />
                   </div>
                 </Card>
               </div>
@@ -34,7 +35,7 @@ export default function EmptyStateCard({
         </div>
       </Card>
       <div
-        className="flex items-center justify-end space-x-2 opacity-0 animate-fadeIn"
+        className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
         style={{
           animationDuration: "0.7s",
           animationDelay: "0.2s",
diff --git a/ui/src/components/popovers/WakeOnLan/Index.tsx b/ui/src/components/popovers/WakeOnLan/Index.tsx
index 8b13a0f..3801461 100644
--- a/ui/src/components/popovers/WakeOnLan/Index.tsx
+++ b/ui/src/components/popovers/WakeOnLan/Index.tsx
@@ -1,10 +1,12 @@
+import { useCallback, useEffect, useState } from "react";
+import { useClose } from "@headlessui/react";
+
 import { GridCard } from "@components/Card";
 import { SettingsPageHeader } from "@components/SettingsPageheader";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
 import { useRTCStore, useUiStore } from "@/hooks/stores";
 import notifications from "@/notifications";
-import { useCallback, useEffect, useState } from "react";
-import { useClose } from "@headlessui/react";
+
 import EmptyStateCard from "./EmptyStateCard";
 import DeviceList, { StoredDevice } from "./DeviceList";
 import AddDeviceForm from "./AddDeviceForm";
@@ -99,7 +101,7 @@ export default function WakeOnLanModal() {
 
   return (
     <GridCard>
-      <div className="p-4 py-3 space-y-4">
+      <div className="space-y-4 p-4 py-3">
         <div className="grid h-full grid-rows-headerBody">
           <div className="space-y-4">
             <SettingsPageHeader
diff --git a/ui/src/components/sidebar/connectionStats.tsx b/ui/src/components/sidebar/connectionStats.tsx
index 82e8ab8..8e7234c 100644
--- a/ui/src/components/sidebar/connectionStats.tsx
+++ b/ui/src/components/sidebar/connectionStats.tsx
@@ -1,9 +1,10 @@
-import SidebarHeader from "@components/SidebarHeader";
-import { GridCard } from "@components/Card";
-import { useRTCStore, useUiStore } from "@/hooks/stores";
-import StatChart from "@components/StatChart";
 import { useInterval } from "usehooks-ts";
 
+import SidebarHeader from "@/components/SidebarHeader";
+import { GridCard } from "@/components/Card";
+import { useRTCStore, useUiStore } from "@/hooks/stores";
+import StatChart from "@/components/StatChart";
+
 function createChartArray<T, K extends keyof T>(
   stream: Map<number, T>,
   metric: K,
@@ -120,7 +121,7 @@ export default function ConnectionStatsSidebar() {
                 <GridCard>
                   <div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
                     {inboundRtpStats.size === 0 ? (
-                      <div className="flex flex-col items-center space-y-1 ">
+                      <div className="flex flex-col items-center space-y-1">
                         <p className="text-slate-700">Waiting for data...</p>
                       </div>
                     ) : isMetricSupported(inboundRtpStats, "packetsLost") ? (
@@ -130,7 +131,7 @@ export default function ConnectionStatsSidebar() {
                         unit=" packets"
                       />
                     ) : (
-                      <div className="flex flex-col items-center space-y-1 ">
+                      <div className="flex flex-col items-center space-y-1">
                         <p className="text-black">Metric not supported</p>
                       </div>
                     )}
@@ -149,7 +150,7 @@ export default function ConnectionStatsSidebar() {
                 <GridCard>
                   <div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
                     {inboundRtpStats.size === 0 ? (
-                      <div className="flex flex-col items-center space-y-1 ">
+                      <div className="flex flex-col items-center space-y-1">
                         <p className="text-slate-700">Waiting for data...</p>
                       </div>
                     ) : isMetricSupported(candidatePairStats, "currentRoundTripTime") ? (
@@ -167,7 +168,7 @@ export default function ConnectionStatsSidebar() {
                         unit=" ms"
                       />
                     ) : (
-                      <div className="flex flex-col items-center space-y-1 ">
+                      <div className="flex flex-col items-center space-y-1">
                         <p className="text-black">Metric not supported</p>
                       </div>
                     )}
@@ -186,7 +187,7 @@ export default function ConnectionStatsSidebar() {
                 <GridCard>
                   <div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
                     {inboundRtpStats.size === 0 ? (
-                      <div className="flex flex-col items-center space-y-1 ">
+                      <div className="flex flex-col items-center space-y-1">
                         <p className="text-slate-700">Waiting for data...</p>
                       </div>
                     ) : (
@@ -216,7 +217,7 @@ export default function ConnectionStatsSidebar() {
                 <GridCard>
                   <div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
                     {inboundRtpStats.size === 0 ? (
-                      <div className="flex flex-col items-center space-y-1 ">
+                      <div className="flex flex-col items-center space-y-1">
                         <p className="text-slate-700">Waiting for data...</p>
                       </div>
                     ) : (
diff --git a/ui/src/hooks/useAppNavigation.ts b/ui/src/hooks/useAppNavigation.ts
index b6fbb49..6c9270a 100644
--- a/ui/src/hooks/useAppNavigation.ts
+++ b/ui/src/hooks/useAppNavigation.ts
@@ -1,7 +1,8 @@
 import { useNavigate, useParams, NavigateOptions } from "react-router-dom";
-import { isOnDevice } from "../main";
 import { useCallback, useMemo } from "react";
 
+import { isOnDevice } from "../main";
+
 /**
  * Generates the correct path based on whether the app is running on device or in cloud mode
  *
diff --git a/ui/src/hooks/useFeatureFlag.ts b/ui/src/hooks/useFeatureFlag.ts
index 9a7fcd8..f4ef97f 100644
--- a/ui/src/hooks/useFeatureFlag.ts
+++ b/ui/src/hooks/useFeatureFlag.ts
@@ -1,5 +1,6 @@
 import { useContext } from "react";
-import { FeatureFlagContext } from "../providers/FeatureFlagProvider";
+
+import { FeatureFlagContext } from "@/providers/FeatureFlagContext";
 
 export const useFeatureFlag = (minAppVersion: string) => {
   const context = useContext(FeatureFlagContext);
diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts
index f3390f7..92b56ff 100644
--- a/ui/src/hooks/useJsonRpc.ts
+++ b/ui/src/hooks/useJsonRpc.ts
@@ -1,4 +1,5 @@
 import { useCallback, useEffect } from "react";
+
 import { useRTCStore } from "@/hooks/stores";
 
 export interface JsonRpcRequest {
@@ -56,7 +57,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
       // The "API" can also "request" data from the client
       // If the payload has a method, it's a request
       if ("method" in payload) {
-        onRequest && onRequest(payload);
+        if (onRequest) onRequest(payload);
         return;
       }
 
diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts
index cd7227f..137fc8b 100644
--- a/ui/src/hooks/useKeyboard.ts
+++ b/ui/src/hooks/useKeyboard.ts
@@ -1,4 +1,5 @@
 import { useCallback } from "react";
+
 import { useHidStore, useRTCStore } from "@/hooks/stores";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
 
diff --git a/ui/src/hooks/useResizeObserver.ts b/ui/src/hooks/useResizeObserver.ts
index 28c28c4..c74b152 100644
--- a/ui/src/hooks/useResizeObserver.ts
+++ b/ui/src/hooks/useResizeObserver.ts
@@ -1,19 +1,18 @@
 import { useEffect, useRef, useState } from "react";
-
 import type { RefObject } from "react";
 
 import { useIsMounted } from "./useIsMounted";
 
 /** The size of the observed element. */
-type Size = {
+interface Size {
   /** The width of the observed element. */
   width: number | undefined;
   /** The height of the observed element. */
   height: number | undefined;
-};
+}
 
 /** The options for the ResizeObserver. */
-type UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {
+interface UseResizeObserverOptions<T extends HTMLElement = HTMLElement> {
   /** The ref of the element to observe. */
   ref: RefObject<T>;
   /**
@@ -26,7 +25,7 @@ type UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {
    * @default 'content-box'
    */
   box?: "border-box" | "content-box" | "device-pixel-content-box";
-};
+}
 
 const initialSize: Size = {
   width: undefined,
@@ -102,7 +101,7 @@ export function useResizeObserver<T extends HTMLElement = HTMLElement>(
     return () => {
       observer.disconnect();
     };
-  }, [box, ref.current, isMounted]);
+  }, [box, isMounted, ref]);
 
   return { width, height };
 }
@@ -127,6 +126,6 @@ function extractSize(
 
   return Array.isArray(entry[box])
     ? entry[box][0][sizeType]
-    : // @ts-ignore Support Firefox's non-standard behavior
+    : // @ts-expect-error Support Firefox's non-standard behavior
       (entry[box][sizeType] as number);
 }
diff --git a/ui/src/main.tsx b/ui/src/main.tsx
index d20f018..066ee57 100644
--- a/ui/src/main.tsx
+++ b/ui/src/main.tsx
@@ -1,5 +1,4 @@
 import ReactDOM from "react-dom/client";
-import Root from "./root";
 import "./index.css";
 import {
   createBrowserRouter,
@@ -8,19 +7,22 @@ import {
   RouterProvider,
   useRouteError,
 } from "react-router-dom";
-import DeviceRoute, { LocalDevice } from "@routes/devices.$id";
-import DevicesRoute, { loader as DeviceListLoader } from "@routes/devices";
-import SetupRoute from "@routes/devices.$id.setup";
-import LoginRoute from "@routes/login";
-import SignupRoute from "@routes/signup";
-import AdoptRoute from "@routes/adopt";
-import DeviceIdRename from "@routes/devices.$id.rename";
-import DevicesIdDeregister from "@routes/devices.$id.deregister";
-import NotFoundPage from "@components/NotFoundPage";
-import EmptyCard from "@components/EmptyCard";
 import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
+
+import EmptyCard from "@components/EmptyCard";
+import NotFoundPage from "@components/NotFoundPage";
+import DevicesIdDeregister from "@routes/devices.$id.deregister";
+import DeviceIdRename from "@routes/devices.$id.rename";
+import AdoptRoute from "@routes/adopt";
+import SignupRoute from "@routes/signup";
+import LoginRoute from "@routes/login";
+import SetupRoute from "@routes/devices.$id.setup";
+import DevicesRoute, { loader as DeviceListLoader } from "@routes/devices";
+import DeviceRoute, { LocalDevice } from "@routes/devices.$id";
 import Card from "@components/Card";
 import DevicesAlreadyAdopted from "@routes/devices.already-adopted";
+
+import Root from "./root";
 import Notifications from "./notifications";
 import LoginLocalRoute from "./routes/login-local";
 import WelcomeLocalModeRoute from "./routes/welcome-local.mode";
diff --git a/ui/src/notifications.tsx b/ui/src/notifications.tsx
index 9505f2a..5677f42 100644
--- a/ui/src/notifications.tsx
+++ b/ui/src/notifications.tsx
@@ -1,8 +1,9 @@
 import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
 import React, { useEffect } from "react";
+import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
+
 import Card from "@/components/Card";
 
-import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
 
 interface NotificationOptions {
   duration?: number;
diff --git a/ui/src/providers/FeatureFlagContext.tsx b/ui/src/providers/FeatureFlagContext.tsx
new file mode 100644
index 0000000..2b2723c
--- /dev/null
+++ b/ui/src/providers/FeatureFlagContext.tsx
@@ -0,0 +1,10 @@
+import { createContext } from "react";
+
+import { FeatureFlagContextType } from "./FeatureFlagProvider";
+
+// Create the context
+
+export const FeatureFlagContext = createContext<FeatureFlagContextType>({
+  appVersion: null,
+  isFeatureEnabled: () => false,
+});
diff --git a/ui/src/providers/FeatureFlagProvider.tsx b/ui/src/providers/FeatureFlagProvider.tsx
index f4637f6..bbf9c54 100644
--- a/ui/src/providers/FeatureFlagProvider.tsx
+++ b/ui/src/providers/FeatureFlagProvider.tsx
@@ -1,17 +1,12 @@
-import { createContext } from "react";
 import semver from "semver";
 
-interface FeatureFlagContextType {
+import { FeatureFlagContext } from "./FeatureFlagContext";
+
+export interface FeatureFlagContextType {
   appVersion: string | null;
   isFeatureEnabled: (minVersion: string) => boolean;
 }
 
-// Create the context
-export const FeatureFlagContext = createContext<FeatureFlagContextType>({
-  appVersion: null,
-  isFeatureEnabled: () => false,
-});
-
 // Provider component that fetches version and provides context
 export const FeatureFlagProvider = ({
   children,
diff --git a/ui/src/routes/adopt.tsx b/ui/src/routes/adopt.tsx
index 8aa7555..8b8325b 100644
--- a/ui/src/routes/adopt.tsx
+++ b/ui/src/routes/adopt.tsx
@@ -1,7 +1,9 @@
 import { LoaderFunctionArgs, redirect } from "react-router-dom";
-import api from "../api";
+
 import { DEVICE_API } from "@/ui.config";
 
+import api from "../api";
+
 export interface CloudState {
   connected: boolean;
   url: string;
diff --git a/ui/src/routes/devices.$id.deregister.tsx b/ui/src/routes/devices.$id.deregister.tsx
index d8a2d77..40cf6a9 100644
--- a/ui/src/routes/devices.$id.deregister.tsx
+++ b/ui/src/routes/devices.$id.deregister.tsx
@@ -6,6 +6,8 @@ import {
   useActionData,
   useLoaderData,
 } from "react-router-dom";
+import { ChevronLeftIcon } from "@heroicons/react/16/solid";
+
 import { Button, LinkButton } from "@components/Button";
 import Card from "@components/Card";
 import { CardHeader } from "@components/CardHeader";
@@ -13,7 +15,6 @@ import DashboardNavbar from "@components/Header";
 import { User } from "@/hooks/stores";
 import { checkAuth } from "@/main";
 import Fieldset from "@components/Fieldset";
-import { ChevronLeftIcon } from "@heroicons/react/16/solid";
 import { CLOUD_API } from "@/ui.config";
 
 interface LoaderData {
@@ -36,6 +37,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
       return { message: "There was an error renaming your device. Please try again." };
     }
   } catch (e) {
+    console.error(e);
     return { message: "There was an error renaming your device. Please try again." };
   }
 
diff --git a/ui/src/routes/devices.$id.mount.tsx b/ui/src/routes/devices.$id.mount.tsx
index 42be090..74fcae2 100644
--- a/ui/src/routes/devices.$id.mount.tsx
+++ b/ui/src/routes/devices.$id.mount.tsx
@@ -1,15 +1,4 @@
-import Card, { GridCard } from "@/components/Card";
 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { Button } from "@components/Button";
-import LogoBlueIcon from "@/assets/logo-blue.svg";
-import LogoWhiteIcon from "@/assets/logo-white.svg";
-import {
-  MountMediaState,
-  RemoteVirtualMediaState,
-  useMountMediaStore,
-  useRTCStore,
-} from "../hooks/stores";
-import { cx } from "../cva.config";
 import {
   LuGlobe,
   LuLink,
@@ -18,8 +7,15 @@ import {
   LuCheck,
   LuUpload,
 } from "react-icons/lu";
+import { PlusCircleIcon , ExclamationTriangleIcon } from "@heroicons/react/20/solid";
+import { TrashIcon } from "@heroicons/react/16/solid";
+import { useNavigate } from "react-router-dom";
+
+import Card, { GridCard } from "@/components/Card";
+import { Button } from "@components/Button";
+import LogoBlueIcon from "@/assets/logo-blue.svg";
+import LogoWhiteIcon from "@/assets/logo-white.svg";
 import { formatters } from "@/utils";
-import { PlusCircleIcon } from "@heroicons/react/20/solid";
 import AutoHeight from "@components/AutoHeight";
 import { InputFieldWithLabel } from "@/components/InputField";
 import DebianIcon from "@/assets/debian-icon.png";
@@ -28,14 +24,20 @@ import FedoraIcon from "@/assets/fedora-icon.png";
 import OpenSUSEIcon from "@/assets/opensuse-icon.png";
 import ArchIcon from "@/assets/arch-icon.png";
 import NetBootIcon from "@/assets/netboot-icon.svg";
-import { TrashIcon } from "@heroicons/react/16/solid";
-import { useJsonRpc } from "../hooks/useJsonRpc";
-import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
-import notifications from "../notifications";
 import Fieldset from "@/components/Fieldset";
-import { isOnDevice } from "../main";
 import { DEVICE_API } from "@/ui.config";
-import { useNavigate } from "react-router-dom";
+
+import { useJsonRpc } from "../hooks/useJsonRpc";
+import notifications from "../notifications";
+import { isOnDevice } from "../main";
+import { cx } from "../cva.config";
+import {
+  MountMediaState,
+  RemoteVirtualMediaState,
+  useMountMediaStore,
+  useRTCStore,
+} from "../hooks/stores";
+
 
 export default function MountRoute() {
   const navigate = useNavigate();
diff --git a/ui/src/routes/devices.$id.other-session.tsx b/ui/src/routes/devices.$id.other-session.tsx
index 28bd3c1..2805666 100644
--- a/ui/src/routes/devices.$id.other-session.tsx
+++ b/ui/src/routes/devices.$id.other-session.tsx
@@ -1,4 +1,5 @@
 import { useNavigate, useOutletContext } from "react-router-dom";
+
 import { GridCard } from "@/components/Card";
 import { Button } from "@components/Button";
 import LogoBlue from "@/assets/logo-blue.svg";
diff --git a/ui/src/routes/devices.$id.rename.tsx b/ui/src/routes/devices.$id.rename.tsx
index 06458e0..8a4b194 100644
--- a/ui/src/routes/devices.$id.rename.tsx
+++ b/ui/src/routes/devices.$id.rename.tsx
@@ -6,8 +6,9 @@ import {
   useActionData,
   useLoaderData,
 } from "react-router-dom";
-import { Button, LinkButton } from "@components/Button";
 import { ChevronLeftIcon } from "@heroicons/react/16/solid";
+
+import { Button, LinkButton } from "@components/Button";
 import Card from "@components/Card";
 import { CardHeader } from "@components/CardHeader";
 import { InputFieldWithLabel } from "@components/InputField";
@@ -15,9 +16,10 @@ import DashboardNavbar from "@components/Header";
 import { User } from "@/hooks/stores";
 import { checkAuth } from "@/main";
 import Fieldset from "@components/Fieldset";
-import api from "../api";
 import { CLOUD_API } from "@/ui.config";
 
+import api from "../api";
+
 interface LoaderData {
   device: { id: string; name: string; user: { googleId: string } };
   user: User;
@@ -39,6 +41,7 @@ const action = async ({ params, request }: ActionFunctionArgs) => {
       return { message: "There was an error renaming your device. Please try again." };
     }
   } catch (e) {
+    console.error(e);
     return { message: "There was an error renaming your device. Please try again." };
   }
 
@@ -80,9 +83,9 @@ export default function DeviceIdRename() {
         picture={user?.picture}
       />
 
-      <div className="w-full h-full">
+      <div className="h-full w-full">
         <div className="mt-4">
-          <div className="w-full h-full px-4 mx-auto space-y-6 sm:max-w-6xl sm:px-8 md:max-w-7xl md:px-12 lg:max-w-8xl">
+          <div className="mx-auto h-full w-full space-y-6 px-4 sm:max-w-6xl sm:px-8 md:max-w-7xl md:px-12 lg:max-w-8xl">
             <div className="space-y-4">
               <LinkButton
                 size="SM"
@@ -100,7 +103,7 @@ export default function DeviceIdRename() {
 
                   <Fieldset>
                     <Form method="POST" className="max-w-sm space-y-4">
-                      <div className="relative group">
+                      <div className="group relative">
                         <InputFieldWithLabel
                           label="New device name"
                           type="text"
diff --git a/ui/src/routes/devices.$id.settings._index.tsx b/ui/src/routes/devices.$id.settings._index.tsx
index 54cee9a..603efec 100644
--- a/ui/src/routes/devices.$id.settings._index.tsx
+++ b/ui/src/routes/devices.$id.settings._index.tsx
@@ -1,4 +1,5 @@
 import { LoaderFunctionArgs, redirect } from "react-router-dom";
+
 import { getDeviceUiPath } from "../hooks/useAppNavigation";
 
 export function loader({ params }: LoaderFunctionArgs) {
diff --git a/ui/src/routes/devices.$id.settings.access._index.tsx b/ui/src/routes/devices.$id.settings.access._index.tsx
index dd5502b..0ed5862 100644
--- a/ui/src/routes/devices.$id.settings.access._index.tsx
+++ b/ui/src/routes/devices.$id.settings.access._index.tsx
@@ -1,20 +1,22 @@
-import { SettingsPageHeader } from "@components/SettingsPageheader";
-import { SettingsItem } from "./devices.$id.settings";
 import { useLoaderData, useNavigate } from "react-router-dom";
-import { Button, LinkButton } from "../components/Button";
-import { DEVICE_API } from "../ui.config";
-import api from "../api";
-import { LocalDevice } from "./devices.$id";
-import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
-import { GridCard } from "../components/Card";
 import { ShieldCheckIcon } from "@heroicons/react/24/outline";
-import notifications from "../notifications";
 import { useCallback, useEffect, useState } from "react";
-import { useJsonRpc } from "../hooks/useJsonRpc";
-import { InputFieldWithLabel } from "../components/InputField";
-import { SelectMenuBasic } from "../components/SelectMenuBasic";
-import { SettingsSectionHeader } from "../components/SettingsSectionHeader";
-import { isOnDevice } from "../main";
+
+import api from "@/api";
+import { SettingsPageHeader } from "@components/SettingsPageheader";
+import { GridCard } from "@/components/Card";
+import { Button, LinkButton } from "@/components/Button";
+import { InputFieldWithLabel } from "@/components/InputField";
+import { SelectMenuBasic } from "@/components/SelectMenuBasic";
+import { SettingsSectionHeader } from "@/components/SettingsSectionHeader";
+import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
+import notifications from "@/notifications";
+import { DEVICE_API } from "@/ui.config";
+import { useJsonRpc } from "@/hooks/useJsonRpc";
+import { isOnDevice } from "@/main";
+
+import { LocalDevice } from "./devices.$id";
+import { SettingsItem } from "./devices.$id.settings";
 import { CloudState } from "./adopt";
 
 export const loader = async () => {
diff --git a/ui/src/routes/devices.$id.settings.access.local-auth.tsx b/ui/src/routes/devices.$id.settings.access.local-auth.tsx
index 4f07bd8..50b2cc4 100644
--- a/ui/src/routes/devices.$id.settings.access.local-auth.tsx
+++ b/ui/src/routes/devices.$id.settings.access.local-auth.tsx
@@ -1,9 +1,10 @@
 import { useState, useEffect } from "react";
+import { useLocation, useRevalidator } from "react-router-dom";
+
 import { Button } from "@components/Button";
 import { InputFieldWithLabel } from "@/components/InputField";
 import api from "@/api";
 import { useLocalAuthModalStore } from "@/hooks/stores";
-import { useLocation, useRevalidator } from "react-router-dom";
 import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
 
 export default function SecurityAccessLocalAuthRoute() {
@@ -53,6 +54,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
         setError(data.error || "An error occurred while setting the password");
       }
     } catch (error) {
+      console.error(error);
       setError("An error occurred while setting the password");
     }
   };
@@ -92,6 +94,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
         setError(data.error || "An error occurred while changing the password");
       }
     } catch (error) {
+      console.error(error);
       setError("An error occurred while changing the password");
     }
   };
@@ -113,6 +116,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
         setError(data.error || "An error occurred while disabling the password");
       }
     } catch (error) {
+      console.error(error);
       setError("An error occurred while disabling the password");
     }
   };
diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx
index 6178bd5..927178e 100644
--- a/ui/src/routes/devices.$id.settings.advanced.tsx
+++ b/ui/src/routes/devices.$id.settings.advanced.tsx
@@ -1,16 +1,19 @@
-import { SettingsItem } from "./devices.$id.settings";
+
+import { useCallback, useState, useEffect } from "react";
+
+import { GridCard } from "@components/Card";
 
 import { SettingsPageHeader } from "../components/SettingsPageheader";
 import Checkbox from "../components/Checkbox";
-
 import { useJsonRpc } from "../hooks/useJsonRpc";
-import { useCallback, useState, useEffect } from "react";
 import notifications from "../notifications";
 import { TextAreaWithLabel } from "../components/TextArea";
 import { isOnDevice } from "../main";
 import { Button } from "../components/Button";
 import { useSettingsStore } from "../hooks/stores";
-import { GridCard } from "@components/Card";
+
+
+import { SettingsItem } from "./devices.$id.settings";
 
 export default function SettingsAdvancedRoute() {
   const [send] = useJsonRpc();
diff --git a/ui/src/routes/devices.$id.settings.appearance.tsx b/ui/src/routes/devices.$id.settings.appearance.tsx
index 11a9536..9baba00 100644
--- a/ui/src/routes/devices.$id.settings.appearance.tsx
+++ b/ui/src/routes/devices.$id.settings.appearance.tsx
@@ -1,6 +1,8 @@
 import { useCallback, useState } from "react";
+
 import { SettingsPageHeader } from "../components/SettingsPageheader";
 import { SelectMenuBasic } from "../components/SelectMenuBasic";
+
 import { SettingsItem } from "./devices.$id.settings";
 
 export default function SettingsAppearanceRoute() {
diff --git a/ui/src/routes/devices.$id.settings.general._index.tsx b/ui/src/routes/devices.$id.settings.general._index.tsx
index b5701f9..6d1d0ce 100644
--- a/ui/src/routes/devices.$id.settings.general._index.tsx
+++ b/ui/src/routes/devices.$id.settings.general._index.tsx
@@ -1,15 +1,17 @@
-import { SettingsPageHeader } from "../components/SettingsPageheader";
 
-import { SettingsItem } from "./devices.$id.settings";
-import { useState } from "react";
-import { useEffect } from "react";
+import { useState , useEffect } from "react";
+
 import { useJsonRpc } from "@/hooks/useJsonRpc";
+
+import { SettingsPageHeader } from "../components/SettingsPageheader";
 import { Button } from "../components/Button";
 import notifications from "../notifications";
 import Checkbox from "../components/Checkbox";
 import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
 import { useDeviceStore } from "../hooks/stores";
 
+import { SettingsItem } from "./devices.$id.settings";
+
 export default function SettingsGeneralRoute() {
   const [send] = useJsonRpc();
   const { navigateTo } = useDeviceUiNavigation();
diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx
index f2b52d6..c62b784 100644
--- a/ui/src/routes/devices.$id.settings.general.update.tsx
+++ b/ui/src/routes/devices.$id.settings.general.update.tsx
@@ -1,11 +1,12 @@
 import { useLocation, useNavigate } from "react-router-dom";
-import Card from "@/components/Card";
 import { useCallback, useEffect, useRef, useState } from "react";
+import { CheckCircleIcon } from "@heroicons/react/20/solid";
+
+import Card from "@/components/Card";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
 import { Button } from "@components/Button";
 import { UpdateState, useDeviceStore, useUpdateStore } from "@/hooks/stores";
 import notifications from "@/notifications";
-import { CheckCircleIcon } from "@heroicons/react/20/solid";
 import LoadingSpinner from "@/components/LoadingSpinner";
 import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
 
diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx
index d9d3919..9fde3e3 100644
--- a/ui/src/routes/devices.$id.settings.hardware.tsx
+++ b/ui/src/routes/devices.$id.settings.hardware.tsx
@@ -1,13 +1,14 @@
+import { useEffect } from "react";
+
 import { SettingsPageHeader } from "@components/SettingsPageheader";
 import { SettingsItem } from "@routes/devices.$id.settings";
 import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
-import { useEffect } from "react";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
+import { SelectMenuBasic } from "@components/SelectMenuBasic";
+import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
 
 import notifications from "../notifications";
-import { SelectMenuBasic } from "@components/SelectMenuBasic";
 import { UsbInfoSetting } from "../components/UsbInfoSetting";
-import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
 import { FeatureFlag } from "../components/FeatureFlag";
 
 export default function SettingsHardwareRoute() {
diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx
index 1d3a6cd..d6223d0 100644
--- a/ui/src/routes/devices.$id.settings.mouse.tsx
+++ b/ui/src/routes/devices.$id.settings.mouse.tsx
@@ -1,3 +1,6 @@
+import { CheckCircleIcon } from "@heroicons/react/16/solid";
+import { useCallback, useEffect, useState } from "react";
+
 import MouseIcon from "@/assets/mouse-icon.svg";
 import PointingFinger from "@/assets/pointing-finger.svg";
 import { GridCard } from "@/components/Card";
@@ -6,11 +9,11 @@ import { useDeviceSettingsStore, useSettingsStore } from "@/hooks/stores";
 import { useJsonRpc } from "@/hooks/useJsonRpc";
 import notifications from "@/notifications";
 import { SettingsPageHeader } from "@components/SettingsPageheader";
-import { CheckCircleIcon } from "@heroicons/react/16/solid";
-import { useCallback, useEffect, useState } from "react";
+
 import { FeatureFlag } from "../components/FeatureFlag";
 import { SelectMenuBasic } from "../components/SelectMenuBasic";
 import { useFeatureFlag } from "../hooks/useFeatureFlag";
+
 import { SettingsItem } from "./devices.$id.settings";
 
 type ScrollSensitivity = "low" | "default" | "high";
diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx
index 1a8de03..4742445 100644
--- a/ui/src/routes/devices.$id.settings.tsx
+++ b/ui/src/routes/devices.$id.settings.tsx
@@ -1,5 +1,4 @@
 import { NavLink, Outlet, useLocation } from "react-router-dom";
-import Card from "@/components/Card";
 import {
   LuSettings,
   LuKeyboard,
@@ -10,8 +9,11 @@ import {
   LuArrowLeft,
   LuPalette,
 } from "react-icons/lu";
-import { LinkButton } from "../components/Button";
 import React, { useEffect, useRef, useState } from "react";
+
+import Card from "@/components/Card";
+
+import { LinkButton } from "../components/Button";
 import { cx } from "../cva.config";
 import { useUiStore } from "../hooks/stores";
 import useKeyboard from "../hooks/useKeyboard";
diff --git a/ui/src/routes/devices.$id.settings.video.tsx b/ui/src/routes/devices.$id.settings.video.tsx
index 3dd65fe..07472a3 100644
--- a/ui/src/routes/devices.$id.settings.video.tsx
+++ b/ui/src/routes/devices.$id.settings.video.tsx
@@ -1,12 +1,14 @@
-import { SettingsPageHeader } from "@components/SettingsPageheader";
-import { SettingsItem } from "./devices.$id.settings";
+import { useState, useEffect } from "react";
+
 import { Button } from "@/components/Button";
 import { TextAreaWithLabel } from "@/components/TextArea";
-
 import { useJsonRpc } from "@/hooks/useJsonRpc";
-import { useState, useEffect } from "react";
+import { SettingsPageHeader } from "@components/SettingsPageheader";
+
 import notifications from "../notifications";
 import { SelectMenuBasic } from "../components/SelectMenuBasic";
+
+import { SettingsItem } from "./devices.$id.settings";
 const defaultEdid =
   "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b";
 const edids = [
diff --git a/ui/src/routes/devices.$id.setup.tsx b/ui/src/routes/devices.$id.setup.tsx
index 3113f9c..f157ed5 100644
--- a/ui/src/routes/devices.$id.setup.tsx
+++ b/ui/src/routes/devices.$id.setup.tsx
@@ -1,8 +1,3 @@
-import SimpleNavbar from "@components/SimpleNavbar";
-import GridBackground from "@components/GridBackground";
-import Container from "@components/Container";
-import StepCounter from "@components/StepCounter";
-import Fieldset from "@components/Fieldset";
 import {
   ActionFunctionArgs,
   Form,
@@ -12,12 +7,19 @@ import {
   useParams,
   useSearchParams,
 } from "react-router-dom";
+
+import SimpleNavbar from "@components/SimpleNavbar";
+import GridBackground from "@components/GridBackground";
+import Container from "@components/Container";
+import StepCounter from "@components/StepCounter";
+import Fieldset from "@components/Fieldset";
 import { InputFieldWithLabel } from "@components/InputField";
 import { Button } from "@components/Button";
 import { checkAuth } from "@/main";
-import api from "../api";
 import { CLOUD_API } from "@/ui.config";
 
+import api from "../api";
+
 const loader = async ({ params }: LoaderFunctionArgs) => {
   await checkAuth();
   const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx
index b454f73..05b0322 100644
--- a/ui/src/routes/devices.$id.tsx
+++ b/ui/src/routes/devices.$id.tsx
@@ -1,4 +1,20 @@
 import { useCallback, useEffect, useRef, useState } from "react";
+import {
+  LoaderFunctionArgs,
+  Outlet,
+  Params,
+  redirect,
+  useLoaderData,
+  useLocation,
+  useNavigate,
+  useOutlet,
+  useParams,
+  useSearchParams,
+} from "react-router-dom";
+import { useInterval } from "usehooks-ts";
+import FocusTrap from "focus-trap-react";
+import { motion, AnimatePresence } from "framer-motion";
+
 import { cx } from "@/cva.config";
 import {
   DeviceSettingsState,
@@ -16,36 +32,23 @@ import {
   VideoState,
 } from "@/hooks/stores";
 import WebRTCVideo from "@components/WebRTCVideo";
-import {
-  LoaderFunctionArgs,
-  Outlet,
-  Params,
-  redirect,
-  useLoaderData,
-  useLocation,
-  useNavigate,
-  useOutlet,
-  useParams,
-  useSearchParams,
-} from "react-router-dom";
 import { checkAuth, isInCloud, isOnDevice } from "@/main";
 import DashboardNavbar from "@components/Header";
-import { useInterval } from "usehooks-ts";
 import ConnectionStatsSidebar from "@/components/sidebar/connectionStats";
 import { JsonRpcRequest, useJsonRpc } from "@/hooks/useJsonRpc";
-import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard";
-import api from "../api";
-import { DeviceStatus } from "./welcome-local";
-import FocusTrap from "focus-trap-react";
 import Terminal from "@components/Terminal";
 import { CLOUD_API, DEVICE_API } from "@/ui.config";
+
+import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard";
+import api from "../api";
 import Modal from "../components/Modal";
-import { motion, AnimatePresence } from "motion/react";
 import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
 import { FeatureFlagProvider } from "../providers/FeatureFlagProvider";
-import { SystemVersionInfo } from "./devices.$id.settings.general.update";
 import notifications from "../notifications";
 
+import { SystemVersionInfo } from "./devices.$id.settings.general.update";
+import { DeviceStatus } from "./welcome-local";
+
 interface LocalLoaderResp {
   authMode: "password" | "noPassword" | null;
 }
diff --git a/ui/src/routes/devices.tsx b/ui/src/routes/devices.tsx
index 664956d..df384cb 100644
--- a/ui/src/routes/devices.tsx
+++ b/ui/src/routes/devices.tsx
@@ -1,4 +1,6 @@
 import { useLoaderData, useRevalidator } from "react-router-dom";
+import { LuMonitorSmartphone } from "react-icons/lu";
+import { ArrowRightIcon } from "@heroicons/react/16/solid";
 
 import DashboardNavbar from "@components/Header";
 import { LinkButton } from "@components/Button";
@@ -7,8 +9,6 @@ import useInterval from "@/hooks/useInterval";
 import { checkAuth } from "@/main";
 import { User } from "@/hooks/stores";
 import EmptyCard from "@components/EmptyCard";
-import { LuMonitorSmartphone } from "react-icons/lu";
-import { ArrowRightIcon } from "@heroicons/react/16/solid";
 import { CLOUD_API } from "@/ui.config";
 
 interface LoaderData {
@@ -49,8 +49,8 @@ export default function DevicesRoute() {
         />
 
         <div className="flex h-full overflow-hidden">
-          <div className="w-full h-full px-4 mx-auto space-y-6 sm:max-w-6xl sm:px-8 md:max-w-7xl md:px-12 lg:max-w-8xl">
-            <div className="flex items-center justify-between pb-4 mt-8 border-b border-b-slate-800/20 dark:border-b-slate-300/20">
+          <div className="mx-auto h-full w-full space-y-6 px-4 sm:max-w-6xl sm:px-8 md:max-w-7xl md:px-12 lg:max-w-8xl">
+            <div className="mt-8 flex items-center justify-between border-b border-b-slate-800/20 pb-4 dark:border-b-slate-300/20">
               <div>
                 <h1 className="text-xl font-bold text-black dark:text-white">
                   Cloud KVMs
diff --git a/ui/src/routes/login-local.tsx b/ui/src/routes/login-local.tsx
index 17e19b4..19d3f95 100644
--- a/ui/src/routes/login-local.tsx
+++ b/ui/src/routes/login-local.tsx
@@ -1,19 +1,22 @@
+import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
+import { useState } from "react";
+import { LuEye, LuEyeOff } from "react-icons/lu";
+
 import SimpleNavbar from "@components/SimpleNavbar";
 import GridBackground from "@components/GridBackground";
 import Container from "@components/Container";
 import Fieldset from "@components/Fieldset";
-import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
 import { InputFieldWithLabel } from "@components/InputField";
 import { Button } from "@components/Button";
-import { useState } from "react";
-import { LuEye, LuEyeOff } from "react-icons/lu";
 import LogoBlueIcon from "@/assets/logo-blue.png";
 import LogoWhiteIcon from "@/assets/logo-white.svg";
-import api from "../api";
-import { DeviceStatus } from "./welcome-local";
-import ExtLink from "../components/ExtLink";
 import { DEVICE_API } from "@/ui.config";
 
+import api from "../api";
+import ExtLink from "../components/ExtLink";
+
+import { DeviceStatus } from "./welcome-local";
+
 const loader = async () => {
   const res = await api
     .GET(`${DEVICE_API}/device/status`)
@@ -31,12 +34,9 @@ const action = async ({ request }: ActionFunctionArgs) => {
   const password = formData.get("password");
 
   try {
-    const response = await api.POST(
-      `${DEVICE_API}/auth/login-local`,
-      {
-        password,
-      },
-    );
+    const response = await api.POST(`${DEVICE_API}/auth/login-local`, {
+      password,
+    });
 
     if (response.ok) {
       return redirect("/");
@@ -44,6 +44,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
       return { error: "Invalid password" };
     }
   } catch (error) {
+    console.error(error);
     return { error: "An error occurred while logging in" };
   }
 };
@@ -58,22 +59,28 @@ export default function LoginLocalRoute() {
       <div className="grid min-h-screen grid-rows-layout">
         <SimpleNavbar />
         <Container>
-          <div className="flex items-center justify-center w-full h-full isolate">
-            <div className="max-w-2xl -mt-32 space-y-8">
+          <div className="isolate flex h-full w-full items-center justify-center">
+            <div className="-mt-32 max-w-2xl space-y-8">
               <div className="flex items-center justify-center">
-                <img src={LogoWhiteIcon} alt="" className="-ml-4 h-[32px] hidden dark:block" />
+                <img
+                  src={LogoWhiteIcon}
+                  alt=""
+                  className="-ml-4 hidden h-[32px] dark:block"
+                />
                 <img src={LogoBlueIcon} alt="" className="-ml-4 h-[32px] dark:hidden" />
               </div>
 
               <div className="space-y-2 text-center">
-                <h1 className="text-4xl font-semibold text-black dark:text-white">Welcome back to JetKVM</h1>
+                <h1 className="text-4xl font-semibold text-black dark:text-white">
+                  Welcome back to JetKVM
+                </h1>
                 <p className="font-medium text-slate-600 dark:text-slate-400">
                   Enter your password to access your JetKVM.
                 </p>
               </div>
 
               <Fieldset className="space-y-12">
-                <Form method="POST" className="max-w-sm mx-auto space-y-4">
+                <Form method="POST" className="mx-auto max-w-sm space-y-4">
                   <div className="space-y-4">
                     <InputFieldWithLabel
                       label="Password"
@@ -88,14 +95,14 @@ export default function LoginLocalRoute() {
                             onClick={() => setShowPassword(false)}
                             className="pointer-events-auto"
                           >
-                            <LuEye className="w-4 h-4 cursor-pointer text-slate-500 dark:text-slate-400" />
+                            <LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
                           </div>
                         ) : (
                           <div
                             onClick={() => setShowPassword(true)}
                             className="pointer-events-auto"
                           >
-                            <LuEyeOff className="w-4 h-4 cursor-pointer text-slate-500 dark:text-slate-400" />
+                            <LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
                           </div>
                         )
                       }
@@ -111,7 +118,7 @@ export default function LoginLocalRoute() {
                     textAlign="center"
                   />
 
-                  <div className="flex justify-start mt-4 text-xs text-slate-500 dark:text-slate-400">
+                  <div className="mt-4 flex justify-start text-xs text-slate-500 dark:text-slate-400">
                     <ExtLink
                       href="https://jetkvm.com/docs/networking/local-access#reset-password"
                       className="hover:underline"
diff --git a/ui/src/routes/login.tsx b/ui/src/routes/login.tsx
index 149561e..e2347a7 100644
--- a/ui/src/routes/login.tsx
+++ b/ui/src/routes/login.tsx
@@ -1,6 +1,7 @@
-import AuthLayout from "@components/AuthLayout";
 import { useLocation, useSearchParams } from "react-router-dom";
 
+import AuthLayout from "@components/AuthLayout";
+
 export default function LoginRoute() {
   const [sq] = useSearchParams();
   const location = useLocation();
diff --git a/ui/src/routes/signup.tsx b/ui/src/routes/signup.tsx
index 6c0a4b5..af06400 100644
--- a/ui/src/routes/signup.tsx
+++ b/ui/src/routes/signup.tsx
@@ -1,6 +1,7 @@
-import AuthLayout from "@components/AuthLayout";
 import { useLocation, useSearchParams } from "react-router-dom";
 
+import AuthLayout from "@components/AuthLayout";
+
 export default function SignupRoute() {
   const [sq] = useSearchParams();
   const location = useLocation();
diff --git a/ui/src/routes/welcome-local.mode.tsx b/ui/src/routes/welcome-local.mode.tsx
index 3ea54a7..ffe0ead 100644
--- a/ui/src/routes/welcome-local.mode.tsx
+++ b/ui/src/routes/welcome-local.mode.tsx
@@ -1,15 +1,19 @@
+import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
+import { useState } from "react";
+
 import GridBackground from "@components/GridBackground";
 import Container from "@components/Container";
-import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
 import { Button } from "@components/Button";
-import { useState } from "react";
-import { GridCard } from "../components/Card";
 import LogoBlueIcon from "@/assets/logo-blue.png";
 import LogoWhiteIcon from "@/assets/logo-white.svg";
+import { DEVICE_API } from "@/ui.config";
+
+import { GridCard } from "../components/Card";
 import { cx } from "../cva.config";
 import api from "../api";
+
 import { DeviceStatus } from "./welcome-local";
-import { DEVICE_API } from "@/ui.config";
+
 
 const loader = async () => {
   const res = await api
diff --git a/ui/src/routes/welcome-local.password.tsx b/ui/src/routes/welcome-local.password.tsx
index 0d6542b..7bb934f 100644
--- a/ui/src/routes/welcome-local.password.tsx
+++ b/ui/src/routes/welcome-local.password.tsx
@@ -1,17 +1,20 @@
+import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
+import { useState, useRef, useEffect } from "react";
+import { LuEye, LuEyeOff } from "react-icons/lu";
+
 import GridBackground from "@components/GridBackground";
 import Container from "@components/Container";
 import Fieldset from "@components/Fieldset";
-import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
 import { InputFieldWithLabel } from "@components/InputField";
 import { Button } from "@components/Button";
-import { useState, useRef, useEffect } from "react";
-import { LuEye, LuEyeOff } from "react-icons/lu";
 import LogoBlueIcon from "@/assets/logo-blue.png";
 import LogoWhiteIcon from "@/assets/logo-white.svg";
-import api from "../api";
-import { DeviceStatus } from "./welcome-local";
 import { DEVICE_API } from "@/ui.config";
 
+import api from "../api";
+
+import { DeviceStatus } from "./welcome-local";
+
 const loader = async () => {
   const res = await api
     .GET(`${DEVICE_API}/device/status`)
diff --git a/ui/src/routes/welcome-local.tsx b/ui/src/routes/welcome-local.tsx
index 9ce6203..803d538 100644
--- a/ui/src/routes/welcome-local.tsx
+++ b/ui/src/routes/welcome-local.tsx
@@ -1,4 +1,7 @@
 import { useEffect, useState } from "react";
+import { cx } from "cva";
+import { redirect } from "react-router-dom";
+
 import GridBackground from "@components/GridBackground";
 import Container from "@components/Container";
 import { LinkButton } from "@components/Button";
@@ -6,11 +9,12 @@ import LogoBlueIcon from "@/assets/logo-blue.png";
 import LogoWhiteIcon from "@/assets/logo-white.svg";
 import DeviceImage from "@/assets/jetkvm-device-still.png";
 import LogoMark from "@/assets/logo-mark.png";
-import { cx } from "cva";
-import api from "../api";
-import { redirect } from "react-router-dom";
 import { DEVICE_API } from "@/ui.config";
 
+import api from "../api";
+
+
+
 export interface DeviceStatus {
   isSetup: boolean;
 }
diff --git a/ui/src/utils.ts b/ui/src/utils.ts
index efd00fd..99c1a50 100644
--- a/ui/src/utils.ts
+++ b/ui/src/utils.ts
@@ -51,8 +51,8 @@ export const formatters = {
     ];
 
     let duration = (date.valueOf() - new Date().valueOf()) / 1000;
-    for (let i = 0; i < DIVISIONS.length; i++) {
-      const division = DIVISIONS[i];
+
+    for (const division of DIVISIONS) {
       if (Math.abs(duration) < division.amount) {
         return relativeTimeFormat.format(Math.round(duration), division.name);
       }
@@ -61,7 +61,7 @@ export const formatters = {
   },
 
   price: (price: number | bigint | string, options?: Intl.NumberFormatOptions) => {
-    let opts: Intl.NumberFormatOptions = {
+    const opts: Intl.NumberFormatOptions = {
       style: "currency",
       currency: "USD",
       ...(options || {}),

From a3580b5465b1f0c62eee05e1b7f6b38b115f2a06 Mon Sep 17 00:00:00 2001
From: Adam Shiervani <adam.shiervani@gmail.com>
Date: Tue, 25 Mar 2025 14:54:04 +0100
Subject: [PATCH 08/17] Improve error handling when `RTCPeerConnection` throws
 (#289)

* fix(WebRTC): improve error handling during peer connection creation and add connection error overlay

* refactor: update peer connection state handling and improve type definitions across components
---
 ui/src/components/Header.tsx                  |  2 +-
 .../components/PeerConnectionStatusCard.tsx   | 11 +++---
 ui/src/components/USBStateStatus.tsx          | 10 +++---
 ui/src/routes/devices.$id.tsx                 | 36 ++++++++++---------
 4 files changed, 34 insertions(+), 25 deletions(-)

diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx
index 452a19c..cdbc3c4 100644
--- a/ui/src/components/Header.tsx
+++ b/ui/src/components/Header.tsx
@@ -36,7 +36,7 @@ export default function DashboardNavbar({
   picture,
   kvmName,
 }: NavbarProps) {
-  const peerConnectionState = useRTCStore(state => state.peerConnection?.connectionState);
+  const peerConnectionState = useRTCStore(state => state.peerConnectionState);
   const setUser = useUserStore(state => state.setUser);
   const navigate = useNavigate();
   const onLogout = useCallback(async () => {
diff --git a/ui/src/components/PeerConnectionStatusCard.tsx b/ui/src/components/PeerConnectionStatusCard.tsx
index 07e91cd..98025cd 100644
--- a/ui/src/components/PeerConnectionStatusCard.tsx
+++ b/ui/src/components/PeerConnectionStatusCard.tsx
@@ -9,19 +9,22 @@ const PeerConnectionStatusMap = {
   failed: "Connection failed",
   closed: "Closed",
   new: "Connecting",
-};
+} as Record<RTCPeerConnectionState | "error" | "closing", string>;
 
 export type PeerConnections = keyof typeof PeerConnectionStatusMap;
 
-type StatusProps = Record<PeerConnections, {
+type StatusProps = Record<
+  PeerConnections,
+  {
     statusIndicatorClassName: string;
-  }>;
+  }
+>;
 
 export default function PeerConnectionStatusCard({
   state,
   title,
 }: {
-  state?: PeerConnections;
+  state?: RTCPeerConnectionState | null;
   title?: string;
 }) {
   if (!state) return null;
diff --git a/ui/src/components/USBStateStatus.tsx b/ui/src/components/USBStateStatus.tsx
index d8e86c6..8feb458 100644
--- a/ui/src/components/USBStateStatus.tsx
+++ b/ui/src/components/USBStateStatus.tsx
@@ -8,11 +8,14 @@ import { HidState } from "@/hooks/stores";
 
 type USBStates = HidState["usbState"];
 
-type StatusProps = Record<USBStates, {
+type StatusProps = Record<
+  USBStates,
+  {
     icon: React.FC<{ className: string | undefined }>;
     iconClassName: string;
     statusIndicatorClassName: string;
-  }>;
+  }
+>;
 
 const USBStateMap: Record<USBStates, string> = {
   configured: "Connected",
@@ -27,9 +30,8 @@ export default function USBStateStatus({
   peerConnectionState,
 }: {
   state: USBStates;
-  peerConnectionState?: RTCPeerConnectionState;
+  peerConnectionState: RTCPeerConnectionState | null;
 }) {
-
   const StatusCardProps: StatusProps = {
     configured: {
       icon: ({ className }) => (
diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx
index 05b0322..50fc79f 100644
--- a/ui/src/routes/devices.$id.tsx
+++ b/ui/src/routes/devices.$id.tsx
@@ -126,7 +126,7 @@ export default function KvmIdRoute() {
 
   const setIsTurnServerInUse = useRTCStore(state => state.setTurnServerInUse);
   const peerConnection = useRTCStore(state => state.peerConnection);
-
+  const peerConnectionState = useRTCStore(state => state.peerConnectionState);
   const setPeerConnectionState = useRTCStore(state => state.setPeerConnectionState);
   const setMediaMediaStream = useRTCStore(state => state.setMediaStream);
   const setPeerConnection = useRTCStore(state => state.setPeerConnection);
@@ -153,6 +153,7 @@ export default function KvmIdRoute() {
       // However, the onconnectionstatechange event doesn't fire when close() is called manually
       // So we need to explicitly update our state to maintain consistency
       // I don't know why this is happening, but this is the best way I can think of to handle it
+      // ALSO, this will render the connection error overlay linking to docs
       setPeerConnectionState("closed");
     },
     [peerConnection, setPeerConnectionState],
@@ -255,12 +256,19 @@ export default function KvmIdRoute() {
     setStartedConnectingAt(new Date());
     setConnectedAt(null);
 
-    const pc = new RTCPeerConnection({
-      // We only use STUN or TURN servers if we're in the cloud
-      ...(isInCloud && iceConfig?.iceServers
-        ? { iceServers: [iceConfig?.iceServers] }
-        : {}),
-    });
+    let pc: RTCPeerConnection;
+    try {
+      pc = new RTCPeerConnection({
+        // We only use STUN or TURN servers if we're in the cloud
+        ...(isInCloud && iceConfig?.iceServers
+          ? { iceServers: [iceConfig?.iceServers] }
+          : {}),
+      });
+    } catch (e) {
+      console.error(`Error creating peer connection: ${e}`);
+      closePeerConnection();
+      return;
+    }
 
     // Set up event listeners and data channels
     pc.onconnectionstatechange = () => {
@@ -296,8 +304,10 @@ export default function KvmIdRoute() {
       setPeerConnection(pc);
     } catch (e) {
       console.error(`Error creating offer: ${e}`);
+      closePeerConnection();
     }
   }, [
+    closePeerConnection,
     iceConfig?.iceServers,
     sdp,
     setDiskChannel,
@@ -315,9 +325,8 @@ export default function KvmIdRoute() {
     if (location.pathname.includes("other-session")) return;
 
     // If we're already connected or connecting, we don't need to connect
-    if (
-      ["connected", "connecting", "new"].includes(peerConnection?.connectionState ?? "")
-    ) {
+    // We have to use the state from the store, because the peerConnection.connectionState doesnt trigger a value change, if called manually from .close()
+    if (["connected", "connecting", "new"].includes(peerConnectionState ?? "")) {
       return;
     }
 
@@ -331,12 +340,7 @@ export default function KvmIdRoute() {
       connectWebRTC();
     }, 3000);
     return () => clearInterval(interval);
-  }, [
-    connectWebRTC,
-    connectionFailed,
-    location.pathname,
-    peerConnection?.connectionState,
-  ]);
+  }, [connectWebRTC, connectionFailed, location.pathname, peerConnectionState]);
 
   // On boot, if the connection state is undefined, we connect to the WebRTC
   useEffect(() => {

From b5e0f894bc678e7a407af9d3545d77a9e03a8219 Mon Sep 17 00:00:00 2001
From: Siyuan Miao <i@xswan.net>
Date: Tue, 25 Mar 2025 14:53:59 +0100
Subject: [PATCH 09/17] chore: move smoketest to private repo

---
 .github/workflows/build.yml     | 107 +---------------------------
 .github/workflows/smoketest.yml | 122 ++++++++++++++++++++++++++++++++
 2 files changed, 124 insertions(+), 105 deletions(-)
 create mode 100644 .github/workflows/smoketest.yml

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 84bc4b1..b31041f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -12,6 +12,7 @@ jobs:
   build:
     runs-on: buildjet-4vcpu-ubuntu-2204
     name: Build
+    if: "github.event.review.state == 'approved' || github.event.event_type != 'pull_request_review'"
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -35,108 +36,4 @@ jobs:
         uses: actions/upload-artifact@v4
         with:
           name: jetkvm-app
-          path: bin/jetkvm_app
-  deploy_and_test:
-    runs-on: buildjet-4vcpu-ubuntu-2204
-    name: Smoke test
-    needs: build
-    concurrency:
-      group: smoketest-jk
-    steps:
-      - name: Download artifact
-        uses: actions/download-artifact@v4
-        with:
-          name: jetkvm-app
-      - name: Configure WireGuard and check connectivity
-        run: |
-          WG_KEY_FILE=$(mktemp)
-          echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \
-          sudo apt-get update && sudo apt-get install -y wireguard-tools && \
-          sudo ip link add dev wg-ci type wireguard && \
-          sudo ip addr add $CI_WG_IPS dev wg-ci && \
-          sudo wg set wg-ci listen-port 51820 \
-            private-key $WG_KEY_FILE \
-            peer $CI_WG_PUBLIC \
-            allowed-ips $CI_WG_ALLOWED_IPS \
-            endpoint $CI_WG_ENDPOINT \
-            persistent-keepalive 15 && \
-          sudo ip link set up dev wg-ci && \
-          sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci
-          ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1)
-        env:
-          CI_HOST: ${{ vars.JETKVM_CI_HOST }}
-          CI_WG_IPS: ${{ vars.JETKVM_CI_WG_IPS }}
-          CI_WG_GATEWAY: ${{ vars.JETKVM_CI_GATEWAY }}
-          CI_WG_ALLOWED_IPS: ${{ vars.JETKVM_CI_WG_ALLOWED_IPS }}
-          CI_WG_PUBLIC: ${{ secrets.JETKVM_CI_WG_PUBLIC }}
-          CI_WG_PRIVATE: ${{ secrets.JETKVM_CI_WG_PRIVATE }}
-          CI_WG_ENDPOINT: ${{ secrets.JETKVM_CI_WG_ENDPOINT }}
-      - name: Configure SSH
-        run: |
-          # Write SSH private key to a file
-          SSH_PRIVATE_KEY=$(mktemp)
-          echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY
-          chmod 0600 $SSH_PRIVATE_KEY
-          # Configure SSH
-          mkdir -p ~/.ssh
-          cat <<EOF >> ~/.ssh/config
-          Host jkci
-            HostName $CI_HOST
-            User $CI_USER
-            StrictHostKeyChecking no
-            UserKnownHostsFile /dev/null
-            IdentityFile $SSH_PRIVATE_KEY
-          EOF
-        env:
-          CI_USER: ${{ vars.JETKVM_CI_USER }}
-          CI_HOST: ${{ vars.JETKVM_CI_HOST }}
-          CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }}
-      - name: Deploy application
-        run: |
-          set -e
-          # Copy the binary to the remote host
-          echo "+ Copying the application to the remote host"
-          cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz"
-          # Deploy and run the application on the remote host
-          echo "+ Deploying the application on the remote host"
-          ssh jkci ash <<EOF
-          # Extract the binary
-          gzip -d /userdata/jetkvm/jetkvm_app.update.gz
-          # Flush filesystem buffers to ensure all data is written to disk
-          sync
-          # Clear the filesystem caches to force a read from disk
-          echo 1 > /proc/sys/vm/drop_caches
-          # Reboot the application
-          reboot -d 5 -f &
-          EOF
-          sleep 10
-          echo "Deployment complete, waiting for JetKVM to come back online "
-          function check_online() {
-            for i in {1..60}; do
-                if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then
-                    echo "JetKVM is back online"
-                    return 0
-                fi
-                echo -n "."
-                sleep 1
-            done
-            echo "JetKVM did not come back online within 60 seconds"
-            return 1
-          }
-          check_online
-        env:
-          CI_HOST: ${{ vars.JETKVM_CI_HOST }}
-      - name: Run smoke tests
-        run: |
-          echo "+ Checking the status of the device"
-          curl -v http://$CI_HOST/device/status && echo
-          echo "+ Collecting logs"
-          ssh jkci "cat /userdata/jetkvm/last.log" > last.log
-          cat last.log
-        env:
-          CI_HOST: ${{ vars.JETKVM_CI_HOST }}
-      - name: Upload logs
-        uses: actions/upload-artifact@v4
-        with:
-          name: device-logs
-          path: last.log
+          path: bin/jetkvm_app
\ No newline at end of file
diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml
new file mode 100644
index 0000000..d5493e7
--- /dev/null
+++ b/.github/workflows/smoketest.yml
@@ -0,0 +1,122 @@
+name: smoketest
+on:
+  repository_dispatch:
+    types: [smoketest]
+
+jobs:
+  ghbot_payload:
+    name: Ghbot payload
+    runs-on: ubuntu-latest
+    steps:
+      - name: "GH_CHECK_RUN_ID=${{ github.event.client_payload.check_run_id }}"
+        run: |
+          echo "== START GHBOT_PAYLOAD =="
+          cat <<'GHPAYLOAD_EOF' | base64
+          ${{ toJson(github.event.client_payload) }}
+          GHPAYLOAD_EOF
+          echo "== END GHBOT_PAYLOAD =="
+  deploy_and_test:
+    runs-on: buildjet-4vcpu-ubuntu-2204
+    name: Smoke test
+    concurrency:
+      group: smoketest-jk
+    steps:
+      - name: Download artifact
+        run: |
+          wget -O /tmp/jk.zip "${{ github.event.client_payload.artifact_download_url }}"
+          unzip /tmp/jk.zip
+      - name: Configure WireGuard and check connectivity
+        run: |
+          WG_KEY_FILE=$(mktemp)
+          echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \
+          sudo apt-get update && sudo apt-get install -y wireguard-tools && \
+          sudo ip link add dev wg-ci type wireguard && \
+          sudo ip addr add $CI_WG_IPS dev wg-ci && \
+          sudo wg set wg-ci listen-port 51820 \
+            private-key $WG_KEY_FILE \
+            peer $CI_WG_PUBLIC \
+            allowed-ips $CI_WG_ALLOWED_IPS \
+            endpoint $CI_WG_ENDPOINT \
+            persistent-keepalive 15 && \
+          sudo ip link set up dev wg-ci && \
+          sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci
+          ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1)
+        env:
+          CI_HOST: ${{ vars.JETKVM_CI_HOST }}
+          CI_WG_IPS: ${{ vars.JETKVM_CI_WG_IPS }}
+          CI_WG_GATEWAY: ${{ vars.JETKVM_CI_GATEWAY }}
+          CI_WG_ALLOWED_IPS: ${{ vars.JETKVM_CI_WG_ALLOWED_IPS }}
+          CI_WG_PUBLIC: ${{ secrets.JETKVM_CI_WG_PUBLIC }}
+          CI_WG_PRIVATE: ${{ secrets.JETKVM_CI_WG_PRIVATE }}
+          CI_WG_ENDPOINT: ${{ secrets.JETKVM_CI_WG_ENDPOINT }}
+      - name: Configure SSH
+        run: |
+          # Write SSH private key to a file
+          SSH_PRIVATE_KEY=$(mktemp)
+          echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY
+          chmod 0600 $SSH_PRIVATE_KEY
+          # Configure SSH
+          mkdir -p ~/.ssh
+          cat <<EOF >> ~/.ssh/config
+          Host jkci
+            HostName $CI_HOST
+            User $CI_USER
+            StrictHostKeyChecking no
+            UserKnownHostsFile /dev/null
+            IdentityFile $SSH_PRIVATE_KEY
+          EOF
+        env:
+          CI_USER: ${{ vars.JETKVM_CI_USER }}
+          CI_HOST: ${{ vars.JETKVM_CI_HOST }}
+          CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }}
+      - name: Deploy application
+        run: |
+          set -e
+          # Copy the binary to the remote host
+          echo "+ Copying the application to the remote host"
+          cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz"
+          # Deploy and run the application on the remote host
+          echo "+ Deploying the application on the remote host"
+          ssh jkci ash <<EOF
+          # Extract the binary
+          gzip -d /userdata/jetkvm/jetkvm_app.update.gz
+          # Flush filesystem buffers to ensure all data is written to disk
+          sync
+          # Clear the filesystem caches to force a read from disk
+          echo 1 > /proc/sys/vm/drop_caches
+          # Reboot the application
+          reboot -d 5 -f &
+          EOF
+          sleep 10
+          echo "Deployment complete, waiting for JetKVM to come back online "
+          function check_online() {
+            for i in {1..60}; do
+                if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then
+                    echo "JetKVM is back online"
+                    return 0
+                fi
+                echo -n "."
+                sleep 1
+            done
+            echo "JetKVM did not come back online within 60 seconds"
+            return 1
+          }
+          check_online
+        env:
+          CI_HOST: ${{ vars.JETKVM_CI_HOST }}
+      - name: Run smoke tests
+        run: |
+          echo "+ Checking the status of the device"
+          curl -v http://$CI_HOST/device/status && echo
+          echo "+ Waiting for 10 seconds to allow all services to start"
+          sleep 10
+          echo "+ Collecting logs"
+          ssh jkci "cat /userdata/jetkvm/last.log" > last.log
+          cat last.log
+        env:
+          CI_HOST: ${{ vars.JETKVM_CI_HOST }}
+      - name: Upload logs
+        uses: actions/upload-artifact@v4
+        with:
+          name: device-logs
+          path: last.log

From aed453cc8cf7da19cd26a97f4cf1bf7b1291ac7b Mon Sep 17 00:00:00 2001
From: SuperQ <superq@gmail.com>
Date: Wed, 12 Mar 2025 16:29:45 +0100
Subject: [PATCH 10/17] chore: Enable more linters

Enable more golangci-lint linters.
* `forbidigo` to stop use of non-logger console printing.
* `goimports` to make sure `import` blocks are formatted nicely.
* `misspell` to catch spelling mistakes.
* `whitespace` to catch whitespace issues.

Signed-off-by: SuperQ <superq@gmail.com>
---
 .golangci.yml | 14 ++++++++++++--
 prometheus.go |  4 ----
 serial.go     |  1 -
 web.go        |  2 +-
 web_tls.go    |  8 ++++----
 5 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/.golangci.yml b/.golangci.yml
index ddf4443..95a1cb8 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,12 +1,22 @@
 ---
 linters:
   enable:
-  # - goimports
-  # - misspell
+  - forbidigo
+  - goimports
+  - misspell
   # - revive
+  - whitespace
 
 issues:
   exclude-rules:
     - path: _test.go
       linters:
         - errcheck
+
+linters-settings:
+  forbidigo:
+    forbid:
+      - p: ^fmt\.Print.*$
+        msg: Do not commit print statements. Use logger package.
+      - p: ^log\.(Fatal|Panic|Print)(f|ln)?.*$
+        msg: Do not commit log statements. Use logger package.
diff --git a/prometheus.go b/prometheus.go
index 8ebf259..5d4c5e7 100644
--- a/prometheus.go
+++ b/prometheus.go
@@ -1,15 +1,11 @@
 package kvm
 
 import (
-	"net/http"
-
 	"github.com/prometheus/client_golang/prometheus"
 	versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version"
 	"github.com/prometheus/common/version"
 )
 
-var promHandler http.Handler
-
 func initPrometheus() {
 	// A Prometheus metrics endpoint.
 	version.Version = builtAppVersion
diff --git a/serial.go b/serial.go
index a4ab7d5..31fd553 100644
--- a/serial.go
+++ b/serial.go
@@ -66,7 +66,6 @@ func runATXControl() {
 			newLedPWRState != ledPWRState ||
 			newBtnRSTState != btnRSTState ||
 			newBtnPWRState != btnPWRState {
-
 			logger.Debugf("Status changed: HDD LED: %v, PWR LED: %v, RST BTN: %v, PWR BTN: %v",
 				newLedHDDState, newLedPWRState, newBtnRSTState, newBtnPWRState)
 
diff --git a/web.go b/web.go
index b35a2db..9201e7b 100644
--- a/web.go
+++ b/web.go
@@ -16,6 +16,7 @@ import (
 	"golang.org/x/crypto/bcrypt"
 )
 
+//nolint:typecheck
 //go:embed all:static
 var staticFiles embed.FS
 
@@ -419,7 +420,6 @@ func handleSetup(c *gin.Context) {
 
 		// Set the cookie
 		c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true)
-
 	} else {
 		// For noPassword mode, ensure the password field is empty
 		config.HashedPassword = ""
diff --git a/web_tls.go b/web_tls.go
index fff9253..976cff6 100644
--- a/web_tls.go
+++ b/web_tls.go
@@ -8,10 +8,10 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/pem"
-	"log"
 	"math/big"
 	"net"
 	"net/http"
+	"os"
 	"strings"
 	"sync"
 	"time"
@@ -38,7 +38,7 @@ func RunWebSecureServer() {
 		TLSConfig: &tls.Config{
 			// TODO: cache certificate in persistent storage
 			GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
-				hostname := WebSecureSelfSignedDefaultDomain
+				var hostname string
 				if info.ServerName != "" {
 					hostname = info.ServerName
 				} else {
@@ -58,7 +58,6 @@ func RunWebSecureServer() {
 	if err != nil {
 		panic(err)
 	}
-	return
 }
 
 func createSelfSignedCert(hostname string) *tls.Certificate {
@@ -72,7 +71,8 @@ func createSelfSignedCert(hostname string) *tls.Certificate {
 
 	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 	if err != nil {
-		log.Fatalf("Failed to generate private key: %v", err)
+		logger.Errorf("Failed to generate private key: %v", err)
+		os.Exit(1)
 	}
 	keyUsage := x509.KeyUsageDigitalSignature
 

From df0d083a28552a0d0a9e693cfb7a91fe0062f28f Mon Sep 17 00:00:00 2001
From: Cameron Fleming <cameron@cpfleming.co.uk>
Date: Sat, 29 Mar 2025 21:13:59 +0000
Subject: [PATCH 11/17] chore: Update README Discord Link

Corrects Discord link in the help section.
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 1b516d7..5d0e9d7 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ We welcome contributions from the community! Whether it's improving the firmware
 
 ## I need help
 
-The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://discord.gg/8MaAhua7NW).
+The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://jetkvm.com/discord).
 
 ## I want to report an issue
 

From 1e9adf81d433a23202be881de7a8cb4cbfca9953 Mon Sep 17 00:00:00 2001
From: Siyuan Miao <i@xswan.net>
Date: Thu, 3 Apr 2025 18:16:41 +0200
Subject: [PATCH 12/17] chore: skip websocket client if net isn't up or time
 sync hasn't complete

---
 cloud.go | 26 +++++++++++++++++++++-----
 main.go  |  8 +++-----
 ntp.go   | 33 +++++++++++++++++++++++++++++++++
 3 files changed, 57 insertions(+), 10 deletions(-)

diff --git a/cloud.go b/cloud.go
index a30a14c..4b9c2b4 100644
--- a/cloud.go
+++ b/cloud.go
@@ -90,11 +90,6 @@ func handleCloudRegister(c *gin.Context) {
 		return
 	}
 
-	if config.CloudToken == "" {
-		cloudLogger.Info("Starting websocket client due to adoption")
-		go RunWebsocketClient()
-	}
-
 	config.CloudToken = tokenResp.SecretToken
 
 	provider, err := oidc.NewProvider(c, "https://accounts.google.com")
@@ -130,6 +125,7 @@ func runWebsocketClient() error {
 		time.Sleep(5 * time.Second)
 		return fmt.Errorf("cloud token is not set")
 	}
+
 	wsURL, err := url.Parse(config.CloudURL)
 	if err != nil {
 		return fmt.Errorf("failed to parse config.CloudURL: %w", err)
@@ -253,6 +249,26 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
 
 func RunWebsocketClient() {
 	for {
+		// If the cloud token is not set, we don't need to run the websocket client.
+		if config.CloudToken == "" {
+			time.Sleep(5 * time.Second)
+			continue
+		}
+
+		// If the network is not up, well, we can't connect to the cloud.
+		if !networkState.Up {
+			cloudLogger.Warn("waiting for network to be up, will retry in 3 seconds")
+			time.Sleep(3 * time.Second)
+			continue
+		}
+
+		// If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail.
+		if isTimeSyncNeeded() && !timeSyncSuccess {
+			cloudLogger.Warn("system time is not synced, will retry in 3 seconds")
+			time.Sleep(3 * time.Second)
+			continue
+		}
+
 		err := runWebsocketClient()
 		if err != nil {
 			cloudLogger.Errorf("websocket client error: %v", err)
diff --git a/main.go b/main.go
index 6a55595..aeb3d85 100644
--- a/main.go
+++ b/main.go
@@ -72,11 +72,9 @@ func Main() {
 	if config.TLSMode != "" {
 		go RunWebSecureServer()
 	}
-	// If the cloud token isn't set, the client won't be started by default.
-	// However, if the user adopts the device via the web interface, handleCloudRegister will start the client.
-	if config.CloudToken != "" {
-		go RunWebsocketClient()
-	}
+	// As websocket client already checks if the cloud token is set, we can start it here.
+	go RunWebsocketClient()
+
 	initSerialPort()
 	sigs := make(chan os.Signal, 1)
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
diff --git a/ntp.go b/ntp.go
index 39ea7af..27ec100 100644
--- a/ntp.go
+++ b/ntp.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/http"
 	"os/exec"
+	"strconv"
 	"time"
 
 	"github.com/beevik/ntp"
@@ -20,13 +21,41 @@ const (
 )
 
 var (
+	builtTimestamp        string
 	timeSyncRetryInterval = 0 * time.Second
+	timeSyncSuccess       = false
 	defaultNTPServers     = []string{
 		"time.cloudflare.com",
 		"time.apple.com",
 	}
 )
 
+func isTimeSyncNeeded() bool {
+	if builtTimestamp == "" {
+		logger.Warnf("Built timestamp is not set, time sync is needed")
+		return true
+	}
+
+	ts, err := strconv.Atoi(builtTimestamp)
+	if err != nil {
+		logger.Warnf("Failed to parse built timestamp: %v", err)
+		return true
+	}
+
+	// builtTimestamp is UNIX timestamp in seconds
+	builtTime := time.Unix(int64(ts), 0)
+	now := time.Now()
+
+	logger.Tracef("Built time: %v, now: %v", builtTime, now)
+
+	if now.Sub(builtTime) < 0 {
+		logger.Warnf("System time is behind the built time, time sync is needed")
+		return true
+	}
+
+	return false
+}
+
 func TimeSyncLoop() {
 	for {
 		if !networkState.checked {
@@ -40,6 +69,9 @@ func TimeSyncLoop() {
 			continue
 		}
 
+		// check if time sync is needed, but do nothing for now
+		isTimeSyncNeeded()
+
 		logger.Infof("Syncing system time")
 		start := time.Now()
 		err := SyncSystemTime()
@@ -56,6 +88,7 @@ func TimeSyncLoop() {
 
 			continue
 		}
+		timeSyncSuccess = true
 		logger.Infof("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
 		time.Sleep(timeSyncInterval) // after the first sync is done
 	}

From f3b5011d65ade31b34aad01a2c1e670582810f28 Mon Sep 17 00:00:00 2001
From: Siyuan Miao <i@xswan.net>
Date: Thu, 3 Apr 2025 19:06:21 +0200
Subject: [PATCH 13/17] feat(cloud): add metrics for cloud connections

---
 cloud.go | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 127 insertions(+)

diff --git a/cloud.go b/cloud.go
index 4b9c2b4..be53b08 100644
--- a/cloud.go
+++ b/cloud.go
@@ -10,6 +10,8 @@ import (
 	"time"
 
 	"github.com/coder/websocket/wsjson"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
 
 	"github.com/coreos/go-oidc/v3/oidc"
 
@@ -36,6 +38,97 @@ const (
 	CloudWebSocketPingInterval = 15 * time.Second
 )
 
+var (
+	metricCloudConnectionStatus = promauto.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "jetkvm_cloud_connection_status",
+			Help: "The status of the cloud connection",
+		},
+	)
+	metricCloudConnectionEstablishedTimestamp = promauto.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "jetkvm_cloud_connection_established_timestamp",
+			Help: "The timestamp when the cloud connection was established",
+		},
+	)
+	metricCloudConnectionLastPingTimestamp = promauto.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "jetkvm_cloud_connection_last_ping_timestamp",
+			Help: "The timestamp when the last ping response was received",
+		},
+	)
+	metricCloudConnectionLastPingDuration = promauto.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "jetkvm_cloud_connection_last_ping_duration",
+			Help: "The duration of the last ping response",
+		},
+	)
+	metricCloudConnectionPingDuration = promauto.NewHistogram(
+		prometheus.HistogramOpts{
+			Name: "jetkvm_cloud_connection_ping_duration",
+			Help: "The duration of the ping response",
+			Buckets: []float64{
+				0.1, 0.5, 1, 10,
+			},
+		},
+	)
+	metricCloudConnectionTotalPingCount = promauto.NewCounter(
+		prometheus.CounterOpts{
+			Name: "jetkvm_cloud_connection_total_ping_count",
+			Help: "The total number of pings sent to the cloud",
+		},
+	)
+	metricCloudConnectionSessionRequestCount = promauto.NewCounter(
+		prometheus.CounterOpts{
+			Name: "jetkvm_cloud_connection_session_total_request_count",
+			Help: "The total number of session requests received from the cloud",
+		},
+	)
+	metricCloudConnectionSessionRequestDuration = promauto.NewHistogram(
+		prometheus.HistogramOpts{
+			Name: "jetkvm_cloud_connection_session_request_duration",
+			Help: "The duration of session requests",
+			Buckets: []float64{
+				0.1, 0.5, 1, 10,
+			},
+		},
+	)
+	metricCloudConnectionLastSessionRequestTimestamp = promauto.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "jetkvm_cloud_connection_last_session_request_timestamp",
+			Help: "The timestamp of the last session request",
+		},
+	)
+	metricCloudConnectionLastSessionRequestDuration = promauto.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "jetkvm_cloud_connection_last_session_request_duration",
+			Help: "The duration of the last session request",
+		},
+	)
+	metricCloudConnectionFailureCount = promauto.NewCounter(
+		prometheus.CounterOpts{
+			Name: "jetkvm_cloud_connection_failure_count",
+			Help: "The number of times the cloud connection has failed",
+		},
+	)
+)
+
+func cloudResetMetrics(established bool) {
+	metricCloudConnectionLastPingTimestamp.Set(-1)
+	metricCloudConnectionLastPingDuration.Set(-1)
+
+	metricCloudConnectionLastSessionRequestTimestamp.Set(-1)
+	metricCloudConnectionLastSessionRequestDuration.Set(-1)
+
+	if established {
+		metricCloudConnectionEstablishedTimestamp.SetToCurrentTime()
+		metricCloudConnectionStatus.Set(1)
+	} else {
+		metricCloudConnectionEstablishedTimestamp.Set(-1)
+		metricCloudConnectionStatus.Set(-1)
+	}
+}
+
 func handleCloudRegister(c *gin.Context) {
 	var req CloudRegisterRequest
 
@@ -130,15 +223,18 @@ func runWebsocketClient() error {
 	if err != nil {
 		return fmt.Errorf("failed to parse config.CloudURL: %w", err)
 	}
+
 	if wsURL.Scheme == "http" {
 		wsURL.Scheme = "ws"
 	} else {
 		wsURL.Scheme = "wss"
 	}
+
 	header := http.Header{}
 	header.Set("X-Device-ID", GetDeviceID())
 	header.Set("Authorization", "Bearer "+config.CloudToken)
 	dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout)
+
 	defer cancelDial()
 	c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
 		HTTPHeader: header,
@@ -148,17 +244,35 @@ func runWebsocketClient() error {
 	}
 	defer c.CloseNow() //nolint:errcheck
 	cloudLogger.Infof("websocket connected to %s", wsURL)
+
+	// set the metrics when we successfully connect to the cloud.
+	cloudResetMetrics(true)
+
 	runCtx, cancelRun := context.WithCancel(context.Background())
 	defer cancelRun()
 	go func() {
 		for {
 			time.Sleep(CloudWebSocketPingInterval)
+
+			// set the timer for the ping duration
+			timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
+				metricCloudConnectionLastPingDuration.Set(v)
+				metricCloudConnectionPingDuration.Observe(v)
+			}))
+
 			err := c.Ping(runCtx)
+
 			if err != nil {
 				cloudLogger.Warnf("websocket ping error: %v", err)
 				cancelRun()
 				return
 			}
+
+			// dont use `defer` here because we want to observe the duration of the ping
+			timer.ObserveDuration()
+
+			metricCloudConnectionTotalPingCount.Inc()
+			metricCloudConnectionLastPingTimestamp.SetToCurrentTime()
 		}
 	}()
 	for {
@@ -180,6 +294,8 @@ func runWebsocketClient() error {
 		cloudLogger.Infof("new session request: %v", req.OidcGoogle)
 		cloudLogger.Tracef("session request info: %v", req)
 
+		metricCloudConnectionSessionRequestCount.Inc()
+		metricCloudConnectionLastSessionRequestTimestamp.SetToCurrentTime()
 		err = handleSessionRequest(runCtx, c, req)
 		if err != nil {
 			cloudLogger.Infof("error starting new session: %v", err)
@@ -189,6 +305,12 @@ func runWebsocketClient() error {
 }
 
 func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
+	timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
+		metricCloudConnectionLastSessionRequestDuration.Set(v)
+		metricCloudConnectionSessionRequestDuration.Observe(v)
+	}))
+	defer timer.ObserveDuration()
+
 	oidcCtx, cancelOIDC := context.WithTimeout(ctx, CloudOidcRequestTimeout)
 	defer cancelOIDC()
 	provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com")
@@ -249,6 +371,9 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
 
 func RunWebsocketClient() {
 	for {
+		// reset the metrics when we start the websocket client.
+		cloudResetMetrics(false)
+
 		// If the cloud token is not set, we don't need to run the websocket client.
 		if config.CloudToken == "" {
 			time.Sleep(5 * time.Second)
@@ -272,6 +397,8 @@ func RunWebsocketClient() {
 		err := runWebsocketClient()
 		if err != nil {
 			cloudLogger.Errorf("websocket client error: %v", err)
+			metricCloudConnectionStatus.Set(0)
+			metricCloudConnectionFailureCount.Inc()
 			time.Sleep(5 * time.Second)
 		}
 	}

From 8268b20f325abb23cdeb833204754856c0d82339 Mon Sep 17 00:00:00 2001
From: Adam Shiervani <adam.shiervani@gmail.com>
Date: Thu, 3 Apr 2025 19:32:14 +0200
Subject: [PATCH 14/17] refactor: Update WebRTC connection handling and
 overlays (#320)

* refactor: Update WebRTC connection handling and overlays

* fix: Update comments for WebRTC connection handling in KvmIdRoute

* chore: Clean up import statements in devices.$id.tsx
---
 ui/src/components/Header.tsx                |   6 +-
 ui/src/components/USBStateStatus.tsx        |   2 +-
 ui/src/components/VideoOverlay.tsx          |  67 ++++-
 ui/src/components/WebRTCVideo.tsx           | 101 +++++---
 ui/src/routes/devices.$id.other-session.tsx |   4 +-
 ui/src/routes/devices.$id.tsx               | 263 ++++++++++----------
 6 files changed, 263 insertions(+), 180 deletions(-)

diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx
index cdbc3c4..03a907e 100644
--- a/ui/src/components/Header.tsx
+++ b/ui/src/components/Header.tsx
@@ -36,7 +36,7 @@ export default function DashboardNavbar({
   picture,
   kvmName,
 }: NavbarProps) {
-  const peerConnectionState = useRTCStore(state => state.peerConnectionState);
+  const peerConnection = useRTCStore(state => state.peerConnection);
   const setUser = useUserStore(state => state.setUser);
   const navigate = useNavigate();
   const onLogout = useCallback(async () => {
@@ -82,14 +82,14 @@ export default function DashboardNavbar({
                 <div className="hidden items-center gap-x-2 md:flex">
                   <div className="w-[159px]">
                     <PeerConnectionStatusCard
-                      state={peerConnectionState}
+                      state={peerConnection?.connectionState}
                       title={kvmName}
                     />
                   </div>
                   <div className="hidden w-[159px] md:block">
                     <USBStateStatus
                       state={usbState}
-                      peerConnectionState={peerConnectionState}
+                      peerConnectionState={peerConnection?.connectionState}
                     />
                   </div>
                 </div>
diff --git a/ui/src/components/USBStateStatus.tsx b/ui/src/components/USBStateStatus.tsx
index 8feb458..f0b2cb2 100644
--- a/ui/src/components/USBStateStatus.tsx
+++ b/ui/src/components/USBStateStatus.tsx
@@ -30,7 +30,7 @@ export default function USBStateStatus({
   peerConnectionState,
 }: {
   state: USBStates;
-  peerConnectionState: RTCPeerConnectionState | null;
+  peerConnectionState?: RTCPeerConnectionState | null;
 }) {
   const StatusCardProps: StatusProps = {
     configured: {
diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx
index f13b6ce..0620af4 100644
--- a/ui/src/components/VideoOverlay.tsx
+++ b/ui/src/components/VideoOverlay.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
-import { ArrowRightIcon } from "@heroicons/react/16/solid";
+import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid";
 import { motion, AnimatePresence } from "framer-motion";
 import { LuPlay } from "react-icons/lu";
 
@@ -25,12 +25,12 @@ interface LoadingOverlayProps {
   show: boolean;
 }
 
-export function LoadingOverlay({ show }: LoadingOverlayProps) {
+export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
   return (
     <AnimatePresence>
       {show && (
         <motion.div
-          className="absolute inset-0 aspect-video h-full w-full"
+          className="aspect-video h-full w-full"
           initial={{ opacity: 0 }}
           animate={{ opacity: 1 }}
           exit={{ opacity: 0 }}
@@ -55,21 +55,59 @@ export function LoadingOverlay({ show }: LoadingOverlayProps) {
   );
 }
 
-interface ConnectionErrorOverlayProps {
+interface LoadingConnectionOverlayProps {
   show: boolean;
+  text: string;
 }
-
-export function ConnectionErrorOverlay({ show }: ConnectionErrorOverlayProps) {
+export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
   return (
     <AnimatePresence>
       {show && (
         <motion.div
-          className="absolute inset-0 z-10 aspect-video h-full w-full"
+          className="aspect-video h-full w-full"
           initial={{ opacity: 0 }}
           animate={{ opacity: 1 }}
-          exit={{ opacity: 0 }}
+          exit={{ opacity: 0, transition: { duration: 0 } }}
           transition={{
-            duration: 0.3,
+            duration: 0.4,
+            ease: "easeInOut",
+          }}
+        >
+          <OverlayContent>
+            <div className="flex flex-col items-center justify-center gap-y-1">
+              <div className="animate flex h-12 w-12 items-center justify-center">
+                <LoadingSpinner className="h-8 w-8 text-blue-800 dark:text-blue-200" />
+              </div>
+              <p className="text-center text-sm text-slate-700 dark:text-slate-300">
+                {text}
+              </p>
+            </div>
+          </OverlayContent>
+        </motion.div>
+      )}
+    </AnimatePresence>
+  );
+}
+
+interface ConnectionErrorOverlayProps {
+  show: boolean;
+  setupPeerConnection: () => Promise<void>;
+}
+
+export function ConnectionErrorOverlay({
+  show,
+  setupPeerConnection,
+}: ConnectionErrorOverlayProps) {
+  return (
+    <AnimatePresence>
+      {show && (
+        <motion.div
+          className="aspect-video h-full w-full"
+          initial={{ opacity: 0 }}
+          animate={{ opacity: 1 }}
+          exit={{ opacity: 0, transition: { duration: 0 } }}
+          transition={{
+            duration: 0.4,
             ease: "easeInOut",
           }}
         >
@@ -87,14 +125,21 @@ export function ConnectionErrorOverlay({ show }: ConnectionErrorOverlayProps) {
                       <li>Try restarting both the device and your computer</li>
                     </ul>
                   </div>
-                  <div>
+                  <div className="flex items-center gap-x-2">
                     <LinkButton
                       to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
-                      theme="light"
+                      theme="primary"
                       text="Troubleshooting Guide"
                       TrailingIcon={ArrowRightIcon}
                       size="SM"
                     />
+                    <Button
+                      onClick={() => setupPeerConnection()}
+                      LeadingIcon={ArrowPathIcon}
+                      text="Try again"
+                      size="SM"
+                      theme="light"
+                    />
                   </div>
                 </div>
               </div>
diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx
index 911c5ea..5d8fb55 100644
--- a/ui/src/components/WebRTCVideo.tsx
+++ b/ui/src/components/WebRTCVideo.tsx
@@ -19,9 +19,8 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
 
 import {
   HDMIErrorOverlay,
+  LoadingVideoOverlay,
   NoAutoplayPermissionsOverlay,
-  ConnectionErrorOverlay,
-  LoadingOverlay,
 } from "./VideoOverlay";
 
 export default function WebRTCVideo() {
@@ -46,15 +45,13 @@ export default function WebRTCVideo() {
 
   // RTC related states
   const peerConnection = useRTCStore(state => state.peerConnection);
-  const peerConnectionState = useRTCStore(state => state.peerConnectionState);
 
   // HDMI and UI states
   const hdmiState = useVideoStore(state => state.hdmiState);
   const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
-  const isLoading = !hdmiError && !isPlaying;
-  const isConnectionError = ["error", "failed", "disconnected", "closed"].includes(
-    peerConnectionState || "",
-  );
+  const isVideoLoading = !isPlaying;
+
+  // console.log("peerConnection?.connectionState", peerConnection?.connectionState);
 
   // Keyboard related states
   const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } =
@@ -379,25 +376,52 @@ export default function WebRTCVideo() {
     }
   }, []);
 
+  const addStreamToVideoElm = useCallback(
+    (mediaStream: MediaStream) => {
+      if (!videoElm.current) return;
+      const videoElmRefValue = videoElm.current;
+      console.log("Adding stream to video element", videoElmRefValue);
+      videoElmRefValue.srcObject = mediaStream;
+      updateVideoSizeStore(videoElmRefValue);
+    },
+    [updateVideoSizeStore],
+  );
+
+  useEffect(
+    function updateVideoStreamOnNewTrack() {
+      if (!peerConnection) return;
+      const abortController = new AbortController();
+      const signal = abortController.signal;
+
+      peerConnection.addEventListener(
+        "track",
+        (e: RTCTrackEvent) => {
+          console.log("Adding stream to video element");
+          addStreamToVideoElm(e.streams[0]);
+        },
+        { signal },
+      );
+
+      return () => {
+        abortController.abort();
+      };
+    },
+    [addStreamToVideoElm, peerConnection],
+  );
+
   useEffect(
     function updateVideoStream() {
       if (!mediaStream) return;
-      if (!videoElm.current) return;
-      if (peerConnection?.iceConnectionState !== "connected") return;
-
-      setTimeout(() => {
-        if (videoElm?.current) {
-          videoElm.current.srcObject = mediaStream;
-        }
-      }, 0);
-      updateVideoSizeStore(videoElm.current);
+      console.log("Updating video stream from mediaStream");
+      // We set the as early as possible
+      addStreamToVideoElm(mediaStream);
     },
     [
       setVideoClientSize,
-      setVideoSize,
       mediaStream,
       updateVideoSizeStore,
-      peerConnection?.iceConnectionState,
+      peerConnection,
+      addStreamToVideoElm,
     ],
   );
 
@@ -474,6 +498,8 @@ export default function WebRTCVideo() {
       const local = resetMousePosition;
       window.addEventListener("blur", local, { signal });
       document.addEventListener("visibilitychange", local, { signal });
+      const preventContextMenu = (e: MouseEvent) => e.preventDefault();
+      videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
 
       return () => {
         abortController.abort();
@@ -517,17 +543,17 @@ export default function WebRTCVideo() {
   );
 
   const hasNoAutoPlayPermissions = useMemo(() => {
-    if (peerConnectionState !== "connected") return false;
+    if (peerConnection?.connectionState !== "connected") return false;
     if (isPlaying) return false;
     if (hdmiError) return false;
     if (videoHeight === 0 || videoWidth === 0) return false;
     return true;
-  }, [peerConnectionState, isPlaying, hdmiError, videoHeight, videoWidth]);
+  }, [peerConnection?.connectionState, isPlaying, hdmiError, videoHeight, videoWidth]);
 
   return (
     <div className="grid h-full w-full grid-rows-layout">
       <div className="min-h-[39.5px]">
-        <fieldset disabled={peerConnectionState !== "connected"}>
+        <fieldset disabled={peerConnection?.connectionState !== "connected"}>
           <Actionbar
             requestFullscreen={async () =>
               videoElm.current?.requestFullscreen({
@@ -575,28 +601,29 @@ export default function WebRTCVideo() {
                             "cursor-none":
                               settings.mouseMode === "absolute" &&
                               settings.isCursorHidden,
-                            "opacity-0": isLoading || isConnectionError || hdmiError,
+                            "opacity-0": isVideoLoading || hdmiError,
                             "animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
                               isPlaying,
                           },
                         )}
                       />
-                      <div
-                        style={{ animationDuration: "500ms" }}
-                        className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center opacity-0"
-                      >
-                        <div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
-                          <LoadingOverlay show={isLoading} />
-                          <ConnectionErrorOverlay show={isConnectionError} />
-                          <HDMIErrorOverlay show={hdmiError} hdmiState={hdmiState} />
-                          <NoAutoplayPermissionsOverlay
-                            show={hasNoAutoPlayPermissions}
-                            onPlayClick={() => {
-                              videoElm.current?.play();
-                            }}
-                          />
+                      {peerConnection?.connectionState == "connected" && (
+                        <div
+                          style={{ animationDuration: "500ms" }}
+                          className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center opacity-0"
+                        >
+                          <div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
+                            <LoadingVideoOverlay show={isVideoLoading} />
+                            <HDMIErrorOverlay show={hdmiError} hdmiState={hdmiState} />
+                            <NoAutoplayPermissionsOverlay
+                              show={hasNoAutoPlayPermissions}
+                              onPlayClick={() => {
+                                videoElm.current?.play();
+                              }}
+                            />
+                          </div>
                         </div>
-                      </div>
+                      )}
                     </div>
                   </div>
                   <VirtualKeyboard />
diff --git a/ui/src/routes/devices.$id.other-session.tsx b/ui/src/routes/devices.$id.other-session.tsx
index 2805666..16cb479 100644
--- a/ui/src/routes/devices.$id.other-session.tsx
+++ b/ui/src/routes/devices.$id.other-session.tsx
@@ -6,7 +6,7 @@ import LogoBlue from "@/assets/logo-blue.svg";
 import LogoWhite from "@/assets/logo-white.svg";
 
 interface ContextType {
-  connectWebRTC: () => Promise<void>;
+  setupPeerConnection: () => Promise<void>;
 }
 /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
 
@@ -16,7 +16,7 @@ export default function OtherSessionRoute() {
 
   // Function to handle closing the modal
   const handleClose = () => {
-    outletContext?.connectWebRTC().then(() => navigate(".."));
+    outletContext?.setupPeerConnection().then(() => navigate(".."));
   };
 
   return (
diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx
index 50fc79f..d2662fc 100644
--- a/ui/src/routes/devices.$id.tsx
+++ b/ui/src/routes/devices.$id.tsx
@@ -45,6 +45,10 @@ import Modal from "../components/Modal";
 import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
 import { FeatureFlagProvider } from "../providers/FeatureFlagProvider";
 import notifications from "../notifications";
+import {
+  ConnectionErrorOverlay,
+  LoadingConnectionOverlay,
+} from "../components/VideoOverlay";
 
 import { SystemVersionInfo } from "./devices.$id.settings.general.update";
 import { DeviceStatus } from "./welcome-local";
@@ -126,8 +130,6 @@ export default function KvmIdRoute() {
 
   const setIsTurnServerInUse = useRTCStore(state => state.setTurnServerInUse);
   const peerConnection = useRTCStore(state => state.peerConnection);
-  const peerConnectionState = useRTCStore(state => state.peerConnectionState);
-  const setPeerConnectionState = useRTCStore(state => state.setPeerConnectionState);
   const setMediaMediaStream = useRTCStore(state => state.setMediaStream);
   const setPeerConnection = useRTCStore(state => state.setPeerConnection);
   const setDiskChannel = useRTCStore(state => state.setDiskChannel);
@@ -135,78 +137,55 @@ export default function KvmIdRoute() {
   const setTransceiver = useRTCStore(state => state.setTransceiver);
   const location = useLocation();
 
-  const [connectionAttempts, setConnectionAttempts] = useState(0);
-
-  const [startedConnectingAt, setStartedConnectingAt] = useState<Date | null>(null);
-  const [connectedAt, setConnectedAt] = useState<Date | null>(null);
-
   const [connectionFailed, setConnectionFailed] = useState(false);
 
   const navigate = useNavigate();
   const { otaState, setOtaState, setModalView } = useUpdateStore();
 
+  const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
   const closePeerConnection = useCallback(
     function closePeerConnection() {
+      console.log("Closing peer connection");
+
+      setConnectionFailed(true);
+      connectionFailedRef.current = true;
+
       peerConnection?.close();
-      // "closed" is a valid RTCPeerConnection state according to the WebRTC spec
-      // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState#closed
-      // However, the onconnectionstatechange event doesn't fire when close() is called manually
-      // So we need to explicitly update our state to maintain consistency
-      // I don't know why this is happening, but this is the best way I can think of to handle it
-      // ALSO, this will render the connection error overlay linking to docs
-      setPeerConnectionState("closed");
+      signalingAttempts.current = 0;
     },
-    [peerConnection, setPeerConnectionState],
+    [peerConnection],
   );
 
+  // We need to track connectionFailed in a ref to avoid stale closure issues
+  // This is necessary because syncRemoteSessionDescription is a callback that captures
+  // the connectionFailed value at creation time, but we need the latest value
+  // when the function is actually called. Without this ref, the function would use
+  // a stale value of connectionFailed in some conditions.
+  //
+  // We still need the state variable for UI rendering, so we sync the ref with the state.
+  // This pattern is a workaround for what useEvent hook would solve more elegantly
+  // (which would give us a callback that always has access to latest state without re-creation).
+  const connectionFailedRef = useRef(false);
   useEffect(() => {
-    const connectionAttemptsThreshold = 30;
-    if (connectionAttempts > connectionAttemptsThreshold) {
-      console.log(`Connection failed after ${connectionAttempts} attempts.`);
-      setConnectionFailed(true);
-      closePeerConnection();
-    }
-  }, [connectionAttempts, closePeerConnection]);
-
-  useEffect(() => {
-    // Skip if already connected
-    if (connectedAt) return;
-
-    // Skip if connection is declared as failed
-    if (connectionFailed) return;
-
-    const interval = setInterval(() => {
-      console.log("Checking connection status");
-
-      // Skip if connection hasn't started
-      if (!startedConnectingAt) return;
-
-      const elapsedTime = Math.floor(
-        new Date().getTime() - startedConnectingAt.getTime(),
-      );
-
-      // Fail connection if it's been over X seconds since we started connecting
-      if (elapsedTime > 60 * 1000) {
-        console.error(`Connection failed after ${elapsedTime} ms.`);
-        setConnectionFailed(true);
-        closePeerConnection();
-      }
-    }, 1000);
-
-    return () => clearInterval(interval);
-  }, [closePeerConnection, connectedAt, connectionFailed, startedConnectingAt]);
-
-  const sdp = useCallback(
-    async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => {
-      if (!pc) return;
-      if (event.candidate !== null) return;
+    connectionFailedRef.current = connectionFailed;
+  }, [connectionFailed]);
 
+  const signalingAttempts = useRef(0);
+  const syncRemoteSessionDescription = useCallback(
+    async function syncRemoteSessionDescription(pc: RTCPeerConnection) {
       try {
+        if (!pc) return;
+
         const sd = btoa(JSON.stringify(pc.localDescription));
 
         const sessionUrl = isOnDevice
           ? `${DEVICE_API}/webrtc/session`
           : `${CLOUD_API}/webrtc/session`;
+
+        console.log("Trying to get remote session description");
+        setLoadingMessage(
+          `Getting remote session description...  ${signalingAttempts.current > 0 ? `(attempt ${signalingAttempts.current + 1})` : ""}`,
+        );
         const res = await api.POST(sessionUrl, {
           sd,
           // When on device, we don't need to specify the device id, as it's already known
@@ -214,73 +193,109 @@ export default function KvmIdRoute() {
         });
 
         const json = await res.json();
-
-        if (isOnDevice) {
-          if (res.status === 401) {
-            return navigate("/login-local");
-          }
+        if (res.status === 401) return navigate(isOnDevice ? "/login-local" : "/login");
+        if (!res.ok) {
+          console.error("Error getting SDP", { status: res.status, json });
+          throw new Error("Error getting SDP");
         }
 
-        if (isInCloud) {
-          // The cloud API returns a 401 if the user is not logged in
-          // Most likely the session has expired
-          if (res.status === 401) return navigate("/login");
+        console.log("Successfully got Remote Session Description. Setting.");
+        setLoadingMessage("Setting remote session description...");
 
-          // If can be a few things
-          // - In cloud mode, the cloud api would return a 404, if the device hasn't contacted the cloud yet
-          // - In device mode, the device api would timeout, the fetch would throw an error, therefore the catch block would be hit
-          // Regardless, we should close the peer connection and let the useInterval handle reconnecting
-          if (!res.ok) {
-            closePeerConnection();
-            console.error(`Error setting SDP - Status: ${res.status}}`, json);
-            return;
-          }
-        }
+        const decodedSd = atob(json.sd);
+        const parsedSd = JSON.parse(decodedSd);
+        pc.setRemoteDescription(new RTCSessionDescription(parsedSd));
 
-        pc.setRemoteDescription(
-          new RTCSessionDescription(JSON.parse(atob(json.sd))),
-        ).catch(e => console.log(`Error setting remote description: ${e}`));
+        await new Promise((resolve, reject) => {
+          console.log("Waiting for remote description to be set");
+          const maxAttempts = 10;
+          const interval = 1000;
+          let attempts = 0;
+
+          const checkInterval = setInterval(() => {
+            attempts++;
+            // When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects
+            if (pc.sctp?.state === "connected") {
+              console.log("Remote description set");
+              clearInterval(checkInterval);
+              resolve(true);
+            } else if (attempts >= maxAttempts) {
+              console.log(
+                `Failed to get remote description after ${maxAttempts} attempts`,
+              );
+              closePeerConnection();
+              clearInterval(checkInterval);
+              reject(
+                new Error(
+                  `Failed to get remote description after ${maxAttempts} attempts`,
+                ),
+              );
+            } else {
+              console.log("Waiting for remote description to be set");
+            }
+          }, interval);
+        });
       } catch (error) {
-        console.error(`Error setting SDP: ${error}`);
-        closePeerConnection();
+        console.error("Error getting SDP", { error });
+        console.log("Connection failed", connectionFailedRef.current);
+        if (connectionFailedRef.current) return;
+        if (signalingAttempts.current < 5) {
+          signalingAttempts.current++;
+          await new Promise(resolve => setTimeout(resolve, 500));
+          console.log("Attempting to get SDP again", signalingAttempts.current);
+          syncRemoteSessionDescription(pc);
+        } else {
+          closePeerConnection();
+        }
       }
     },
     [closePeerConnection, navigate, params.id],
   );
 
-  const connectWebRTC = useCallback(async () => {
-    console.log("Attempting to connect WebRTC");
-
-    // Track connection status to detect failures and show error overlay
-    setConnectionAttempts(x => x + 1);
-    setStartedConnectingAt(new Date());
-    setConnectedAt(null);
+  const setupPeerConnection = useCallback(async () => {
+    console.log("Setting up peer connection");
+    setConnectionFailed(false);
+    setLoadingMessage("Connecting to device...");
 
     let pc: RTCPeerConnection;
     try {
+      console.log("Creating peer connection");
+      setLoadingMessage("Creating peer connection...");
       pc = new RTCPeerConnection({
         // We only use STUN or TURN servers if we're in the cloud
         ...(isInCloud && iceConfig?.iceServers
           ? { iceServers: [iceConfig?.iceServers] }
           : {}),
       });
+      console.log("Peer connection created", pc);
+      setLoadingMessage("Peer connection created");
     } catch (e) {
       console.error(`Error creating peer connection: ${e}`);
-      closePeerConnection();
+      setTimeout(() => {
+        closePeerConnection();
+      }, 1000);
       return;
     }
 
     // Set up event listeners and data channels
     pc.onconnectionstatechange = () => {
-      // If the connection state is connected, we reset the connection attempts.
-      if (pc.connectionState === "connected") {
-        setConnectionAttempts(0);
-        setConnectedAt(new Date());
-      }
-      setPeerConnectionState(pc.connectionState);
+      console.log("Connection state changed", pc.connectionState);
     };
 
-    pc.onicecandidate = event => sdp(event, pc);
+    pc.onicegatheringstatechange = event => {
+      const pc = event.currentTarget as RTCPeerConnection;
+      console.log("ICE Gathering State Changed", pc.iceGatheringState);
+      if (pc.iceGatheringState === "complete") {
+        console.log("ICE Gathering completed");
+        setLoadingMessage("ICE Gathering completed");
+
+        // We can now start the https/ws connection to get the remote session description from the KVM device
+        syncRemoteSessionDescription(pc);
+      } else if (pc.iceGatheringState === "gathering") {
+        console.log("ICE Gathering Started");
+        setLoadingMessage("Gathering ICE candidates...");
+      }
+    };
 
     pc.ontrack = function (event) {
       setMediaMediaStream(event.streams[0]);
@@ -298,56 +313,32 @@ export default function KvmIdRoute() {
       setDiskChannel(diskDataChannel);
     };
 
+    setPeerConnection(pc);
+
     try {
       const offer = await pc.createOffer();
       await pc.setLocalDescription(offer);
-      setPeerConnection(pc);
     } catch (e) {
-      console.error(`Error creating offer: ${e}`);
+      console.error(`Error creating offer: ${e}`, new Date().toISOString());
       closePeerConnection();
     }
   }, [
     closePeerConnection,
     iceConfig?.iceServers,
-    sdp,
     setDiskChannel,
     setMediaMediaStream,
     setPeerConnection,
-    setPeerConnectionState,
     setRpcDataChannel,
     setTransceiver,
+    syncRemoteSessionDescription,
   ]);
 
-  useEffect(() => {
-    console.log("Attempting to connect WebRTC");
-
-    // If we're in an other session, we don't need to connect
-    if (location.pathname.includes("other-session")) return;
-
-    // If we're already connected or connecting, we don't need to connect
-    // We have to use the state from the store, because the peerConnection.connectionState doesnt trigger a value change, if called manually from .close()
-    if (["connected", "connecting", "new"].includes(peerConnectionState ?? "")) {
-      return;
-    }
-
-    // In certain cases, we want to never connect again. This happens when we've tried for a long time and failed
-    if (connectionFailed) {
-      console.log("Connection failed. We won't attempt to connect again.");
-      return;
-    }
-
-    const interval = setInterval(() => {
-      connectWebRTC();
-    }, 3000);
-    return () => clearInterval(interval);
-  }, [connectWebRTC, connectionFailed, location.pathname, peerConnectionState]);
-
   // On boot, if the connection state is undefined, we connect to the WebRTC
   useEffect(() => {
     if (peerConnection?.connectionState === undefined) {
-      connectWebRTC();
+      setupPeerConnection();
     }
-  }, [connectWebRTC, peerConnection?.connectionState]);
+  }, [setupPeerConnection, peerConnection?.connectionState]);
 
   // Cleanup effect
   const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats);
@@ -601,7 +592,27 @@ export default function KvmIdRoute() {
             kvmName={deviceName || "JetKVM Device"}
           />
 
-          <div className="flex h-full overflow-hidden">
+          <div className="flex h-full w-full overflow-hidden">
+            <div className="pointer-events-none fixed inset-0 isolate z-50 flex h-full w-full items-center justify-center">
+              <div className="my-2 h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
+                <LoadingConnectionOverlay
+                  show={
+                    !connectionFailed &&
+                    (["connecting", "new"].includes(
+                      peerConnection?.connectionState || "",
+                    ) ||
+                      peerConnection === null) &&
+                    !location.pathname.includes("other-session")
+                  }
+                  text={loadingMessage}
+                />
+                <ConnectionErrorOverlay
+                  show={connectionFailed && !location.pathname.includes("other-session")}
+                  setupPeerConnection={setupPeerConnection}
+                />
+              </div>
+            </div>
+
             <WebRTCVideo />
             <SidebarContainer sidebarView={sidebarView} />
           </div>
@@ -618,7 +629,7 @@ export default function KvmIdRoute() {
       >
         <Modal open={outlet !== null} onClose={onModalClose}>
           {/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
-          <Outlet context={{ connectWebRTC }} />
+          <Outlet context={{ setupPeerConnection }} />
         </Modal>
       </div>
 

From 73e715117ebc807cad9aca163c06b9c0fb89807c Mon Sep 17 00:00:00 2001
From: Siyuan Miao <i@xswan.net>
Date: Fri, 4 Apr 2025 12:58:19 +0200
Subject: [PATCH 15/17] feat(cloud): disconnect from cloud immediately when
 cloud URL changes or user requests to deregister

---
 cloud.go   | 44 ++++++++++++++++++++++++++++++++++++++++++++
 jsonrpc.go |  5 +++++
 2 files changed, 49 insertions(+)

diff --git a/cloud.go b/cloud.go
index be53b08..f91085a 100644
--- a/cloud.go
+++ b/cloud.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"sync"
 	"time"
 
 	"github.com/coder/websocket/wsjson"
@@ -113,6 +114,11 @@ var (
 	)
 )
 
+var (
+	cloudDisconnectChan chan error
+	cloudDisconnectLock = &sync.Mutex{}
+)
+
 func cloudResetMetrics(established bool) {
 	metricCloudConnectionLastPingTimestamp.Set(-1)
 	metricCloudConnectionLastPingDuration.Set(-1)
@@ -213,6 +219,24 @@ func handleCloudRegister(c *gin.Context) {
 	c.JSON(200, gin.H{"message": "Cloud registration successful"})
 }
 
+func disconnectCloud(reason error) {
+	cloudDisconnectLock.Lock()
+	defer cloudDisconnectLock.Unlock()
+
+	if cloudDisconnectChan == nil {
+		cloudLogger.Tracef("cloud disconnect channel is not set, no need to disconnect")
+		return
+	}
+
+	// just in case the channel is closed, we don't want to panic
+	defer func() {
+		if r := recover(); r != nil {
+			cloudLogger.Infof("cloud disconnect channel is closed, no need to disconnect: %v", r)
+		}
+	}()
+	cloudDisconnectChan <- reason
+}
+
 func runWebsocketClient() error {
 	if config.CloudToken == "" {
 		time.Sleep(5 * time.Second)
@@ -275,6 +299,23 @@ func runWebsocketClient() error {
 			metricCloudConnectionLastPingTimestamp.SetToCurrentTime()
 		}
 	}()
+
+	// create a channel to receive the disconnect event, once received, we cancelRun
+	cloudDisconnectChan = make(chan error)
+	defer func() {
+		close(cloudDisconnectChan)
+		cloudDisconnectChan = nil
+	}()
+	go func() {
+		for err := range cloudDisconnectChan {
+			if err == nil {
+				continue
+			}
+			cloudLogger.Infof("disconnecting from cloud due to: %v", err)
+			cancelRun()
+		}
+	}()
+
 	for {
 		typ, msg, err := c.Read(runCtx)
 		if err != nil {
@@ -448,6 +489,9 @@ func rpcDeregisterDevice() error {
 			return fmt.Errorf("failed to save configuration after deregistering: %w", err)
 		}
 
+		cloudLogger.Infof("device deregistered, disconnecting from cloud")
+		disconnectCloud(fmt.Errorf("device deregistered"))
+
 		return nil
 	}
 
diff --git a/jsonrpc.go b/jsonrpc.go
index 64935e1..9ce1f1b 100644
--- a/jsonrpc.go
+++ b/jsonrpc.go
@@ -771,9 +771,14 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
 }
 
 func rpcSetCloudUrl(apiUrl string, appUrl string) error {
+	currentCloudURL := config.CloudURL
 	config.CloudURL = apiUrl
 	config.CloudAppURL = appUrl
 
+	if currentCloudURL != apiUrl {
+		disconnectCloud(fmt.Errorf("cloud url changed from %s to %s", currentCloudURL, apiUrl))
+	}
+
 	if err := SaveConfig(); err != nil {
 		return fmt.Errorf("failed to save config: %w", err)
 	}

From fa1b11b228a2c432415eed57d5dd5708d81fa0e8 Mon Sep 17 00:00:00 2001
From: Aveline <352441+ym@users.noreply.github.com>
Date: Tue, 8 Apr 2025 00:43:03 +0200
Subject: [PATCH 16/17] chore(ota): allow a longer timeout when downloading
 packages (#332)

---
 ota.go | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/ota.go b/ota.go
index f813c09..9c583b6 100644
--- a/ota.go
+++ b/ota.go
@@ -126,7 +126,15 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
 		return fmt.Errorf("error creating request: %w", err)
 	}
 
-	resp, err := http.DefaultClient.Do(req)
+	client := http.Client{
+		// allow a longer timeout for the download but keep the TLS handshake short
+		Timeout: 10 * time.Minute,
+		Transport: &http.Transport{
+			TLSHandshakeTimeout: 1 * time.Minute,
+		},
+	}
+
+	resp, err := client.Do(req)
 	if err != nil {
 		return fmt.Errorf("error downloading file: %w", err)
 	}

From 1a30977085ba1795a1e6827976f2346c722fdac4 Mon Sep 17 00:00:00 2001
From: Adam Shiervani <adam.shiervani@gmail.com>
Date: Wed, 9 Apr 2025 00:10:38 +0200
Subject: [PATCH 17/17] Feat/Trickle ice (#336)

* feat(cloud): Use Websocket signaling in cloud mode

* refactor: Enhance WebRTC signaling and connection handling

* refactor: Improve WebRTC connection management and logging in KvmIdRoute

* refactor: Update PeerConnectionDisconnectedOverlay to use Card component for better UI structure

* refactor: Standardize metric naming and improve websocket logging

* refactor: Rename WebRTC signaling functions and update deployment script for debug version

* fix: Handle error when writing new ICE candidate to WebRTC signaling channel

* refactor: Rename signaling handler function for clarity

* refactor: Remove old http local http endpoint

* refactor: Improve metric help text and standardize comparison operator in KvmIdRoute

* chore(websocket): use MetricVec instead of Metric to store metrics

* fix conflicts

* fix: use wss when the page is served over https

* feat: Add app version header and update WebRTC signaling endpoint

* fix: Handle error when writing device metadata to WebRTC signaling channel

---------

Co-authored-by: Siyuan Miao <i@xswan.net>
---
 cloud.go                           | 179 +++++------
 log.go                             |   1 +
 ui/package-lock.json               |   6 +
 ui/package.json                    |   1 +
 ui/src/components/Header.tsx       |   6 +-
 ui/src/components/VideoOverlay.tsx |  58 +++-
 ui/src/components/WebRTCVideo.tsx  |   4 +-
 ui/src/routes/devices.$id.tsx      | 463 ++++++++++++++++++++---------
 web.go                             | 188 ++++++++++--
 webrtc.go                          |  23 +-
 10 files changed, 654 insertions(+), 275 deletions(-)

diff --git a/cloud.go b/cloud.go
index f91085a..7ad8b75 100644
--- a/cloud.go
+++ b/cloud.go
@@ -35,8 +35,8 @@ const (
 	// CloudOidcRequestTimeout is the timeout for OIDC token verification requests
 	// should be lower than the websocket response timeout set in cloud-api
 	CloudOidcRequestTimeout = 10 * time.Second
-	// CloudWebSocketPingInterval is the interval at which the websocket client sends ping messages to the cloud
-	CloudWebSocketPingInterval = 15 * time.Second
+	// WebsocketPingInterval is the interval at which the websocket client sends ping messages to the cloud
+	WebsocketPingInterval = 15 * time.Second
 )
 
 var (
@@ -52,59 +52,67 @@ var (
 			Help: "The timestamp when the cloud connection was established",
 		},
 	)
-	metricCloudConnectionLastPingTimestamp = promauto.NewGauge(
+	metricConnectionLastPingTimestamp = promauto.NewGaugeVec(
 		prometheus.GaugeOpts{
-			Name: "jetkvm_cloud_connection_last_ping_timestamp",
+			Name: "jetkvm_connection_last_ping_timestamp",
 			Help: "The timestamp when the last ping response was received",
 		},
+		[]string{"type", "source"},
 	)
-	metricCloudConnectionLastPingDuration = promauto.NewGauge(
+	metricConnectionLastPingDuration = promauto.NewGaugeVec(
 		prometheus.GaugeOpts{
-			Name: "jetkvm_cloud_connection_last_ping_duration",
+			Name: "jetkvm_connection_last_ping_duration",
 			Help: "The duration of the last ping response",
 		},
+		[]string{"type", "source"},
 	)
-	metricCloudConnectionPingDuration = promauto.NewHistogram(
+	metricConnectionPingDuration = promauto.NewHistogramVec(
 		prometheus.HistogramOpts{
-			Name: "jetkvm_cloud_connection_ping_duration",
+			Name: "jetkvm_connection_ping_duration",
 			Help: "The duration of the ping response",
 			Buckets: []float64{
 				0.1, 0.5, 1, 10,
 			},
 		},
+		[]string{"type", "source"},
 	)
-	metricCloudConnectionTotalPingCount = promauto.NewCounter(
+	metricConnectionTotalPingCount = promauto.NewCounterVec(
 		prometheus.CounterOpts{
-			Name: "jetkvm_cloud_connection_total_ping_count",
-			Help: "The total number of pings sent to the cloud",
+			Name: "jetkvm_connection_total_ping_count",
+			Help: "The total number of pings sent to the connection",
 		},
+		[]string{"type", "source"},
 	)
-	metricCloudConnectionSessionRequestCount = promauto.NewCounter(
+	metricConnectionSessionRequestCount = promauto.NewCounterVec(
 		prometheus.CounterOpts{
-			Name: "jetkvm_cloud_connection_session_total_request_count",
-			Help: "The total number of session requests received from the cloud",
+			Name: "jetkvm_connection_session_total_request_count",
+			Help: "The total number of session requests received",
 		},
+		[]string{"type", "source"},
 	)
-	metricCloudConnectionSessionRequestDuration = promauto.NewHistogram(
+	metricConnectionSessionRequestDuration = promauto.NewHistogramVec(
 		prometheus.HistogramOpts{
-			Name: "jetkvm_cloud_connection_session_request_duration",
+			Name: "jetkvm_connection_session_request_duration",
 			Help: "The duration of session requests",
 			Buckets: []float64{
 				0.1, 0.5, 1, 10,
 			},
 		},
+		[]string{"type", "source"},
 	)
-	metricCloudConnectionLastSessionRequestTimestamp = promauto.NewGauge(
+	metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec(
 		prometheus.GaugeOpts{
-			Name: "jetkvm_cloud_connection_last_session_request_timestamp",
+			Name: "jetkvm_connection_last_session_request_timestamp",
 			Help: "The timestamp of the last session request",
 		},
+		[]string{"type", "source"},
 	)
-	metricCloudConnectionLastSessionRequestDuration = promauto.NewGauge(
+	metricConnectionLastSessionRequestDuration = promauto.NewGaugeVec(
 		prometheus.GaugeOpts{
-			Name: "jetkvm_cloud_connection_last_session_request_duration",
+			Name: "jetkvm_connection_last_session_request_duration",
 			Help: "The duration of the last session request",
 		},
+		[]string{"type", "source"},
 	)
 	metricCloudConnectionFailureCount = promauto.NewCounter(
 		prometheus.CounterOpts{
@@ -119,12 +127,16 @@ var (
 	cloudDisconnectLock = &sync.Mutex{}
 )
 
-func cloudResetMetrics(established bool) {
-	metricCloudConnectionLastPingTimestamp.Set(-1)
-	metricCloudConnectionLastPingDuration.Set(-1)
+func wsResetMetrics(established bool, sourceType string, source string) {
+	metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1)
+	metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1)
 
-	metricCloudConnectionLastSessionRequestTimestamp.Set(-1)
-	metricCloudConnectionLastSessionRequestDuration.Set(-1)
+	metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1)
+	metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1)
+
+	if sourceType != "cloud" {
+		return
+	}
 
 	if established {
 		metricCloudConnectionEstablishedTimestamp.SetToCurrentTime()
@@ -256,6 +268,7 @@ func runWebsocketClient() error {
 
 	header := http.Header{}
 	header.Set("X-Device-ID", GetDeviceID())
+	header.Set("X-App-Version", builtAppVersion)
 	header.Set("Authorization", "Bearer "+config.CloudToken)
 	dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout)
 
@@ -270,88 +283,13 @@ func runWebsocketClient() error {
 	cloudLogger.Infof("websocket connected to %s", wsURL)
 
 	// set the metrics when we successfully connect to the cloud.
-	cloudResetMetrics(true)
+	wsResetMetrics(true, "cloud", "")
 
-	runCtx, cancelRun := context.WithCancel(context.Background())
-	defer cancelRun()
-	go func() {
-		for {
-			time.Sleep(CloudWebSocketPingInterval)
-
-			// set the timer for the ping duration
-			timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
-				metricCloudConnectionLastPingDuration.Set(v)
-				metricCloudConnectionPingDuration.Observe(v)
-			}))
-
-			err := c.Ping(runCtx)
-
-			if err != nil {
-				cloudLogger.Warnf("websocket ping error: %v", err)
-				cancelRun()
-				return
-			}
-
-			// dont use `defer` here because we want to observe the duration of the ping
-			timer.ObserveDuration()
-
-			metricCloudConnectionTotalPingCount.Inc()
-			metricCloudConnectionLastPingTimestamp.SetToCurrentTime()
-		}
-	}()
-
-	// create a channel to receive the disconnect event, once received, we cancelRun
-	cloudDisconnectChan = make(chan error)
-	defer func() {
-		close(cloudDisconnectChan)
-		cloudDisconnectChan = nil
-	}()
-	go func() {
-		for err := range cloudDisconnectChan {
-			if err == nil {
-				continue
-			}
-			cloudLogger.Infof("disconnecting from cloud due to: %v", err)
-			cancelRun()
-		}
-	}()
-
-	for {
-		typ, msg, err := c.Read(runCtx)
-		if err != nil {
-			return err
-		}
-		if typ != websocket.MessageText {
-			// ignore non-text messages
-			continue
-		}
-		var req WebRTCSessionRequest
-		err = json.Unmarshal(msg, &req)
-		if err != nil {
-			cloudLogger.Warnf("unable to parse ws message: %v", string(msg))
-			continue
-		}
-
-		cloudLogger.Infof("new session request: %v", req.OidcGoogle)
-		cloudLogger.Tracef("session request info: %v", req)
-
-		metricCloudConnectionSessionRequestCount.Inc()
-		metricCloudConnectionLastSessionRequestTimestamp.SetToCurrentTime()
-		err = handleSessionRequest(runCtx, c, req)
-		if err != nil {
-			cloudLogger.Infof("error starting new session: %v", err)
-			continue
-		}
-	}
+	// we don't have a source for the cloud connection
+	return handleWebRTCSignalWsMessages(c, true, "")
 }
 
-func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
-	timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
-		metricCloudConnectionLastSessionRequestDuration.Set(v)
-		metricCloudConnectionSessionRequestDuration.Observe(v)
-	}))
-	defer timer.ObserveDuration()
-
+func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
 	oidcCtx, cancelOIDC := context.WithTimeout(ctx, CloudOidcRequestTimeout)
 	defer cancelOIDC()
 	provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com")
@@ -379,10 +317,35 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
 		return fmt.Errorf("google identity mismatch")
 	}
 
+	return nil
+}
+
+func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest, isCloudConnection bool, source string) error {
+	var sourceType string
+	if isCloudConnection {
+		sourceType = "cloud"
+	} else {
+		sourceType = "local"
+	}
+
+	timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
+		metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(v)
+		metricConnectionSessionRequestDuration.WithLabelValues(sourceType, source).Observe(v)
+	}))
+	defer timer.ObserveDuration()
+
+	// If the message is from the cloud, we need to authenticate the session.
+	if isCloudConnection {
+		if err := authenticateSession(ctx, c, req); err != nil {
+			return err
+		}
+	}
+
 	session, err := newSession(SessionConfig{
-		ICEServers: req.ICEServers,
+		ws:         c,
+		IsCloud:    isCloudConnection,
 		LocalIP:    req.IP,
-		IsCloud:    true,
+		ICEServers: req.ICEServers,
 	})
 	if err != nil {
 		_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
@@ -406,14 +369,14 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
 	cloudLogger.Info("new session accepted")
 	cloudLogger.Tracef("new session accepted: %v", session)
 	currentSession = session
-	_ = wsjson.Write(context.Background(), c, gin.H{"sd": sd})
+	_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
 	return nil
 }
 
 func RunWebsocketClient() {
 	for {
 		// reset the metrics when we start the websocket client.
-		cloudResetMetrics(false)
+		wsResetMetrics(false, "cloud", "")
 
 		// If the cloud token is not set, we don't need to run the websocket client.
 		if config.CloudToken == "" {
diff --git a/log.go b/log.go
index 7718a28..0d36c0d 100644
--- a/log.go
+++ b/log.go
@@ -6,3 +6,4 @@ import "github.com/pion/logging"
 // ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC
 var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm")
 var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud")
+var websocketLogger = logging.NewDefaultLoggerFactory().NewLogger("websocket")
diff --git a/ui/package-lock.json b/ui/package-lock.json
index e9caa20..ebce148 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -30,6 +30,7 @@
         "react-icons": "^5.4.0",
         "react-router-dom": "^6.22.3",
         "react-simple-keyboard": "^3.7.112",
+        "react-use-websocket": "^4.13.0",
         "react-xtermjs": "^1.0.9",
         "recharts": "^2.15.0",
         "tailwind-merge": "^2.5.5",
@@ -5180,6 +5181,11 @@
         "react-dom": ">=16.6.0"
       }
     },
+    "node_modules/react-use-websocket": {
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz",
+      "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw=="
+    },
     "node_modules/react-xtermjs": {
       "version": "1.0.9",
       "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.9.tgz",
diff --git a/ui/package.json b/ui/package.json
index f8f1c7a..a248616 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -40,6 +40,7 @@
     "react-icons": "^5.4.0",
     "react-router-dom": "^6.22.3",
     "react-simple-keyboard": "^3.7.112",
+    "react-use-websocket": "^4.13.0",
     "react-xtermjs": "^1.0.9",
     "recharts": "^2.15.0",
     "tailwind-merge": "^2.5.5",
diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx
index 03a907e..19e9652 100644
--- a/ui/src/components/Header.tsx
+++ b/ui/src/components/Header.tsx
@@ -36,7 +36,7 @@ export default function DashboardNavbar({
   picture,
   kvmName,
 }: NavbarProps) {
-  const peerConnection = useRTCStore(state => state.peerConnection);
+  const peerConnectionState = useRTCStore(state => state.peerConnectionState);
   const setUser = useUserStore(state => state.setUser);
   const navigate = useNavigate();
   const onLogout = useCallback(async () => {
@@ -82,14 +82,14 @@ export default function DashboardNavbar({
                 <div className="hidden items-center gap-x-2 md:flex">
                   <div className="w-[159px]">
                     <PeerConnectionStatusCard
-                      state={peerConnection?.connectionState}
+                      state={peerConnectionState}
                       title={kvmName}
                     />
                   </div>
                   <div className="hidden w-[159px] md:block">
                     <USBStateStatus
                       state={usbState}
-                      peerConnectionState={peerConnection?.connectionState}
+                        peerConnectionState={peerConnectionState}
                     />
                   </div>
                 </div>
diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx
index 0620af4..e34cf10 100644
--- a/ui/src/components/VideoOverlay.tsx
+++ b/ui/src/components/VideoOverlay.tsx
@@ -6,7 +6,7 @@ import { LuPlay } from "react-icons/lu";
 
 import { Button, LinkButton } from "@components/Button";
 import LoadingSpinner from "@components/LoadingSpinner";
-import { GridCard } from "@components/Card";
+import Card, { GridCard } from "@components/Card";
 
 interface OverlayContentProps {
   children: React.ReactNode;
@@ -94,7 +94,7 @@ interface ConnectionErrorOverlayProps {
   setupPeerConnection: () => Promise<void>;
 }
 
-export function ConnectionErrorOverlay({
+export function ConnectionFailedOverlay({
   show,
   setupPeerConnection,
 }: ConnectionErrorOverlayProps) {
@@ -151,6 +151,60 @@ export function ConnectionErrorOverlay({
   );
 }
 
+interface PeerConnectionDisconnectedOverlay {
+  show: boolean;
+}
+
+export function PeerConnectionDisconnectedOverlay({
+  show,
+}: PeerConnectionDisconnectedOverlay) {
+  return (
+    <AnimatePresence>
+      {show && (
+        <motion.div
+          className="aspect-video h-full w-full"
+          initial={{ opacity: 0 }}
+          animate={{ opacity: 1 }}
+          exit={{ opacity: 0, transition: { duration: 0 } }}
+          transition={{
+            duration: 0.4,
+            ease: "easeInOut",
+          }}
+        >
+          <OverlayContent>
+            <div className="flex flex-col items-start gap-y-1">
+              <ExclamationTriangleIcon className="h-12 w-12 text-yellow-500" />
+              <div className="text-left text-sm text-slate-700 dark:text-slate-300">
+                <div className="space-y-4">
+                  <div className="space-y-2 text-black dark:text-white">
+                    <h2 className="text-xl font-bold">Connection Issue Detected</h2>
+                    <ul className="list-disc space-y-2 pl-4 text-left">
+                      <li>Verify that the device is powered on and properly connected</li>
+                      <li>Check all cable connections for any loose or damaged wires</li>
+                      <li>Ensure your network connection is stable and active</li>
+                      <li>Try restarting both the device and your computer</li>
+                    </ul>
+                  </div>
+                  <div className="flex items-center gap-x-2">
+                    <Card>
+                      <div className="flex items-center gap-x-2 p-4">
+                        <LoadingSpinner className="h-4 w-4 text-blue-800 dark:text-blue-200" />
+                        <p className="text-sm text-slate-700 dark:text-slate-300">
+                          Retrying connection...
+                        </p>
+                      </div>
+                    </Card>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </OverlayContent>
+        </motion.div>
+      )}
+    </AnimatePresence>
+  );
+}
+
 interface HDMIErrorOverlayProps {
   show: boolean;
   hdmiState: string;
diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx
index 5d8fb55..99c0191 100644
--- a/ui/src/components/WebRTCVideo.tsx
+++ b/ui/src/components/WebRTCVideo.tsx
@@ -380,7 +380,7 @@ export default function WebRTCVideo() {
     (mediaStream: MediaStream) => {
       if (!videoElm.current) return;
       const videoElmRefValue = videoElm.current;
-      console.log("Adding stream to video element", videoElmRefValue);
+      // console.log("Adding stream to video element", videoElmRefValue);
       videoElmRefValue.srcObject = mediaStream;
       updateVideoSizeStore(videoElmRefValue);
     },
@@ -396,7 +396,7 @@ export default function WebRTCVideo() {
       peerConnection.addEventListener(
         "track",
         (e: RTCTrackEvent) => {
-          console.log("Adding stream to video element");
+          // console.log("Adding stream to video element");
           addStreamToVideoElm(e.streams[0]);
         },
         { signal },
diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx
index d2662fc..fef1764 100644
--- a/ui/src/routes/devices.$id.tsx
+++ b/ui/src/routes/devices.$id.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import {
   LoaderFunctionArgs,
   Outlet,
@@ -14,6 +14,7 @@ import {
 import { useInterval } from "usehooks-ts";
 import FocusTrap from "focus-trap-react";
 import { motion, AnimatePresence } from "framer-motion";
+import useWebSocket from "react-use-websocket";
 
 import { cx } from "@/cva.config";
 import {
@@ -43,15 +44,16 @@ import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard
 import api from "../api";
 import Modal from "../components/Modal";
 import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
+import {
+  ConnectionFailedOverlay,
+  LoadingConnectionOverlay,
+  PeerConnectionDisconnectedOverlay,
+} from "../components/VideoOverlay";
 import { FeatureFlagProvider } from "../providers/FeatureFlagProvider";
 import notifications from "../notifications";
-import {
-  ConnectionErrorOverlay,
-  LoadingConnectionOverlay,
-} from "../components/VideoOverlay";
 
-import { SystemVersionInfo } from "./devices.$id.settings.general.update";
 import { DeviceStatus } from "./welcome-local";
+import { SystemVersionInfo } from "./devices.$id.settings.general.update";
 
 interface LocalLoaderResp {
   authMode: "password" | "noPassword" | null;
@@ -117,7 +119,6 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
 
 export default function KvmIdRoute() {
   const loaderResp = useLoaderData() as LocalLoaderResp | CloudLoaderResp;
-
   // Depending on the mode, we set the appropriate variables
   const user = "user" in loaderResp ? loaderResp.user : null;
   const deviceName = "deviceName" in loaderResp ? loaderResp.deviceName : null;
@@ -130,6 +131,8 @@ export default function KvmIdRoute() {
 
   const setIsTurnServerInUse = useRTCStore(state => state.setTurnServerInUse);
   const peerConnection = useRTCStore(state => state.peerConnection);
+  const setPeerConnectionState = useRTCStore(state => state.setPeerConnectionState);
+  const peerConnectionState = useRTCStore(state => state.peerConnectionState);
   const setMediaMediaStream = useRTCStore(state => state.setMediaStream);
   const setPeerConnection = useRTCStore(state => state.setPeerConnection);
   const setDiskChannel = useRTCStore(state => state.setDiskChannel);
@@ -137,23 +140,28 @@ export default function KvmIdRoute() {
   const setTransceiver = useRTCStore(state => state.setTransceiver);
   const location = useLocation();
 
+  const isLegacySignalingEnabled = useRef(false);
+
   const [connectionFailed, setConnectionFailed] = useState(false);
 
   const navigate = useNavigate();
   const { otaState, setOtaState, setModalView } = useUpdateStore();
 
   const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
-  const closePeerConnection = useCallback(
-    function closePeerConnection() {
+  const cleanupAndStopReconnecting = useCallback(
+    function cleanupAndStopReconnecting() {
       console.log("Closing peer connection");
 
       setConnectionFailed(true);
+      if (peerConnection) {
+        setPeerConnectionState(peerConnection.connectionState);
+      }
       connectionFailedRef.current = true;
 
       peerConnection?.close();
       signalingAttempts.current = 0;
     },
-    [peerConnection],
+    [peerConnection, setPeerConnectionState],
   );
 
   // We need to track connectionFailed in a ref to avoid stale closure issues
@@ -171,95 +179,233 @@ export default function KvmIdRoute() {
   }, [connectionFailed]);
 
   const signalingAttempts = useRef(0);
-  const syncRemoteSessionDescription = useCallback(
-    async function syncRemoteSessionDescription(pc: RTCPeerConnection) {
+  const setRemoteSessionDescription = useCallback(
+    async function setRemoteSessionDescription(
+      pc: RTCPeerConnection,
+      remoteDescription: RTCSessionDescriptionInit,
+    ) {
+      setLoadingMessage("Setting remote description");
+
       try {
-        if (!pc) return;
-
-        const sd = btoa(JSON.stringify(pc.localDescription));
-
-        const sessionUrl = isOnDevice
-          ? `${DEVICE_API}/webrtc/session`
-          : `${CLOUD_API}/webrtc/session`;
-
-        console.log("Trying to get remote session description");
-        setLoadingMessage(
-          `Getting remote session description...  ${signalingAttempts.current > 0 ? `(attempt ${signalingAttempts.current + 1})` : ""}`,
-        );
-        const res = await api.POST(sessionUrl, {
-          sd,
-          // When on device, we don't need to specify the device id, as it's already known
-          ...(isOnDevice ? {} : { id: params.id }),
-        });
-
-        const json = await res.json();
-        if (res.status === 401) return navigate(isOnDevice ? "/login-local" : "/login");
-        if (!res.ok) {
-          console.error("Error getting SDP", { status: res.status, json });
-          throw new Error("Error getting SDP");
-        }
-
-        console.log("Successfully got Remote Session Description. Setting.");
-        setLoadingMessage("Setting remote session description...");
-
-        const decodedSd = atob(json.sd);
-        const parsedSd = JSON.parse(decodedSd);
-        pc.setRemoteDescription(new RTCSessionDescription(parsedSd));
-
-        await new Promise((resolve, reject) => {
-          console.log("Waiting for remote description to be set");
-          const maxAttempts = 10;
-          const interval = 1000;
-          let attempts = 0;
-
-          const checkInterval = setInterval(() => {
-            attempts++;
-            // When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects
-            if (pc.sctp?.state === "connected") {
-              console.log("Remote description set");
-              clearInterval(checkInterval);
-              resolve(true);
-            } else if (attempts >= maxAttempts) {
-              console.log(
-                `Failed to get remote description after ${maxAttempts} attempts`,
-              );
-              closePeerConnection();
-              clearInterval(checkInterval);
-              reject(
-                new Error(
-                  `Failed to get remote description after ${maxAttempts} attempts`,
-                ),
-              );
-            } else {
-              console.log("Waiting for remote description to be set");
-            }
-          }, interval);
-        });
+        await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
+        console.log("[setRemoteSessionDescription] Remote description set successfully");
+        setLoadingMessage("Establishing secure connection...");
       } catch (error) {
-        console.error("Error getting SDP", { error });
-        console.log("Connection failed", connectionFailedRef.current);
-        if (connectionFailedRef.current) return;
-        if (signalingAttempts.current < 5) {
-          signalingAttempts.current++;
-          await new Promise(resolve => setTimeout(resolve, 500));
-          console.log("Attempting to get SDP again", signalingAttempts.current);
-          syncRemoteSessionDescription(pc);
-        } else {
-          closePeerConnection();
-        }
+        console.error(
+          "[setRemoteSessionDescription] Failed to set remote description:",
+          error,
+        );
+        cleanupAndStopReconnecting();
+        return;
       }
+
+      // Replace the interval-based check with a more reliable approach
+      let attempts = 0;
+      const checkInterval = setInterval(() => {
+        attempts++;
+
+        // When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects
+        if (pc.sctp?.state === "connected") {
+          console.log("[setRemoteSessionDescription] Remote description set");
+          clearInterval(checkInterval);
+          setLoadingMessage("Connection established");
+        } else if (attempts >= 10) {
+          console.log(
+            "[setRemoteSessionDescription] Failed to establish connection after 10 attempts",
+            {
+              connectionState: pc.connectionState,
+              iceConnectionState: pc.iceConnectionState,
+            },
+          );
+          cleanupAndStopReconnecting();
+          clearInterval(checkInterval);
+        } else {
+          console.log("[setRemoteSessionDescription] Waiting for connection, state:", {
+            connectionState: pc.connectionState,
+            iceConnectionState: pc.iceConnectionState,
+          });
+        }
+      }, 1000);
     },
-    [closePeerConnection, navigate, params.id],
+    [cleanupAndStopReconnecting],
+  );
+
+  const ignoreOffer = useRef(false);
+  const isSettingRemoteAnswerPending = useRef(false);
+  const makingOffer = useRef(false);
+
+  const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+
+  const { sendMessage, getWebSocket } = useWebSocket(
+    isOnDevice
+      ? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
+      : `${CLOUD_API.replace("http", "ws")}/webrtc/signaling/client?id=${params.id}`,
+    {
+      heartbeat: true,
+      retryOnError: true,
+      reconnectAttempts: 5,
+      reconnectInterval: 1000,
+      onReconnectStop: () => {
+        console.log("Reconnect stopped");
+        cleanupAndStopReconnecting();
+      },
+
+      shouldReconnect(event) {
+        console.log("[Websocket] shouldReconnect", event);
+        // TODO: Why true?
+        return true;
+      },
+
+      onClose(event) {
+        console.log("[Websocket] onClose", event);
+        // We don't want to close everything down, we wait for the reconnect to stop instead
+      },
+
+      onError(event) {
+        console.log("[Websocket] onError", event);
+        // We don't want to close everything down, we wait for the reconnect to stop instead
+      },
+      onOpen() {
+        console.log("[Websocket] onOpen");
+      },
+
+      onMessage: message => {
+        if (message.data === "pong") return;
+
+        /*
+          Currently the signaling process is as follows:
+            After open, the other side will send a `device-metadata` message with the device version
+            If the device version is not set, we can assume the device is using the legacy signaling
+            Otherwise, we can assume the device is using the new signaling
+
+            If the device is using the legacy signaling, we close the websocket connection
+            and use the legacy HTTPSignaling function to get the remote session description
+
+            If the device is using the new signaling, we don't need to do anything special, but continue to use the websocket connection
+            to chat with the other peer about the connection
+        */
+
+        const parsedMessage = JSON.parse(message.data);
+        if (parsedMessage.type === "device-metadata") {
+          const { deviceVersion } = parsedMessage.data;
+          console.log("[Websocket] Received device-metadata message");
+          console.log("[Websocket] Device version", deviceVersion);
+          // If the device version is not set, we can assume the device is using the legacy signaling
+          if (!deviceVersion) {
+            console.log("[Websocket] Device is using legacy signaling");
+
+            // Now we don't need the websocket connection anymore, as we've established that we need to use the legacy signaling
+            // which does everything over HTTP(at least from the perspective of the client)
+            isLegacySignalingEnabled.current = true;
+            getWebSocket()?.close();
+          } else {
+            console.log("[Websocket] Device is using new signaling");
+            isLegacySignalingEnabled.current = false;
+          }
+          setupPeerConnection();
+        }
+
+        if (!peerConnection) return;
+        if (parsedMessage.type === "answer") {
+          console.log("[Websocket] Received answer");
+          const readyForOffer =
+            // If we're making an offer, we don't want to accept an answer
+            !makingOffer &&
+            // If the peer connection is stable or we're setting the remote answer pending, we're ready for an offer
+            (peerConnection?.signalingState === "stable" ||
+              isSettingRemoteAnswerPending.current);
+
+          // If we're not ready for an offer, we don't want to accept an offer
+          ignoreOffer.current = parsedMessage.type === "offer" && !readyForOffer;
+          if (ignoreOffer.current) return;
+
+          // Set so we don't accept an answer while we're setting the remote description
+          isSettingRemoteAnswerPending.current = parsedMessage.type === "answer";
+          console.log(
+            "[Websocket] Setting remote answer pending",
+            isSettingRemoteAnswerPending.current,
+          );
+
+          const sd = atob(parsedMessage.data);
+          const remoteSessionDescription = JSON.parse(sd);
+
+          setRemoteSessionDescription(
+            peerConnection,
+            new RTCSessionDescription(remoteSessionDescription),
+          );
+
+          // Reset the remote answer pending flag
+          isSettingRemoteAnswerPending.current = false;
+        } else if (parsedMessage.type === "new-ice-candidate") {
+          console.log("[Websocket] Received new-ice-candidate");
+          const candidate = parsedMessage.data;
+          peerConnection.addIceCandidate(candidate);
+        }
+      },
+    },
+
+    // Don't even retry once we declare failure
+    !connectionFailed && isLegacySignalingEnabled.current === false,
+  );
+
+  const sendWebRTCSignal = useCallback(
+    (type: string, data: unknown) => {
+      // Second argument tells the library not to queue the message, and send it once the connection is established again.
+      // We have event handlers that handle the connection set up, so we don't need to queue the message.
+      sendMessage(JSON.stringify({ type, data }), false);
+    },
+    [sendMessage],
+  );
+
+  const legacyHTTPSignaling = useCallback(
+    async (pc: RTCPeerConnection) => {
+      const sd = btoa(JSON.stringify(pc.localDescription));
+
+      // Legacy mode == UI in cloud with updated code connecting to older device version.
+      // In device mode, old devices wont server this JS, and on newer devices legacy mode wont be enabled
+      const sessionUrl = `${CLOUD_API}/webrtc/session`;
+
+      console.log("Trying to get remote session description");
+      setLoadingMessage(
+        `Getting remote session description...  ${signalingAttempts.current > 0 ? `(attempt ${signalingAttempts.current + 1})` : ""}`,
+      );
+      const res = await api.POST(sessionUrl, {
+        sd,
+        // When on device, we don't need to specify the device id, as it's already known
+        ...(isOnDevice ? {} : { id: params.id }),
+      });
+
+      const json = await res.json();
+      if (res.status === 401) return navigate(isOnDevice ? "/login-local" : "/login");
+      if (!res.ok) {
+        console.error("Error getting SDP", { status: res.status, json });
+        cleanupAndStopReconnecting();
+        return;
+      }
+
+      console.log("Successfully got Remote Session Description. Setting.");
+      setLoadingMessage("Setting remote session description...");
+
+      const decodedSd = atob(json.sd);
+      const parsedSd = JSON.parse(decodedSd);
+      setRemoteSessionDescription(pc, new RTCSessionDescription(parsedSd));
+    },
+    [cleanupAndStopReconnecting, navigate, params.id, setRemoteSessionDescription],
   );
 
   const setupPeerConnection = useCallback(async () => {
-    console.log("Setting up peer connection");
+    console.log("[setupPeerConnection] Setting up peer connection");
     setConnectionFailed(false);
     setLoadingMessage("Connecting to device...");
 
+    if (peerConnection?.signalingState === "stable") {
+      console.log("[setupPeerConnection] Peer connection already established");
+      return;
+    }
+
     let pc: RTCPeerConnection;
     try {
-      console.log("Creating peer connection");
+      console.log("[setupPeerConnection] Creating peer connection");
       setLoadingMessage("Creating peer connection...");
       pc = new RTCPeerConnection({
         // We only use STUN or TURN servers if we're in the cloud
@@ -267,30 +413,65 @@ export default function KvmIdRoute() {
           ? { iceServers: [iceConfig?.iceServers] }
           : {}),
       });
-      console.log("Peer connection created", pc);
-      setLoadingMessage("Peer connection created");
+
+      setPeerConnectionState(pc.connectionState);
+      console.log("[setupPeerConnection] Peer connection created", pc);
+      setLoadingMessage("Setting up connection to device...");
     } catch (e) {
-      console.error(`Error creating peer connection: ${e}`);
+      console.error(`[setupPeerConnection] Error creating peer connection: ${e}`);
       setTimeout(() => {
-        closePeerConnection();
+        cleanupAndStopReconnecting();
       }, 1000);
       return;
     }
 
     // Set up event listeners and data channels
     pc.onconnectionstatechange = () => {
-      console.log("Connection state changed", pc.connectionState);
+      console.log("[setupPeerConnection] Connection state changed", pc.connectionState);
+      setPeerConnectionState(pc.connectionState);
+    };
+
+    pc.onnegotiationneeded = async () => {
+      try {
+        console.log("[setupPeerConnection] Creating offer");
+        makingOffer.current = true;
+
+        const offer = await pc.createOffer();
+        await pc.setLocalDescription(offer);
+        const sd = btoa(JSON.stringify(pc.localDescription));
+        const isNewSignalingEnabled = isLegacySignalingEnabled.current === false;
+        if (isNewSignalingEnabled) {
+          sendWebRTCSignal("offer", { sd: sd });
+        } else {
+          console.log("Legacy signanling. Waiting for ICE Gathering to complete...");
+        }
+      } catch (e) {
+        console.error(
+          `[setupPeerConnection] Error creating offer: ${e}`,
+          new Date().toISOString(),
+        );
+        cleanupAndStopReconnecting();
+      } finally {
+        makingOffer.current = false;
+      }
+    };
+
+    pc.onicecandidate = async ({ candidate }) => {
+      if (!candidate) return;
+      if (candidate.candidate === "") return;
+      sendWebRTCSignal("new-ice-candidate", candidate);
     };
 
     pc.onicegatheringstatechange = event => {
       const pc = event.currentTarget as RTCPeerConnection;
-      console.log("ICE Gathering State Changed", pc.iceGatheringState);
       if (pc.iceGatheringState === "complete") {
         console.log("ICE Gathering completed");
         setLoadingMessage("ICE Gathering completed");
 
-        // We can now start the https/ws connection to get the remote session description from the KVM device
-        syncRemoteSessionDescription(pc);
+        if (isLegacySignalingEnabled.current) {
+          // We can now start the https/ws connection to get the remote session description from the KVM device
+          legacyHTTPSignaling(pc);
+        }
       } else if (pc.iceGatheringState === "gathering") {
         console.log("ICE Gathering Started");
         setLoadingMessage("Gathering ICE candidates...");
@@ -314,31 +495,26 @@ export default function KvmIdRoute() {
     };
 
     setPeerConnection(pc);
-
-    try {
-      const offer = await pc.createOffer();
-      await pc.setLocalDescription(offer);
-    } catch (e) {
-      console.error(`Error creating offer: ${e}`, new Date().toISOString());
-      closePeerConnection();
-    }
   }, [
-    closePeerConnection,
+    cleanupAndStopReconnecting,
     iceConfig?.iceServers,
+    legacyHTTPSignaling,
+    peerConnection?.signalingState,
+    sendWebRTCSignal,
     setDiskChannel,
     setMediaMediaStream,
     setPeerConnection,
+    setPeerConnectionState,
     setRpcDataChannel,
     setTransceiver,
-    syncRemoteSessionDescription,
   ]);
 
-  // On boot, if the connection state is undefined, we connect to the WebRTC
   useEffect(() => {
-    if (peerConnection?.connectionState === undefined) {
-      setupPeerConnection();
+    if (peerConnectionState === "failed") {
+      console.log("Connection failed, closing peer connection");
+      cleanupAndStopReconnecting();
     }
-  }, [setupPeerConnection, peerConnection?.connectionState]);
+  }, [peerConnectionState, cleanupAndStopReconnecting]);
 
   // Cleanup effect
   const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats);
@@ -363,7 +539,7 @@ export default function KvmIdRoute() {
 
   // TURN server usage detection
   useEffect(() => {
-    if (peerConnection?.connectionState !== "connected") return;
+    if (peerConnectionState !== "connected") return;
     const { localCandidateStats, remoteCandidateStats } = useRTCStore.getState();
 
     const lastLocalStat = Array.from(localCandidateStats).pop();
@@ -375,7 +551,7 @@ export default function KvmIdRoute() {
     const remoteCandidateIsUsingTurn = lastRemoteStat[1].candidateType === "relay"; // [0] is the timestamp, which we don't care about here
 
     setIsTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn);
-  }, [peerConnection?.connectionState, setIsTurnServerInUse]);
+  }, [peerConnectionState, setIsTurnServerInUse]);
 
   // TURN server usage reporting
   const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
@@ -466,10 +642,6 @@ export default function KvmIdRoute() {
     });
   }, [rpcDataChannel?.readyState, send, setHdmiState]);
 
-  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-  // @ts-expect-error
-  window.send = send;
-
   // When the update is successful, we need to refresh the client javascript and show a success modal
   useEffect(() => {
     if (queryParams.get("updateSuccess")) {
@@ -506,12 +678,12 @@ export default function KvmIdRoute() {
   useEffect(() => {
     if (!peerConnection) return;
     if (!kvmTerminal) {
-      console.log('Creating data channel "terminal"');
+      // console.log('Creating data channel "terminal"');
       setKvmTerminal(peerConnection.createDataChannel("terminal"));
     }
 
     if (!serialConsole) {
-      console.log('Creating data channel "serial"');
+      // console.log('Creating data channel "serial"');
       setSerialConsole(peerConnection.createDataChannel("serial"));
     }
   }, [kvmTerminal, peerConnection, serialConsole]);
@@ -554,6 +726,43 @@ export default function KvmIdRoute() {
     [send, setScrollSensitivity],
   );
 
+  const ConnectionStatusElement = useMemo(() => {
+    const hasConnectionFailed =
+      connectionFailed || ["failed", "closed"].includes(peerConnectionState || "");
+
+    const isPeerConnectionLoading =
+      ["connecting", "new"].includes(peerConnectionState || "") ||
+      peerConnection === null;
+
+    const isDisconnected = peerConnectionState === "disconnected";
+
+    const isOtherSession = location.pathname.includes("other-session");
+
+    if (isOtherSession) return null;
+    if (peerConnectionState === "connected") return null;
+    if (isDisconnected) {
+      return <PeerConnectionDisconnectedOverlay show={true} />;
+    }
+
+    if (hasConnectionFailed)
+      return (
+        <ConnectionFailedOverlay show={true} setupPeerConnection={setupPeerConnection} />
+      );
+
+    if (isPeerConnectionLoading) {
+      return <LoadingConnectionOverlay show={true} text={loadingMessage} />;
+    }
+
+    return null;
+  }, [
+    connectionFailed,
+    loadingMessage,
+    location.pathname,
+    peerConnection,
+    peerConnectionState,
+    setupPeerConnection,
+  ]);
+
   return (
     <FeatureFlagProvider appVersion={appVersion}>
       {!outlet && otaState.updating && (
@@ -593,27 +802,13 @@ export default function KvmIdRoute() {
           />
 
           <div className="flex h-full w-full overflow-hidden">
-            <div className="pointer-events-none fixed inset-0 isolate z-50 flex h-full w-full items-center justify-center">
+            <div className="pointer-events-none fixed inset-0 isolate z-20 flex h-full w-full items-center justify-center">
               <div className="my-2 h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
-                <LoadingConnectionOverlay
-                  show={
-                    !connectionFailed &&
-                    (["connecting", "new"].includes(
-                      peerConnection?.connectionState || "",
-                    ) ||
-                      peerConnection === null) &&
-                    !location.pathname.includes("other-session")
-                  }
-                  text={loadingMessage}
-                />
-                <ConnectionErrorOverlay
-                  show={connectionFailed && !location.pathname.includes("other-session")}
-                  setupPeerConnection={setupPeerConnection}
-                />
+                {!!ConnectionStatusElement && ConnectionStatusElement}
               </div>
             </div>
 
-            <WebRTCVideo />
+            {peerConnectionState === "connected" && <WebRTCVideo />}
             <SidebarContainer sidebarView={sidebarView} />
           </div>
         </div>
diff --git a/web.go b/web.go
index 9201e7b..c3f6d8d 100644
--- a/web.go
+++ b/web.go
@@ -1,6 +1,7 @@
 package kvm
 
 import (
+	"context"
 	"embed"
 	"encoding/json"
 	"fmt"
@@ -10,8 +11,12 @@ import (
 	"strings"
 	"time"
 
+	"github.com/coder/websocket"
+	"github.com/coder/websocket/wsjson"
 	"github.com/gin-gonic/gin"
 	"github.com/google/uuid"
+	"github.com/pion/webrtc/v4"
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"golang.org/x/crypto/bcrypt"
 )
@@ -94,7 +99,7 @@ func setupRouter() *gin.Engine {
 	protected := r.Group("/")
 	protected.Use(protectedMiddleware())
 	{
-		protected.POST("/webrtc/session", handleWebRTCSession)
+		protected.GET("/webrtc/signaling/client", handleLocalWebRTCSignal)
 		protected.POST("/cloud/register", handleCloudRegister)
 		protected.GET("/cloud/state", handleCloudState)
 		protected.GET("/device", handleDevice)
@@ -121,35 +126,182 @@ func setupRouter() *gin.Engine {
 // TODO: support multiple sessions?
 var currentSession *Session
 
-func handleWebRTCSession(c *gin.Context) {
-	var req WebRTCSessionRequest
-
-	if err := c.ShouldBindJSON(&req); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-		return
+func handleLocalWebRTCSignal(c *gin.Context) {
+	cloudLogger.Infof("new websocket connection established")
+	// Create WebSocket options with InsecureSkipVerify to bypass origin check
+	wsOptions := &websocket.AcceptOptions{
+		InsecureSkipVerify: true, // Allow connections from any origin
 	}
 
-	session, err := newSession(SessionConfig{})
+	wsCon, err := websocket.Accept(c.Writer, c.Request, wsOptions)
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": err})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
 		return
 	}
 
-	sd, err := session.ExchangeOffer(req.Sd)
+	// get the source from the request
+	source := c.ClientIP()
+
+	// Now use conn for websocket operations
+	defer wsCon.Close(websocket.StatusNormalClosure, "")
+
+	err = wsjson.Write(context.Background(), wsCon, gin.H{"type": "device-metadata", "data": gin.H{"deviceVersion": builtAppVersion}})
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": err})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
 		return
 	}
-	if currentSession != nil {
-		writeJSONRPCEvent("otherSessionConnected", nil, currentSession)
-		peerConn := currentSession.peerConnection
+
+	err = handleWebRTCSignalWsMessages(wsCon, false, source)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		return
+	}
+}
+
+func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, source string) error {
+	runCtx, cancelRun := context.WithCancel(context.Background())
+	defer cancelRun()
+
+	// Add connection tracking to detect reconnections
+	connectionID := uuid.New().String()
+	cloudLogger.Infof("new websocket connection established with ID: %s", connectionID)
+
+	// connection type
+	var sourceType string
+	if isCloudConnection {
+		sourceType = "cloud"
+	} else {
+		sourceType = "local"
+	}
+
+	// probably we can use a better logging framework here
+	logInfof := func(format string, args ...interface{}) {
+		args = append(args, source, sourceType)
+		websocketLogger.Infof(format+", source: %s, sourceType: %s", args...)
+	}
+	logWarnf := func(format string, args ...interface{}) {
+		args = append(args, source, sourceType)
+		websocketLogger.Warnf(format+", source: %s, sourceType: %s", args...)
+	}
+	logTracef := func(format string, args ...interface{}) {
+		args = append(args, source, sourceType)
+		websocketLogger.Tracef(format+", source: %s, sourceType: %s", args...)
+	}
+
+	go func() {
+		for {
+			time.Sleep(WebsocketPingInterval)
+
+			// set the timer for the ping duration
+			timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
+				metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(v)
+				metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v)
+			}))
+
+			logInfof("pinging websocket")
+			err := wsCon.Ping(runCtx)
+
+			if err != nil {
+				logWarnf("websocket ping error: %v", err)
+				cancelRun()
+				return
+			}
+
+			// dont use `defer` here because we want to observe the duration of the ping
+			timer.ObserveDuration()
+
+			metricConnectionTotalPingCount.WithLabelValues(sourceType, source).Inc()
+			metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
+		}
+	}()
+
+	if isCloudConnection {
+		// create a channel to receive the disconnect event, once received, we cancelRun
+		cloudDisconnectChan = make(chan error)
+		defer func() {
+			close(cloudDisconnectChan)
+			cloudDisconnectChan = nil
+		}()
 		go func() {
-			time.Sleep(1 * time.Second)
-			_ = peerConn.Close()
+			for err := range cloudDisconnectChan {
+				if err == nil {
+					continue
+				}
+				cloudLogger.Infof("disconnecting from cloud due to: %v", err)
+				cancelRun()
+			}
 		}()
 	}
-	currentSession = session
-	c.JSON(http.StatusOK, gin.H{"sd": sd})
+
+	for {
+		typ, msg, err := wsCon.Read(runCtx)
+		if err != nil {
+			logWarnf("websocket read error: %v", err)
+			return err
+		}
+		if typ != websocket.MessageText {
+			// ignore non-text messages
+			continue
+		}
+
+		var message struct {
+			Type string          `json:"type"`
+			Data json.RawMessage `json:"data"`
+		}
+
+		err = json.Unmarshal(msg, &message)
+		if err != nil {
+			logWarnf("unable to parse ws message: %v", err)
+			continue
+		}
+
+		if message.Type == "offer" {
+			logInfof("new session request received")
+			var req WebRTCSessionRequest
+			err = json.Unmarshal(message.Data, &req)
+			if err != nil {
+				logWarnf("unable to parse session request data: %v", err)
+				continue
+			}
+
+			logInfof("new session request: %v", req.OidcGoogle)
+			logTracef("session request info: %v", req)
+
+			metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc()
+			metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
+			err = handleSessionRequest(runCtx, wsCon, req, isCloudConnection, source)
+			if err != nil {
+				logWarnf("error starting new session: %v", err)
+				continue
+			}
+		} else if message.Type == "new-ice-candidate" {
+			logInfof("The client sent us a new ICE candidate: %v", string(message.Data))
+			var candidate webrtc.ICECandidateInit
+
+			// Attempt to unmarshal as a ICECandidateInit
+			if err := json.Unmarshal(message.Data, &candidate); err != nil {
+				logWarnf("unable to parse incoming ICE candidate data: %v", string(message.Data))
+				continue
+			}
+
+			if candidate.Candidate == "" {
+				logWarnf("empty incoming ICE candidate, skipping")
+				continue
+			}
+
+			logInfof("unmarshalled incoming ICE candidate: %v", candidate)
+
+			if currentSession == nil {
+				logInfof("no current session, skipping incoming ICE candidate")
+				continue
+			}
+
+			logInfof("adding incoming ICE candidate to current session: %v", candidate)
+			if err = currentSession.peerConnection.AddICECandidate(candidate); err != nil {
+				logWarnf("failed to add incoming ICE candidate to our peer connection: %v", err)
+			}
+		}
+	}
 }
 
 func handleLogin(c *gin.Context) {
diff --git a/webrtc.go b/webrtc.go
index 12d4f95..a047ecc 100644
--- a/webrtc.go
+++ b/webrtc.go
@@ -1,11 +1,15 @@
 package kvm
 
 import (
+	"context"
 	"encoding/base64"
 	"encoding/json"
 	"net"
 	"strings"
 
+	"github.com/coder/websocket"
+	"github.com/coder/websocket/wsjson"
+	"github.com/gin-gonic/gin"
 	"github.com/pion/webrtc/v4"
 )
 
@@ -23,6 +27,7 @@ type SessionConfig struct {
 	ICEServers []string
 	LocalIP    string
 	IsCloud    bool
+	ws         *websocket.Conn
 }
 
 func (s *Session) ExchangeOffer(offerStr string) (string, error) {
@@ -46,19 +51,11 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
 		return "", err
 	}
 
-	// Create channel that is blocked until ICE Gathering is complete
-	gatherComplete := webrtc.GatheringCompletePromise(s.peerConnection)
-
 	// Sets the LocalDescription, and starts our UDP listeners
 	if err = s.peerConnection.SetLocalDescription(answer); err != nil {
 		return "", err
 	}
 
-	// Block until ICE Gathering is complete, disabling trickle ICE
-	// we do this because we only can exchange one signaling message
-	// in a production application you should exchange ICE Candidates via OnICECandidate
-	<-gatherComplete
-
 	localDescription, err := json.Marshal(s.peerConnection.LocalDescription())
 	if err != nil {
 		return "", err
@@ -144,6 +141,16 @@ func newSession(config SessionConfig) (*Session, error) {
 	}()
 	var isConnected bool
 
+	peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
+		logger.Infof("Our WebRTC peerConnection has a new ICE candidate: %v", candidate)
+		if candidate != nil {
+			err := wsjson.Write(context.Background(), config.ws, gin.H{"type": "new-ice-candidate", "data": candidate.ToJSON()})
+			if err != nil {
+				logger.Errorf("failed to write new-ice-candidate to WebRTC signaling channel: %v", err)
+			}
+		}
+	})
+
 	peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
 		logger.Infof("Connection State has changed %s", connectionState)
 		if connectionState == webrtc.ICEConnectionStateConnected {