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'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'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 {