From 28919bf37c01a24082429b6107b35009263a992b Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:25:13 +0100 Subject: [PATCH 1/8] fix: await sleep needs to be called inside async function (#946) --- ui/src/routes/devices.$id.settings.advanced.tsx | 3 ++- ui/src/routes/devices.$id.settings.general.reboot.tsx | 3 ++- ui/src/routes/devices.$id.settings.general.update.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx index dbc98a75..4c3c9e94 100644 --- a/ui/src/routes/devices.$id.settings.advanced.tsx +++ b/ui/src/routes/devices.$id.settings.advanced.tsx @@ -12,6 +12,7 @@ import { TextAreaWithLabel } from "@components/TextArea"; import { isOnDevice } from "@/main"; import notifications from "@/notifications"; import { m } from "@localizations/messages.js"; +import { sleep } from "@/utils"; export default function SettingsAdvancedRoute() { const { send } = useJsonRpc(); @@ -311,7 +312,7 @@ export default function SettingsAdvancedRoute() { size="SM" theme="light" text={m.advanced_reset_config_button()} - onClick={() => { + onClick={async () => { handleResetConfig(); // Add 2s delay between resetting the configuration and calling reload() to prevent reload from interrupting the RPC call to reset things. await sleep(2000); diff --git a/ui/src/routes/devices.$id.settings.general.reboot.tsx b/ui/src/routes/devices.$id.settings.general.reboot.tsx index 5a4474e6..fc0feeaa 100644 --- a/ui/src/routes/devices.$id.settings.general.reboot.tsx +++ b/ui/src/routes/devices.$id.settings.general.reboot.tsx @@ -4,12 +4,13 @@ import { useNavigate } from "react-router"; import { useJsonRpc } from "@hooks/useJsonRpc"; import { Button } from "@components/Button"; import { m } from "@localizations/messages.js"; +import { sleep } from "@/utils"; export default function SettingsGeneralRebootRoute() { const navigate = useNavigate(); const { send } = useJsonRpc(); - const onClose = useCallback(() => { + const onClose = useCallback(async () => { navigate(".."); // back to the devices.$id.settings page // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation. await sleep(1000); diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 3a52eee2..285ce940 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -21,7 +21,7 @@ export default function SettingsGeneralUpdateRoute() { const { setModalView, otaState } = useUpdateStore(); const { send } = useJsonRpc(); - const onClose = useCallback(() => { + const onClose = useCallback(async () => { navigate(".."); // back to the devices.$id.settings page // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation. await sleep(1000); From 5f15d8b2f6949173db3427013e41420cc34b6dd9 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 6 Nov 2025 11:12:19 +0100 Subject: [PATCH 2/8] refactor: More robust handling of jsonrpc calls (#915) Co-authored-by: Marc Brooks --- ui/src/hooks/stores.ts | 109 ++++++++++++++++++++-------------- ui/src/routes/devices.$id.tsx | 3 +- ui/src/utils/jsonrpc.ts | 57 ++++++++++++++---- 3 files changed, 110 insertions(+), 59 deletions(-) diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 06f29582..c56cb5f8 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -116,7 +116,7 @@ export interface RTCState { peerConnection: RTCPeerConnection | null; setPeerConnection: (pc: RTCState["peerConnection"]) => void; - setRpcDataChannel: (channel: RTCDataChannel) => void; + setRpcDataChannel: (channel: RTCDataChannel | null) => void; rpcDataChannel: RTCDataChannel | null; hidRpcDisabled: boolean; @@ -178,41 +178,42 @@ export const useRTCStore = create(set => ({ setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }), rpcDataChannel: null, - setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }), + setRpcDataChannel: channel => set({ rpcDataChannel: channel }), hidRpcDisabled: false, - setHidRpcDisabled: (disabled: boolean) => set({ hidRpcDisabled: disabled }), + setHidRpcDisabled: disabled => set({ hidRpcDisabled: disabled }), rpcHidProtocolVersion: null, - setRpcHidProtocolVersion: (version: number | null) => set({ rpcHidProtocolVersion: version }), + setRpcHidProtocolVersion: version => set({ rpcHidProtocolVersion: version }), rpcHidChannel: null, - setRpcHidChannel: (channel: RTCDataChannel) => set({ rpcHidChannel: channel }), + setRpcHidChannel: channel => set({ rpcHidChannel: channel }), rpcHidUnreliableChannel: null, - setRpcHidUnreliableChannel: (channel: RTCDataChannel) => set({ rpcHidUnreliableChannel: channel }), + setRpcHidUnreliableChannel: channel => set({ rpcHidUnreliableChannel: channel }), rpcHidUnreliableNonOrderedChannel: null, - setRpcHidUnreliableNonOrderedChannel: (channel: RTCDataChannel) => set({ rpcHidUnreliableNonOrderedChannel: channel }), + setRpcHidUnreliableNonOrderedChannel: channel => + set({ rpcHidUnreliableNonOrderedChannel: channel }), transceiver: null, - setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }), + setTransceiver: transceiver => set({ transceiver }), peerConnectionState: null, - setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }), + setPeerConnectionState: state => set({ peerConnectionState: state }), mediaStream: null, - setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }), + setMediaStream: stream => set({ mediaStream: stream }), videoStreamStats: null, - appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => set({ videoStreamStats: stats }), + appendVideoStreamStats: stats => set({ videoStreamStats: stats }), videoStreamStatsHistory: new Map(), isTurnServerInUse: false, - setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }), + setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }), inboundRtpStats: new Map(), - appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => { + appendInboundRtpStats: stats => { set(prevState => ({ inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats), })); @@ -220,7 +221,7 @@ export const useRTCStore = create(set => ({ clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }), candidatePairStats: new Map(), - appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => { + appendCandidatePairStats: stats => { set(prevState => ({ candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats), })); @@ -228,21 +229,21 @@ export const useRTCStore = create(set => ({ clearCandidatePairStats: () => set({ candidatePairStats: new Map() }), localCandidateStats: new Map(), - appendLocalCandidateStats: (stats: RTCIceCandidateStats) => { + appendLocalCandidateStats: stats => { set(prevState => ({ localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats), })); }, remoteCandidateStats: new Map(), - appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => { + appendRemoteCandidateStats: stats => { set(prevState => ({ remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats), })); }, diskDataChannelStats: new Map(), - appendDiskDataChannelStats: (stats: RTCDataChannelStats) => { + appendDiskDataChannelStats: stats => { set(prevState => ({ diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats), })); @@ -250,7 +251,7 @@ export const useRTCStore = create(set => ({ // Add these new properties to the store implementation terminalChannel: null, - setTerminalChannel: (channel: RTCDataChannel) => set({ terminalChannel: channel }), + setTerminalChannel: channel => set({ terminalChannel: channel }), })); export interface MouseMove { @@ -270,12 +271,20 @@ export interface MouseState { export const useMouseStore = create(set => ({ mouseX: 0, mouseY: 0, - setMouseMove: (move?: MouseMove) => set({ mouseMove: move }), - setMousePosition: (x: number, y: number) => set({ mouseX: x, mouseY: y }), + setMouseMove: move => set({ mouseMove: move }), + setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }), })); -export type HdmiStates = "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting"; -export type HdmiErrorStates = Extract +export type HdmiStates = + | "ready" + | "no_signal" + | "no_lock" + | "out_of_range" + | "connecting"; +export type HdmiErrorStates = Extract< + VideoState["hdmiState"], + "no_signal" | "no_lock" | "out_of_range" +>; export interface HdmiState { ready: boolean; @@ -290,10 +299,7 @@ export interface VideoState { setClientSize: (width: number, height: number) => void; setSize: (width: number, height: number) => void; hdmiState: HdmiStates; - setHdmiState: (state: { - ready: boolean; - error?: HdmiErrorStates; - }) => void; + setHdmiState: (state: { ready: boolean; error?: HdmiErrorStates }) => void; } export const useVideoStore = create(set => ({ @@ -304,7 +310,8 @@ export const useVideoStore = create(set => ({ clientHeight: 0, // The video element's client size - setClientSize: (clientWidth: number, clientHeight: number) => set({ clientWidth, clientHeight }), + setClientSize: (clientWidth: number, clientHeight: number) => + set({ clientWidth, clientHeight }), // Resolution setSize: (width: number, height: number) => set({ width, height }), @@ -451,13 +458,15 @@ export interface MountMediaState { export const useMountMediaStore = create(set => ({ remoteVirtualMediaState: null, - setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }), + setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => + set({ remoteVirtualMediaState: state }), modalView: "mode", setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }), isMountMediaDialogOpen: false, - setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => set({ isMountMediaDialogOpen: isOpen }), + setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => + set({ isMountMediaDialogOpen: isOpen }), uploadedFiles: [], addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) => @@ -474,7 +483,7 @@ export interface KeyboardLedState { compose: boolean; kana: boolean; shift: boolean; // Optional, as not all keyboards have a shift LED -}; +} export const hidKeyBufferSize = 6; export const hidErrorRollOver = 0x01; @@ -509,14 +518,23 @@ export interface HidState { } export const useHidStore = create(set => ({ - keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState, - setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }), + keyboardLedState: { + num_lock: false, + caps_lock: false, + scroll_lock: false, + compose: false, + kana: false, + shift: false, + } as KeyboardLedState, + setKeyboardLedState: (ledState: KeyboardLedState): void => + set({ keyboardLedState: ledState }), keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState, setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }), isVirtualKeyboardEnabled: false, - setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }), + setVirtualKeyboardEnabled: (enabled: boolean): void => + set({ isVirtualKeyboardEnabled: enabled }), isPasteInProgress: false, setPasteModeEnabled: (enabled: boolean): void => set({ isPasteInProgress: enabled }), @@ -568,7 +586,7 @@ export interface OtaState { systemUpdateProgress: number; systemUpdatedAt: string | null; -}; +} export interface UpdateState { isUpdatePending: boolean; @@ -580,7 +598,7 @@ export interface UpdateState { otaState: OtaState; setOtaState: (state: OtaState) => void; - modalView: UpdateModalViews + modalView: UpdateModalViews; setModalView: (view: UpdateModalViews) => void; updateErrorMessage: string | null; @@ -620,12 +638,11 @@ export const useUpdateStore = create(set => ({ setModalView: (view: UpdateModalViews) => set({ modalView: view }), updateErrorMessage: null, - setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }), + setUpdateErrorMessage: (errorMessage: string) => + set({ updateErrorMessage: errorMessage }), })); -export type UsbConfigModalViews = - | "updateUsbConfig" - | "updateUsbConfigSuccess"; +export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess"; export interface UsbConfigModalState { modalView: UsbConfigModalViews; @@ -833,12 +850,12 @@ export interface MacrosState { loadMacros: () => Promise; saveMacros: (macros: KeySequence[]) => Promise; sendFn: - | (( - method: string, - params: unknown, - callback?: ((resp: JsonRpcResponse) => void) | undefined, - ) => void) - | null; + | (( + method: string, + params: unknown, + callback?: ((resp: JsonRpcResponse) => void) | undefined, + ) => void) + | null; setSendFn: ( sendFn: ( method: string, @@ -978,5 +995,5 @@ export const useMacrosStore = create((set, get) => ({ } finally { set({ loading: false }); } - } + }, })); diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index bae8faa6..1e2bb6b3 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -557,8 +557,9 @@ export default function KvmIdRoute() { clearCandidatePairStats(); setSidebarView(null); setPeerConnection(null); + setRpcDataChannel(null); }; - }, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]); + }, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView, setRpcDataChannel]); // TURN server usage detection useEffect(() => { diff --git a/ui/src/utils/jsonrpc.ts b/ui/src/utils/jsonrpc.ts index 18659f00..ae97be13 100644 --- a/ui/src/utils/jsonrpc.ts +++ b/ui/src/utils/jsonrpc.ts @@ -24,17 +24,47 @@ export interface JsonRpcCallResponse { let rpcCallCounter = 0; // Helper: wait for RTC data channel to be ready +// This waits indefinitely for the channel to be ready, only aborting via the signal +// Throws if the channel instance changed while waiting (stale connection detected) async function waitForRtcReady(signal: AbortSignal): Promise { const pollInterval = 100; + let lastSeenChannel: RTCDataChannel | null = null; while (!signal.aborted) { const state = useRTCStore.getState(); - if (state.rpcDataChannel?.readyState === "open") { - return state.rpcDataChannel; + const currentChannel = state.rpcDataChannel; + + // Channel instance changed (new connection replaced old one) + if (lastSeenChannel && currentChannel && lastSeenChannel !== currentChannel) { + console.debug("[waitForRtcReady] Channel instance changed, aborting wait"); + throw new Error("RTC connection changed while waiting for readiness"); } + + // Channel was removed from store (connection closed) + if (lastSeenChannel && !currentChannel) { + console.debug("[waitForRtcReady] Channel was removed from store, aborting wait"); + throw new Error("RTC connection was closed while waiting for readiness"); + } + + // No channel yet, keep waiting + if (!currentChannel) { + await sleep(pollInterval); + continue; + } + + // Track this channel instance + lastSeenChannel = currentChannel; + + // Channel is ready! + if (currentChannel.readyState === "open") { + return currentChannel; + } + await sleep(pollInterval); } + // Signal was aborted for some reason + console.debug("[waitForRtcReady] Aborted via signal"); throw new Error("RTC readiness check aborted"); } @@ -97,25 +127,26 @@ export async function callJsonRpc( const timeout = options.attemptTimeoutMs || 5000; for (let attempt = 0; attempt < maxAttempts; attempt++) { - const abortController = new AbortController(); - const timeoutId = setTimeout(() => abortController.abort(), timeout); - // Exponential backoff for retries that starts at 500ms up to a maximum of 10 seconds const backoffMs = Math.min(500 * Math.pow(2, attempt), 10000); + let timeoutId: ReturnType | null = null; try { - // Wait for RTC readiness - const rpcDataChannel = await waitForRtcReady(abortController.signal); + // Wait for RTC readiness without timeout - this allows time for WebRTC to connect + const readyAbortController = new AbortController(); + const rpcDataChannel = await waitForRtcReady(readyAbortController.signal); + + // Now apply timeout only to the actual RPC request/response + const rpcAbortController = new AbortController(); + timeoutId = setTimeout(() => rpcAbortController.abort(), timeout); // Send RPC request and wait for response const response = await sendRpcRequest( rpcDataChannel, options, - abortController.signal, + rpcAbortController.signal, ); - clearTimeout(timeoutId); - // Retry on error if attempts remain if (response.error && attempt < maxAttempts - 1) { await sleep(backoffMs); @@ -124,8 +155,6 @@ export async function callJsonRpc( return response; } catch (error) { - clearTimeout(timeoutId); - // Retry on timeout/error if attempts remain if (attempt < maxAttempts - 1) { await sleep(backoffMs); @@ -135,6 +164,10 @@ export async function callJsonRpc( throw error instanceof Error ? error : new Error(`JSON-RPC call failed after ${timeout}ms`); + } finally { + if (timeoutId !== null) { + clearTimeout(timeoutId); + } } } From 36f06a064a2222c89cbe2e0fb3f5d676b963195f Mon Sep 17 00:00:00 2001 From: tadic-luka Date: Thu, 6 Nov 2025 21:39:22 +0100 Subject: [PATCH 3/8] feat: add web route for sending WOL package to given mac addr (#945) * feat: add web route for sending WOL package to given mac addr ``` adds a new route /device/send-wol/:mac-addr to send the magic WOL package to the specified mac-addr. Method is POST and is protected. Useful for custom wake up scripts: example is sending HTTP request through iOS shortcut Test plan: calling the API with curl ``` $ curl -X POST http:///device/send-wol/xx:xx:xx:xx:xx:xx WOL sent to xx:xx:xx:xx:xx:xx ``` and observing the magic packet on my laptop/PC: ``` $ ncat -u -l 9 -k | xxd 00000000: ffff ffff ffff d050 9978 a620 d050 9978 .......P.x. .P.x 00000010: a620 d050 9978 a620 d050 9978 a620 d050 . .P.x. .P.x. .P 00000020: 9978 a620 d050 9978 a620 d050 9978 a620 .x. .P.x. .P.x. 00000030: d050 9978 a620 d050 9978 a620 d050 9978 .P.x. .P.x. .P.x 00000040: a620 d050 9978 a620 d050 9978 a620 d050 . .P.x. .P.x. .P 00000050: 9978 a620 d050 9978 a620 d050 9978 a620 .x. .P.x. .P.x. ``` calling the api with invalid mac addr returns HTTP 400 error ``` $ curl -X POST -v http:///device/send-wol/abcd ... * Request completely sent off < HTTP/1.1 400 Bad Request ... ... Invalid mac address provided * Resolve golint complaint --------- Co-authored-by: Marc Brooks --- web.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/web.go b/web.go index 0fd968b8..bef5a741 100644 --- a/web.go +++ b/web.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io/fs" + "net" "net/http" "net/http/pprof" "path/filepath" @@ -184,6 +185,8 @@ func setupRouter() *gin.Engine { protected.PUT("/auth/password-local", handleUpdatePassword) protected.DELETE("/auth/local-password", handleDeletePassword) protected.POST("/storage/upload", handleUploadHttp) + + protected.POST("/device/send-wol/:mac-addr", handleSendWOLMagicPacket) } // Catch-all route for SPA @@ -341,7 +344,6 @@ func handleWebRTCSignalWsMessages( l.Trace().Msg("sending ping frame") err := wsCon.Ping(runCtx) - if err != nil { l.Warn().Str("error", err.Error()).Msg("websocket ping error") cancelRun() @@ -807,3 +809,23 @@ func handleSetup(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"}) } + +func handleSendWOLMagicPacket(c *gin.Context) { + inputMacAddr := c.Param("mac-addr") + macAddr, err := net.ParseMAC(inputMacAddr) + if err != nil { + logger.Warn().Err(err).Str("sendWol", inputMacAddr).Msg("Invalid mac address provided") + c.String(http.StatusBadRequest, "Invalid mac address provided") + return + } + + macAddrString := macAddr.String() + err = rpcSendWOLMagicPacket(macAddrString) + if err != nil { + logger.Warn().Err(err).Str("sendWOL", macAddrString).Msg("Failed to send WOL magic packet") + c.String(http.StatusInternalServerError, "Failed to send WOL to %s: %v", macAddrString, err) + return + } + + c.String(http.StatusOK, "WOL sent to %s ", macAddr) +} From 6e1b84f39b9de3f410a9d6ecf1dd43ed635dcbbb Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 7 Nov 2025 06:21:38 -0600 Subject: [PATCH 4/8] chore: clean up logger messaging --- web.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web.go b/web.go index bef5a741..667dee1c 100644 --- a/web.go +++ b/web.go @@ -814,7 +814,7 @@ func handleSendWOLMagicPacket(c *gin.Context) { inputMacAddr := c.Param("mac-addr") macAddr, err := net.ParseMAC(inputMacAddr) if err != nil { - logger.Warn().Err(err).Str("sendWol", inputMacAddr).Msg("Invalid mac address provided") + logger.Warn().Err(err).Str("inputMacAddr", inputMacAddr).Msg("Invalid MAC address provided") c.String(http.StatusBadRequest, "Invalid mac address provided") return } @@ -822,7 +822,7 @@ func handleSendWOLMagicPacket(c *gin.Context) { macAddrString := macAddr.String() err = rpcSendWOLMagicPacket(macAddrString) if err != nil { - logger.Warn().Err(err).Str("sendWOL", macAddrString).Msg("Failed to send WOL magic packet") + logger.Warn().Err(err).Str("macAddrString", macAddrString).Msg("Failed to send WOL magic packet") c.String(http.StatusInternalServerError, "Failed to send WOL to %s: %v", macAddrString, err) return } From 7f2dcc84b440c74ba4900b271ab5ecde93589157 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 10 Nov 2025 05:58:57 -0600 Subject: [PATCH 5/8] fix: don't reload page if we didn't attempt an upgrade. (#955) --- ui/src/hooks/stores.ts | 18 ++++++++++++------ .../devices.$id.settings.general.update.tsx | 17 +++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index c56cb5f8..0be28425 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -603,6 +603,9 @@ export interface UpdateState { updateErrorMessage: string | null; setUpdateErrorMessage: (errorMessage: string) => void; + + shouldReload: boolean; + setShouldReload: (reloadRequired: boolean) => void; } export const useUpdateStore = create(set => ({ @@ -640,6 +643,9 @@ export const useUpdateStore = create(set => ({ updateErrorMessage: null, setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }), + + shouldReload: false, + setShouldReload: (reloadRequired: boolean) => set({ shouldReload: reloadRequired }), })); export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess"; @@ -850,12 +856,12 @@ export interface MacrosState { loadMacros: () => Promise; saveMacros: (macros: KeySequence[]) => Promise; sendFn: - | (( - method: string, - params: unknown, - callback?: ((resp: JsonRpcResponse) => void) | undefined, - ) => void) - | null; + | (( + method: string, + params: unknown, + callback?: ((resp: JsonRpcResponse) => void) | undefined, + ) => void) + | null; setSendFn: ( sendFn: ( method: string, diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 285ce940..dd800011 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -18,20 +18,24 @@ export default function SettingsGeneralUpdateRoute() { const location = useLocation(); const { updateSuccess } = location.state || {}; - const { setModalView, otaState } = useUpdateStore(); + const { setModalView, otaState, shouldReload, setShouldReload } = useUpdateStore(); const { send } = useJsonRpc(); const onClose = useCallback(async () => { navigate(".."); // back to the devices.$id.settings page - // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation. - await sleep(1000); - window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded - }, [navigate]); + + if (shouldReload) { + setShouldReload(false); + await sleep(1000); // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation. + window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded + } + }, [navigate, setShouldReload, shouldReload]); const onConfirmUpdate = useCallback(() => { + setShouldReload(true); send("tryUpdate", {}); setModalView("updating"); - }, [send, setModalView]); + }, [send, setModalView, setShouldReload]); useEffect(() => { if (otaState.updating) { @@ -133,6 +137,7 @@ function LoadingState({ const { setModalView } = useUpdateStore(); const progressBarRef = useRef(null); + useEffect(() => { abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; From 31ea366e51f9fb641543d16b69630a3cc045e747 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 10 Nov 2025 07:53:18 -0600 Subject: [PATCH 6/8] chore: upgrade UI packages (except vite) (#954) --- ui/package-lock.json | 1004 +++++++++++++++++++----------------------- ui/package.json | 30 +- 2 files changed, 474 insertions(+), 560 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index c3f2ab35..437b0c93 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "kvm-ui", - "version": "2025.10.24.2140", + "version": "2025.11.07.2130", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kvm-ui", - "version": "2025.10.24.2140", + "version": "2025.11.07.2130", "dependencies": { "@headlessui/react": "^2.2.9", "@headlessui/tailwindcss": "^0.2.2", @@ -19,7 +19,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.4", - "dayjs": "^1.11.18", + "dayjs": "^1.11.19", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^11.0.4", "framer-motion": "^12.23.24", @@ -28,52 +28,52 @@ "react": "^19.2.0", "react-animate-height": "^3.2.3", "react-dom": "^19.2.0", - "react-hook-form": "^7.65.0", + "react-hook-form": "^7.66.0", "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "react-router": "^7.9.5", - "react-simple-keyboard": "^3.8.131", + "react-simple-keyboard": "^3.8.132", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^3.3.0", "tailwind-merge": "^3.3.1", "usehooks-ts": "^3.1.1", - "validator": "^13.15.15", + "validator": "^13.15.20", "zustand": "^4.5.2" }, "devDependencies": { "@eslint/compat": "^1.4.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.39.0", + "@eslint/js": "^9.39.1", "@inlang/cli": "^3.0.12", "@inlang/paraglide-js": "^2.4.0", "@inlang/plugin-m-function-matcher": "^2.1.0", "@inlang/plugin-message-format": "^4.0.0", "@inlang/sdk": "^2.4.9", "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/postcss": "^4.1.16", + "@tailwindcss/postcss": "^4.1.17", "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.16", + "@tailwindcss/vite": "^4.1.17", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@types/semver": "^7.7.1", - "@types/validator": "^13.15.3", - "@typescript-eslint/eslint-plugin": "^8.46.2", - "@typescript-eslint/parser": "^8.46.2", - "@vitejs/plugin-react-swc": "^4.2.0", + "@types/validator": "^13.15.4", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.46.3", + "@vitejs/plugin-react-swc": "^4.2.1", "autoprefixer": "^10.4.21", - "eslint": "^9.38.0", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.4.0", + "globals": "^16.5.0", "postcss": "^8.5.6", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.7.1", - "tailwindcss": "^4.1.16", + "tailwindcss": "^4.1.17", "typescript": "^5.9.3", "vite": "^7.1.12", "vite-tsconfig-paths": "^5.1.4" @@ -356,9 +356,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -372,9 +372,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -388,9 +388,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -404,9 +404,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -420,9 +420,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -436,9 +436,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -452,9 +452,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -468,9 +468,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -484,9 +484,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -500,9 +500,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -516,9 +516,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -532,9 +532,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -548,9 +548,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -564,9 +564,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -580,9 +580,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -596,9 +596,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -612,9 +612,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -628,9 +628,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -644,9 +644,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -660,9 +660,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -676,9 +676,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -692,9 +692,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -708,9 +708,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -724,9 +724,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -740,9 +740,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -756,9 +756,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -819,19 +819,6 @@ } } }, - "node_modules/@eslint/compat/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/config-array": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", @@ -847,21 +834,21 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -906,10 +893,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", - "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", - "dev": true, + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -928,12 +914,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1388,14 +1374,14 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz", - "integrity": "sha512-ZAYu/NXkl/OhqTz7rfPaAhY0+e8Fr15jqNxte/2exKUxvHyQ/hcqmdekiN1f+Lcw3pE+34FCgX+26zcUE3duCg==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", - "immer": "^10.0.3", + "immer": "^10.2.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" @@ -1414,16 +1400,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.43", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", - "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "version": "1.0.0-beta.46", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.46.tgz", + "integrity": "sha512-xMNwJo/pHkEP/mhNVnW+zUiJDle6/hxrwO0mfSJuEVRbBfgrJFuUSRoZx/nYUw5pCjrysl9OkNXCkAdih8GCnA==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.1.tgz", + "integrity": "sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==", "cpu": [ "arm" ], @@ -1434,9 +1420,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.1.tgz", + "integrity": "sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==", "cpu": [ "arm64" ], @@ -1447,9 +1433,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.1.tgz", + "integrity": "sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==", "cpu": [ "arm64" ], @@ -1460,9 +1446,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.1.tgz", + "integrity": "sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==", "cpu": [ "x64" ], @@ -1473,9 +1459,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.1.tgz", + "integrity": "sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==", "cpu": [ "arm64" ], @@ -1486,9 +1472,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.1.tgz", + "integrity": "sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==", "cpu": [ "x64" ], @@ -1499,9 +1485,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.1.tgz", + "integrity": "sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==", "cpu": [ "arm" ], @@ -1512,9 +1498,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.1.tgz", + "integrity": "sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==", "cpu": [ "arm" ], @@ -1525,9 +1511,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.1.tgz", + "integrity": "sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==", "cpu": [ "arm64" ], @@ -1538,9 +1524,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.1.tgz", + "integrity": "sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==", "cpu": [ "arm64" ], @@ -1551,9 +1537,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.1.tgz", + "integrity": "sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==", "cpu": [ "loong64" ], @@ -1564,9 +1550,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.1.tgz", + "integrity": "sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==", "cpu": [ "ppc64" ], @@ -1577,9 +1563,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.1.tgz", + "integrity": "sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==", "cpu": [ "riscv64" ], @@ -1590,9 +1576,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.1.tgz", + "integrity": "sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==", "cpu": [ "riscv64" ], @@ -1603,9 +1589,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.1.tgz", + "integrity": "sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==", "cpu": [ "s390x" ], @@ -1616,9 +1602,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.1.tgz", + "integrity": "sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==", "cpu": [ "x64" ], @@ -1629,9 +1615,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.1.tgz", + "integrity": "sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==", "cpu": [ "x64" ], @@ -1642,9 +1628,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.1.tgz", + "integrity": "sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==", "cpu": [ "arm64" ], @@ -1655,9 +1641,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.1.tgz", + "integrity": "sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==", "cpu": [ "arm64" ], @@ -1668,9 +1654,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.1.tgz", + "integrity": "sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==", "cpu": [ "ia32" ], @@ -1681,9 +1667,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.1.tgz", + "integrity": "sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==", "cpu": [ "x64" ], @@ -1694,9 +1680,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.1.tgz", + "integrity": "sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==", "cpu": [ "x64" ], @@ -1742,9 +1728,9 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.21.tgz", - "integrity": "sha512-umBaSb65O1v6Lt8RV3o5srw0nKr25amf/yRIGFPug63sAerL9n2UkmfGywA1l1aN81W7faXIynF0JmlQ2wPSdw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.0.tgz", + "integrity": "sha512-8SnJV+JV0rYbfSiEiUvYOmf62E7QwsEG+aZueqSlKoxFt0pw333+bgZSQXGUV6etXU88nxur0afVMaINujBMSw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1760,16 +1746,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.13.21", - "@swc/core-darwin-x64": "1.13.21", - "@swc/core-linux-arm-gnueabihf": "1.13.21", - "@swc/core-linux-arm64-gnu": "1.13.21", - "@swc/core-linux-arm64-musl": "1.13.21", - "@swc/core-linux-x64-gnu": "1.13.21", - "@swc/core-linux-x64-musl": "1.13.21", - "@swc/core-win32-arm64-msvc": "1.13.21", - "@swc/core-win32-ia32-msvc": "1.13.21", - "@swc/core-win32-x64-msvc": "1.13.21" + "@swc/core-darwin-arm64": "1.15.0", + "@swc/core-darwin-x64": "1.15.0", + "@swc/core-linux-arm-gnueabihf": "1.15.0", + "@swc/core-linux-arm64-gnu": "1.15.0", + "@swc/core-linux-arm64-musl": "1.15.0", + "@swc/core-linux-x64-gnu": "1.15.0", + "@swc/core-linux-x64-musl": "1.15.0", + "@swc/core-win32-arm64-msvc": "1.15.0", + "@swc/core-win32-ia32-msvc": "1.15.0", + "@swc/core-win32-x64-msvc": "1.15.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1781,9 +1767,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.21.tgz", - "integrity": "sha512-0jaz9r7f0PDK8OyyVooadv8dkFlQmVmBK6DtAnWSRjkCbNt4sdqsc9ZkyEDJXaxOVcMQ3pJx/Igniyw5xqACLw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.0.tgz", + "integrity": "sha512-TBKWkbnShnEjlIbO4/gfsrIgAqHBVqgPWLbWmPdZ80bF393yJcLgkrb7bZEnJs6FCbSSuGwZv2rx1jDR2zo6YA==", "cpu": [ "arm64" ], @@ -1798,9 +1784,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.21.tgz", - "integrity": "sha512-pLeZn+NTGa7oW/ysD6oM82BjKZl71WNJR9BKXRsOhrNQeUWv55DCoZT2P4DzeU5Xgjmos+iMoDLg/9R6Ngc0PA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.0.tgz", + "integrity": "sha512-f5JKL1v1H56CIZc1pVn4RGPOfnWqPwmuHdpf4wesvXunF1Bx85YgcspW5YxwqG5J9g3nPU610UFuExJXVUzOiQ==", "cpu": [ "x64" ], @@ -1815,9 +1801,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.21.tgz", - "integrity": "sha512-p9aYzTmP7qVDPkXxnbekOfbT11kxnPiuLrUbgpN/vn6sxXDCObMAiY63WlDR0IauBK571WUdmgb04goe/xTQWw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.0.tgz", + "integrity": "sha512-duK6nG+WyuunnfsfiTUQdzC9Fk8cyDLqT9zyXvY2i2YgDu5+BH5W6wM5O4mDNCU5MocyB/SuF5YDF7XySnowiQ==", "cpu": [ "arm" ], @@ -1832,9 +1818,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.21.tgz", - "integrity": "sha512-yRqFoGlCwEX1nS7OajBE23d0LPeONmFAgoe4rgRYvaUb60qGxIJoMMdvF2g3dum9ZyVDYAb3kP09hbXFbMGr4A==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.0.tgz", + "integrity": "sha512-ITe9iDtTRXM98B91rvyPP6qDVbhUBnmA/j4UxrHlMQ0RlwpqTjfZYZkD0uclOxSZ6qIrOj/X5CaoJlDUuQ0+Cw==", "cpu": [ "arm64" ], @@ -1849,9 +1835,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.21.tgz", - "integrity": "sha512-wu5EGA86gtdYMW69eU80jROzArzD3/6G6zzK0VVR+OFt/0zqbajiiszIpaniOVACObLfJEcShQ05B3q0+CpUEg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.0.tgz", + "integrity": "sha512-Q5ldc2bzriuzYEoAuqJ9Vr3FyZhakk5hiwDbniZ8tlEXpbjBhbOleGf9/gkhLaouDnkNUEazFW9mtqwUTRdh7Q==", "cpu": [ "arm64" ], @@ -1866,9 +1852,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.21.tgz", - "integrity": "sha512-AoGGVPNXH3C4S7WlJOxN1nGW5nj//J9uKysS7CIBotRmHXfHO4wPK3TVFRTA4cuouAWBBn7O8m3A99p/GR+iaw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.0.tgz", + "integrity": "sha512-pY4is+jEpOxlYCSnI+7N8Oxbap9TmTz5YT84tUvRTlOlTBwFAUlWFCX0FRwWJlsfP0TxbqhIe8dNNzlsEmJbXQ==", "cpu": [ "x64" ], @@ -1883,9 +1869,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.21.tgz", - "integrity": "sha512-cBy2amuDuxMZnEq16MqGu+DUlEFqI+7F/OACNlk7zEJKq48jJKGEMqJz3X2ucJE5jqUIg6Pos6Uo/y+vuWQymQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.0.tgz", + "integrity": "sha512-zYEt5eT8y8RUpoe7t5pjpoOdGu+/gSTExj8PV86efhj6ugB3bPlj3Y85ogdW3WMVXr4NvwqvzdaYGCZfXzSyVg==", "cpu": [ "x64" ], @@ -1900,9 +1886,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.21.tgz", - "integrity": "sha512-2xfR5gnqBGOMOlY3s1QiFTXZaivTILMwX67FD2uzT6OCbT/3lyAM/4+3BptBXD8pUkkOGMFLsdeHw4fbO1GrpQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.0.tgz", + "integrity": "sha512-zC1rmOgFH5v2BCbByOazEqs0aRNpTdLRchDExfcCfgKgeaD+IdpUOqp7i3VG1YzkcnbuZjMlXfM0ugpt+CddoA==", "cpu": [ "arm64" ], @@ -1917,9 +1903,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.21.tgz", - "integrity": "sha512-0pkpgKlBDwUImWTQxLakKbzZI6TIGVVAxk658oxrY8VK+hxRy2iezFY6m5Urmeds47M/cnW3dO+OY4C2caOF8A==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.0.tgz", + "integrity": "sha512-7t9U9KwMwQblkdJIH+zX1V4q1o3o41i0HNO+VlnAHT5o+5qHJ963PHKJ/pX3P2UlZnBCY465orJuflAN4rAP9A==", "cpu": [ "ia32" ], @@ -1934,9 +1920,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.13.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.21.tgz", - "integrity": "sha512-DAnIw2J95TOW4Kr7NBx12vlZPW3QndbpFMmuC7x+fPoozoLpEscaDkiYhk7/sTtY9pubPMfHFPBORlbqyQCfOQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.0.tgz", + "integrity": "sha512-VE0Zod5vcs8iMLT64m5QS1DlTMXJFI/qSgtMDRx8rtZrnjt6/9NW8XUaiPJuRu8GluEO1hmHoyf1qlbY19gGSQ==", "cpu": [ "x64" ], @@ -1990,9 +1976,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", - "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", "dev": true, "license": "MIT", "dependencies": { @@ -2000,39 +1986,39 @@ "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", - "magic-string": "^0.30.19", + "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.16" + "tailwindcss": "4.1.17" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", - "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", "dev": true, "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.16", - "@tailwindcss/oxide-darwin-arm64": "4.1.16", - "@tailwindcss/oxide-darwin-x64": "4.1.16", - "@tailwindcss/oxide-freebsd-x64": "4.1.16", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", - "@tailwindcss/oxide-linux-x64-musl": "4.1.16", - "@tailwindcss/oxide-wasm32-wasi": "4.1.16", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz", - "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", "cpu": [ "arm64" ], @@ -2047,9 +2033,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz", - "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", "cpu": [ "arm64" ], @@ -2064,9 +2050,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz", - "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", "cpu": [ "x64" ], @@ -2081,9 +2067,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz", - "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", "cpu": [ "x64" ], @@ -2098,9 +2084,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz", - "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", "cpu": [ "arm" ], @@ -2115,9 +2101,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz", - "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", "cpu": [ "arm64" ], @@ -2132,9 +2118,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz", - "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", "cpu": [ "arm64" ], @@ -2149,9 +2135,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz", - "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", "cpu": [ "x64" ], @@ -2166,9 +2152,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz", - "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", "cpu": [ "x64" ], @@ -2183,9 +2169,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz", - "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2201,8 +2187,8 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", @@ -2212,70 +2198,10 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.5.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.5.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", - "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", "cpu": [ "arm64" ], @@ -2290,9 +2216,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz", - "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", "cpu": [ "x64" ], @@ -2307,17 +2233,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.16.tgz", - "integrity": "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.16", - "@tailwindcss/oxide": "4.1.16", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", - "tailwindcss": "4.1.16" + "tailwindcss": "4.1.17" } }, "node_modules/@tailwindcss/typography": { @@ -2334,15 +2260,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz", - "integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", + "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.16", - "@tailwindcss/oxide": "4.1.16", - "tailwindcss": "4.1.16" + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -2488,24 +2414,24 @@ "license": "MIT" }, "node_modules/@types/validator": { - "version": "13.15.3", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", - "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "version": "13.15.4", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.4.tgz", + "integrity": "sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", - "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/type-utils": "8.46.2", - "@typescript-eslint/utils": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2519,7 +2445,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.2", + "@typescript-eslint/parser": "^8.46.3", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2535,16 +2461,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", - "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "debug": "^4.3.4" }, "engines": { @@ -2560,14 +2486,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", - "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", "debug": "^4.3.4" }, "engines": { @@ -2582,14 +2508,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", - "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2" + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2600,9 +2526,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", - "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", "dev": true, "license": "MIT", "engines": { @@ -2617,15 +2543,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", - "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2642,9 +2568,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", "dev": true, "license": "MIT", "engines": { @@ -2656,16 +2582,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", - "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.2", - "@typescript-eslint/tsconfig-utils": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2711,16 +2637,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", - "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2" + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2735,13 +2661,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", - "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/types": "8.46.3", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2778,13 +2704,13 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.2.0.tgz", - "integrity": "sha512-/tesahXD1qpkGC6FzMoFOJj0RyZdw9xLELOL+6jbElwmWfwOnIVy+IfpY+o9JfD9PKaR/Eyb6DNrvbXpuvA+8Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.2.1.tgz", + "integrity": "sha512-SIZ/XxeS2naLw4L2vVvpTyujM2OY+Rf+y6nWETqfoBrZpI3SFdyNJof3nQ8HbLhXJ1Eh9e9c0JGYC8GYPhLkCw==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.43", + "@rolldown/pluginutils": "1.0.0-beta.46", "@swc/core": "^1.13.5" }, "engines": { @@ -3136,9 +3062,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", - "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "version": "2.8.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", + "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3259,9 +3185,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "dev": true, "funding": [ { @@ -3612,9 +3538,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, "node_modules/debug": { @@ -3732,9 +3658,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.240", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", - "integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==", + "version": "1.5.248", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.248.tgz", + "integrity": "sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==", "dev": true, "license": "ISC" }, @@ -3933,9 +3859,9 @@ ] }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3945,32 +3871,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/esbuild-wasm": { @@ -4009,19 +3935,19 @@ } }, "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4342,18 +4268,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -4596,12 +4510,12 @@ "license": "ISC" }, "node_modules/focus-trap": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", - "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", + "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "license": "MIT", "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.3.0" } }, "node_modules/focus-trap-react": { @@ -4814,9 +4728,9 @@ } }, "node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -5008,9 +4922,9 @@ } }, "node_modules/immer": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", - "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -6022,9 +5936,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", - "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -6533,9 +6447,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.65.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", - "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", + "version": "7.66.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", + "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -6627,9 +6541,9 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.8.131", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.131.tgz", - "integrity": "sha512-gICYtaV38AU/E1PTTwzJOF6s5fu6Nu3GZQwnaSNB4VGOO3UwOn8rioDEFBLvjMWpP8kwfWp2of8xywY647rTxA==", + "version": "3.8.132", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.132.tgz", + "integrity": "sha512-GoXK+6SRu72Jn8qT8fy+PxstIdZEACyIi/7zy0qXcrB6EJaN6zZk0/w3Sv3ALLwXqQd/3t3yUL4DQOwoNO1cbw==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -6782,9 +6696,9 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.1.tgz", + "integrity": "sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6797,28 +6711,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.53.1", + "@rollup/rollup-android-arm64": "4.53.1", + "@rollup/rollup-darwin-arm64": "4.53.1", + "@rollup/rollup-darwin-x64": "4.53.1", + "@rollup/rollup-freebsd-arm64": "4.53.1", + "@rollup/rollup-freebsd-x64": "4.53.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.1", + "@rollup/rollup-linux-arm-musleabihf": "4.53.1", + "@rollup/rollup-linux-arm64-gnu": "4.53.1", + "@rollup/rollup-linux-arm64-musl": "4.53.1", + "@rollup/rollup-linux-loong64-gnu": "4.53.1", + "@rollup/rollup-linux-ppc64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-musl": "4.53.1", + "@rollup/rollup-linux-s390x-gnu": "4.53.1", + "@rollup/rollup-linux-x64-gnu": "4.53.1", + "@rollup/rollup-linux-x64-musl": "4.53.1", + "@rollup/rollup-openharmony-arm64": "4.53.1", + "@rollup/rollup-win32-arm64-msvc": "4.53.1", + "@rollup/rollup-win32-ia32-msvc": "4.53.1", + "@rollup/rollup-win32-x64-gnu": "4.53.1", + "@rollup/rollup-win32-x64-msvc": "4.53.1", "fsevents": "~2.3.2" } }, @@ -6918,9 +6832,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, "node_modules/set-function-length": { @@ -7269,9 +7183,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", - "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "license": "MIT" }, "node_modules/tapable": { @@ -7656,9 +7570,9 @@ } }, "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -7687,9 +7601,9 @@ } }, "node_modules/vite": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", - "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", diff --git a/ui/package.json b/ui/package.json index 6c1016d6..0f4d7f64 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "kvm-ui", "private": true, - "version": "2025.10.24.2140", + "version": "2025.11.07.2130", "type": "module", "engines": { "node": "^22.20.0" @@ -38,7 +38,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.4", - "dayjs": "^1.11.18", + "dayjs": "^1.11.19", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^11.0.4", "framer-motion": "^12.23.24", @@ -47,52 +47,52 @@ "react": "^19.2.0", "react-animate-height": "^3.2.3", "react-dom": "^19.2.0", - "react-hook-form": "^7.65.0", + "react-hook-form": "^7.66.0", "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "react-router": "^7.9.5", - "react-simple-keyboard": "^3.8.131", + "react-simple-keyboard": "^3.8.132", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^3.3.0", "tailwind-merge": "^3.3.1", "usehooks-ts": "^3.1.1", - "validator": "^13.15.15", + "validator": "^13.15.20", "zustand": "^4.5.2" }, "devDependencies": { "@eslint/compat": "^1.4.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.39.0", + "@eslint/js": "^9.39.1", "@inlang/cli": "^3.0.12", "@inlang/paraglide-js": "^2.4.0", "@inlang/plugin-m-function-matcher": "^2.1.0", "@inlang/plugin-message-format": "^4.0.0", "@inlang/sdk": "^2.4.9", "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/postcss": "^4.1.16", + "@tailwindcss/postcss": "^4.1.17", "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.16", + "@tailwindcss/vite": "^4.1.17", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@types/semver": "^7.7.1", - "@types/validator": "^13.15.3", - "@typescript-eslint/eslint-plugin": "^8.46.2", - "@typescript-eslint/parser": "^8.46.2", - "@vitejs/plugin-react-swc": "^4.2.0", + "@types/validator": "^13.15.4", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.46.3", + "@vitejs/plugin-react-swc": "^4.2.1", "autoprefixer": "^10.4.21", - "eslint": "^9.38.0", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.4.0", + "globals": "^16.5.0", "postcss": "^8.5.6", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.7.1", - "tailwindcss": "^4.1.16", + "tailwindcss": "^4.1.17", "typescript": "^5.9.3", "vite": "^7.1.12", "vite-tsconfig-paths": "^5.1.4" From cf629bed461380dd5af8cf9c70f5aaee4f3e4136 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:00:37 +0100 Subject: [PATCH 7/8] feat: add remote IP address to connection stats sidebar (#944) --- .vscode/settings.json | 3 +- jsonrpc.go | 6 + main.go | 1 + network.go | 71 +++++++ pkg/myip/check.go | 160 ++++++++++++++ pkg/myip/ip.go | 196 ++++++++++++++++++ ui/localization/messages/en.json | 8 +- ui/src/components/PublicIPCard.tsx | 106 ++++++++++ ui/src/components/sidebar/connectionStats.tsx | 32 +++ ui/src/hooks/stores.ts | 5 + .../routes/devices.$id.settings.network.tsx | 41 ++-- 11 files changed, 608 insertions(+), 21 deletions(-) create mode 100644 pkg/myip/check.go create mode 100644 pkg/myip/ip.go create mode 100644 ui/src/components/PublicIPCard.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index ba3550bf..41aeee58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ ] }, "git.ignoreLimitWarning": true, - "cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo" + "cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo", + "cmake.ignoreCMakeListsMissing": true } \ No newline at end of file diff --git a/jsonrpc.go b/jsonrpc.go index 5ed90a7a..46d03864 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -932,6 +932,10 @@ func rpcSetCloudUrl(apiUrl string, appUrl string) error { disconnectCloud(fmt.Errorf("cloud url changed from %s to %s", currentCloudURL, apiUrl)) } + if publicIPState != nil { + publicIPState.SetCloudflareEndpoint(apiUrl) + } + if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } @@ -1248,4 +1252,6 @@ var rpcHandlers = map[string]RPCHandler{ "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, + "getPublicIPAddresses": {Func: rpcGetPublicIPAddresses, Params: []string{"refresh"}}, + "checkPublicIPAddresses": {Func: rpcCheckPublicIPAddresses}, } diff --git a/main.go b/main.go index bcc2d73d..66545bf6 100644 --- a/main.go +++ b/main.go @@ -126,6 +126,7 @@ func Main() { // As websocket client already checks if the cloud token is set, we can start it here. go RunWebsocketClient() + initPublicIPState() initSerialPort() sigs := make(chan os.Signal, 1) diff --git a/network.go b/network.go index 846f41f1..3a2b24f9 100644 --- a/network.go +++ b/network.go @@ -3,12 +3,17 @@ package kvm import ( "context" "fmt" + "net" + "net/http" "reflect" + "time" "github.com/jetkvm/kvm/internal/confparser" "github.com/jetkvm/kvm/internal/mdns" "github.com/jetkvm/kvm/internal/network/types" + "github.com/jetkvm/kvm/pkg/myip" "github.com/jetkvm/kvm/pkg/nmlite" + "github.com/jetkvm/kvm/pkg/nmlite/link" ) const ( @@ -17,6 +22,7 @@ const ( var ( networkManager *nmlite.NetworkManager + publicIPState *myip.PublicIPState ) type RpcNetworkSettings struct { @@ -104,6 +110,13 @@ func triggerTimeSyncOnNetworkStateChange() { }() } +func setPublicIPReadyState(ipv4Ready, ipv6Ready bool) { + if publicIPState == nil { + return + } + publicIPState.SetIPv4AndIPv6(ipv4Ready, ipv6Ready) +} + func networkStateChanged(_ string, state types.InterfaceState) { // do not block the main thread go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed") @@ -117,6 +130,8 @@ func networkStateChanged(_ string, state types.InterfaceState) { triggerTimeSyncOnNetworkStateChange() } + setPublicIPReadyState(state.IPv4Ready, state.IPv6Ready) + // always restart mDNS when the network state changes if mDNS != nil { restartMdns() @@ -164,6 +179,40 @@ func initNetwork() error { return nil } +func initPublicIPState() { + // the feature will be only enabled if the cloud has been adopted + // due to privacy reasons + + // but it will be initialized anyway to avoid nil pointer dereferences + ps := myip.NewPublicIPState(&myip.PublicIPStateConfig{ + Logger: networkLogger, + CloudflareEndpoint: config.CloudURL, + APIEndpoint: "", + IPv4: false, + IPv6: false, + HttpClientGetter: func(family int) *http.Client { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = config.NetworkConfig.GetTransportProxyFunc() + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + netType := network + switch family { + case link.AfInet: + netType = "tcp4" + case link.AfInet6: + netType = "tcp6" + } + return (&net.Dialer{}).DialContext(ctx, netType, addr) + } + + return &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + }, + }) + publicIPState = ps +} + func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error { if nm == nil { return nil @@ -312,3 +361,25 @@ func rpcToggleDHCPClient() error { return rpcReboot(true) } + +func rpcGetPublicIPAddresses(refresh bool) ([]myip.PublicIP, error) { + if publicIPState == nil { + return nil, fmt.Errorf("public IP state not initialized") + } + + if refresh { + if err := publicIPState.ForceUpdate(); err != nil { + return nil, err + } + } + + return publicIPState.GetAddresses(), nil +} + +func rpcCheckPublicIPAddresses() error { + if publicIPState == nil { + return fmt.Errorf("public IP state not initialized") + } + + return publicIPState.ForceUpdate() +} diff --git a/pkg/myip/check.go b/pkg/myip/check.go new file mode 100644 index 00000000..86d3ba50 --- /dev/null +++ b/pkg/myip/check.go @@ -0,0 +1,160 @@ +package myip + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/jetkvm/kvm/pkg/nmlite/link" +) + +func (ps *PublicIPState) request(ctx context.Context, url string, family int) ([]byte, error) { + client := ps.httpClient(family) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error sending request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + return body, err +} + +// checkCloudflare uses cdn-cgi/trace to get the public IP address +func (ps *PublicIPState) checkCloudflare(ctx context.Context, family int) (*PublicIP, error) { + u, err := url.JoinPath(ps.cloudflareEndpoint, "/cdn-cgi/trace") + if err != nil { + return nil, fmt.Errorf("error joining path: %w", err) + } + + body, err := ps.request(ctx, u, family) + if err != nil { + return nil, err + } + + values := make(map[string]string) + for line := range strings.SplitSeq(string(body), "\n") { + key, value, ok := strings.Cut(line, "=") + if !ok { + continue + } + values[key] = value + } + + ps.lastUpdated = time.Now() + if ts, ok := values["ts"]; ok { + if ts, err := strconv.ParseFloat(ts, 64); err == nil { + ps.lastUpdated = time.Unix(int64(ts), 0) + } + } + + ipStr, ok := values["ip"] + if !ok { + return nil, fmt.Errorf("no IP address found") + } + + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("invalid IP address: %s", ipStr) + } + + return &PublicIP{ + IPAddress: ip, + LastUpdated: ps.lastUpdated, + }, nil +} + +// checkAPI uses the API endpoint to get the public IP address +func (ps *PublicIPState) checkAPI(_ context.Context, _ int) (*PublicIP, error) { + return nil, fmt.Errorf("not implemented") +} + +// checkIPs checks both IPv4 and IPv6 public IP addresses in parallel +// and updates the IPAddresses slice with the results +func (ps *PublicIPState) checkIPs(ctx context.Context, checkIPv4, checkIPv6 bool) error { + var wg sync.WaitGroup + var mu sync.Mutex + var ips []PublicIP + var errors []error + + checkFamily := func(family int, familyName string) { + wg.Add(1) + go func(f int, name string) { + defer wg.Done() + + ip, err := ps.checkIPForFamily(ctx, f) + mu.Lock() + defer mu.Unlock() + if err != nil { + errors = append(errors, fmt.Errorf("%s check failed: %w", name, err)) + return + } + if ip != nil { + ips = append(ips, *ip) + } + }(family, familyName) + } + + if checkIPv4 { + checkFamily(link.AfInet, "IPv4") + } + + if checkIPv6 { + checkFamily(link.AfInet6, "IPv6") + } + + wg.Wait() + + if len(ips) > 0 { + ps.mu.Lock() + defer ps.mu.Unlock() + + ps.addresses = ips + ps.lastUpdated = time.Now() + } + + if len(errors) > 0 && len(ips) == 0 { + return errors[0] + } + + return nil +} + +func (ps *PublicIPState) checkIPForFamily(ctx context.Context, family int) (*PublicIP, error) { + if ps.apiEndpoint != "" { + ip, err := ps.checkAPI(ctx, family) + if err == nil && ip != nil { + return ip, nil + } + } + + if ps.cloudflareEndpoint != "" { + ip, err := ps.checkCloudflare(ctx, family) + if err == nil && ip != nil { + return ip, nil + } + } + + return nil, fmt.Errorf("all IP check methods failed for family %d", family) +} diff --git a/pkg/myip/ip.go b/pkg/myip/ip.go new file mode 100644 index 00000000..15afc24e --- /dev/null +++ b/pkg/myip/ip.go @@ -0,0 +1,196 @@ +package myip + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "sync" + "time" + + "github.com/jetkvm/kvm/internal/logging" + "github.com/rs/zerolog" +) + +type PublicIP struct { + IPAddress net.IP `json:"ip"` + LastUpdated time.Time `json:"last_updated"` +} + +type HttpClientGetter func(family int) *http.Client + +type PublicIPState struct { + addresses []PublicIP + lastUpdated time.Time + + cloudflareEndpoint string // cdn-cgi/trace domain + apiEndpoint string // api endpoint + ipv4 bool + ipv6 bool + httpClient HttpClientGetter + logger *zerolog.Logger + + timer *time.Timer + ctx context.Context + cancel context.CancelFunc + mu sync.Mutex +} + +type PublicIPStateConfig struct { + CloudflareEndpoint string + APIEndpoint string + IPv4 bool + IPv6 bool + HttpClientGetter HttpClientGetter + Logger *zerolog.Logger +} + +func stripURLPath(s string) string { + parsed, err := url.Parse(s) + if err != nil { + return "" + } + scheme := parsed.Scheme + if scheme != "http" && scheme != "https" { + scheme = "https" + } + + return fmt.Sprintf("%s://%s", scheme, parsed.Host) +} + +// NewPublicIPState creates a new PublicIPState +func NewPublicIPState(config *PublicIPStateConfig) *PublicIPState { + if config.Logger == nil { + config.Logger = logging.GetSubsystemLogger("publicip") + } + + ctx, cancel := context.WithCancel(context.Background()) + ps := &PublicIPState{ + addresses: make([]PublicIP, 0), + lastUpdated: time.Now(), + cloudflareEndpoint: stripURLPath(config.CloudflareEndpoint), + apiEndpoint: config.APIEndpoint, + ipv4: config.IPv4, + ipv6: config.IPv6, + httpClient: config.HttpClientGetter, + ctx: ctx, + cancel: cancel, + logger: config.Logger, + } + // Start the timer automatically + ps.Start() + return ps +} + +// SetFamily sets if we need to track IPv4 and IPv6 public IP addresses +func (ps *PublicIPState) SetIPv4AndIPv6(ipv4, ipv6 bool) { + ps.mu.Lock() + defer ps.mu.Unlock() + + ps.ipv4 = ipv4 + ps.ipv6 = ipv6 +} + +// SetCloudflareEndpoint sets the Cloudflare endpoint +func (ps *PublicIPState) SetCloudflareEndpoint(endpoint string) { + ps.mu.Lock() + defer ps.mu.Unlock() + + ps.cloudflareEndpoint = stripURLPath(endpoint) +} + +// SetAPIEndpoint sets the API endpoint +func (ps *PublicIPState) SetAPIEndpoint(endpoint string) { + ps.mu.Lock() + defer ps.mu.Unlock() + + ps.apiEndpoint = endpoint +} + +// GetAddresses returns the public IP addresses +func (ps *PublicIPState) GetAddresses() []PublicIP { + ps.mu.Lock() + defer ps.mu.Unlock() + + return ps.addresses +} + +// Start starts the timer loop to check public IP addresses periodically +func (ps *PublicIPState) Start() { + ps.mu.Lock() + defer ps.mu.Unlock() + + // Stop any existing timer + if ps.timer != nil { + ps.timer.Stop() + } + + if ps.cancel != nil { + ps.cancel() + } + + // Create new context and cancel function + ps.ctx, ps.cancel = context.WithCancel(context.Background()) + + // Start the timer loop in a goroutine + go ps.timerLoop(ps.ctx) +} + +// Stop stops the timer loop +func (ps *PublicIPState) Stop() { + ps.mu.Lock() + defer ps.mu.Unlock() + + if ps.cancel != nil { + ps.cancel() + ps.cancel = nil + } + + if ps.timer != nil { + ps.timer.Stop() + ps.timer = nil + } +} + +// ForceUpdate forces an update of the public IP addresses +func (ps *PublicIPState) ForceUpdate() error { + return ps.checkIPs(context.Background(), true, true) +} + +// timerLoop runs the periodic IP check loop +func (ps *PublicIPState) timerLoop(ctx context.Context) { + timer := time.NewTimer(5 * time.Minute) + defer timer.Stop() + + // Store timer reference for Stop() to access + ps.mu.Lock() + ps.timer = timer + checkIPv4 := ps.ipv4 + checkIPv6 := ps.ipv6 + ps.mu.Unlock() + + // Perform initial check immediately + checkIPs := func() { + if err := ps.checkIPs(ctx, checkIPv4, checkIPv6); err != nil { + ps.logger.Error().Err(err).Msg("failed to check public IP addresses") + } + } + + checkIPs() + + for { + select { + case <-timer.C: + // Perform the check + checkIPs() + + // Reset the timer for the next check + timer.Reset(5 * time.Minute) + + case <-ctx.Done(): + // Timer was stopped + return + } + } +} diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 0356e8e5..06d2bc36 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -897,5 +897,11 @@ "wake_on_lan_invalid_mac": "Invalid MAC address", "wake_on_lan_magic_sent_success": "Magic Packet sent successfully", "welcome_to_jetkvm": "Welcome to JetKVM", - "welcome_to_jetkvm_description": "Control any computer remotely" + "welcome_to_jetkvm_description": "Control any computer remotely","connection_stats_remote_ip_address": "Remote IP Address", + "connection_stats_remote_ip_address_description": "The IP address of the remote device.", + "connection_stats_remote_ip_address_copy_error": "Failed to copy remote IP address", + "connection_stats_remote_ip_address_copy_success": "Remote IP address { ip } copied to clipboard", + "public_ip_card_header": "Public IP addresses", + "public_ip_card_refresh": "Refresh", + "public_ip_card_refresh_error": "Failed to refresh public IP addresses: {error}" } diff --git a/ui/src/components/PublicIPCard.tsx b/ui/src/components/PublicIPCard.tsx new file mode 100644 index 00000000..55c1c407 --- /dev/null +++ b/ui/src/components/PublicIPCard.tsx @@ -0,0 +1,106 @@ +import { LuRefreshCcw } from "react-icons/lu"; +import { useCallback, useEffect, useState } from "react"; + +import { Button } from "@components/Button"; +import { GridCard } from "@components/Card"; +import { PublicIP } from "@hooks/stores"; +import { m } from "@localizations/messages.js"; +import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; +import notifications from "@/notifications"; +import { formatters } from "@/utils"; + + +const TimeAgoLabel = ({ date }: { date: Date }) => { + const [timeAgo, setTimeAgo] = useState(formatters.timeAgo(date)); + useEffect(() => { + const interval = setInterval(() => { + setTimeAgo(formatters.timeAgo(date)); + }, 1000); + return () => clearInterval(interval); + }, [date]); + + return ( + + {timeAgo} + + ); +}; + +export default function PublicIPCard() { + const { send } = useJsonRpc(); + + const [publicIPs, setPublicIPs] = useState([]); + const refreshPublicIPs = useCallback(() => { + send("getPublicIPAddresses", { refresh: true }, (resp: JsonRpcResponse) => { + setPublicIPs([]); + if ("error" in resp) { + notifications.error(m.public_ip_card_refresh_error({ error: resp.error.data || m.unknown_error() })); + return; + } + const publicIPs = resp.result as PublicIP[]; + // sort the public IPs by IP address + // IPv6 addresses are sorted after IPv4 addresses + setPublicIPs(publicIPs.sort(({ ip: aIp }, { ip: bIp }) => { + const aIsIPv6 = aIp.includes(":"); + const bIsIPv6 = bIp.includes(":"); + if (aIsIPv6 && !bIsIPv6) return 1; + if (!aIsIPv6 && bIsIPv6) return -1; + return aIp.localeCompare(bIp); + })); + }); + }, [send, setPublicIPs]); + + useEffect(() => { + refreshPublicIPs(); + }, [refreshPublicIPs]); + + return ( + +
+
+
+

+ {m.public_ip_card_header()} +

+ +
+
+
+ {publicIPs.length === 0 ? ( +
+
+
+
+
+
+
+
+
+ ) : ( +
+
+ {publicIPs?.map(ip => ( +
+ + {ip.ip} + + {ip.last_updated && } +
+ ))} +
+
+ )} +
+
+ + ); +} diff --git a/ui/src/components/sidebar/connectionStats.tsx b/ui/src/components/sidebar/connectionStats.tsx index f10f906c..e140ad5c 100644 --- a/ui/src/components/sidebar/connectionStats.tsx +++ b/ui/src/components/sidebar/connectionStats.tsx @@ -1,4 +1,6 @@ import { useInterval } from "usehooks-ts"; +import { LuCopy } from "react-icons/lu"; +import { useState } from "react"; import { m } from "@localizations/messages.js"; import { useRTCStore, useUiStore } from "@hooks/stores"; @@ -6,6 +8,10 @@ import { createChartArray, Metric } from "@components/Metric"; import { SettingsSectionHeader } from "@components/SettingsSectionHeader"; import SidebarHeader from "@components/SidebarHeader"; import { someIterable } from "@/utils"; +import { GridCard } from "@components/Card"; +import { Button } from "@components/Button"; +import { useCopyToClipboard } from "@components/useCopyToClipBoard"; +import notifications from "@/notifications"; export default function ConnectionStatsSidebar() { const { sidebarView, setSidebarView } = useUiStore(); @@ -21,6 +27,8 @@ export default function ConnectionStatsSidebar() { appendDiskDataChannelStats, } = useRTCStore(); + const [remoteIPAddress, setRemoteIPAddress] = useState(null); + useInterval(function collectWebRTCStats() { (async () => { if (!mediaStream) return; @@ -49,6 +57,7 @@ export default function ConnectionStatsSidebar() { } else if (report.type === "remote-candidate") { if (successfulRemoteCandidateId === report.id) { appendRemoteCandidateStats(report); + setRemoteIPAddress(report.address); } } else if (report.type === "data-channel" && report.label === "disk") { appendDiskDataChannelStats(report); @@ -93,6 +102,8 @@ export default function ConnectionStatsSidebar() { return { date: d.date, metric: valueMs }; }); + const { copy } = useCopyToClipboard(); + return (
@@ -106,6 +117,27 @@ export default function ConnectionStatsSidebar() { title={m.connection_stats_connection()} description={m.connection_stats_connection_description()} /> + {remoteIPAddress && ( +
+
+ {m.connection_stats_remote_ip_address()} +
+
+ +
+ {remoteIPAddress} +
+
+
+
+ )} + +
{formState.isLoading ? ( @@ -540,25 +543,25 @@ export default function SettingsNetworkRoute() {
- { isLLDPAvailable && - ( -
- - - -
- ) + {isLLDPAvailable && + ( +
+ + + +
+ ) }
From 5fb4c629dd62de62ec38af7c2e59c089000b8828 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Tue, 11 Nov 2025 13:13:42 +0100 Subject: [PATCH 8/8] feat: failsafe mode (#952) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Adam Shiervani Co-authored-by: Marc Brooks --- cmd/main.go | 7 +- failsafe.go | 107 +++++++++ internal/native/cgo_linux.go | 127 +++++++++- internal/native/native.go | 9 + jsonrpc.go | 1 + log.go | 1 + main.go | 8 +- native.go | 1 + ui/src/components/FailSafeModeBanner.tsx | 30 +++ ui/src/components/FailSafeModeOverlay.tsx | 216 ++++++++++++++++++ ui/src/hooks/stores.ts | 12 + ui/src/hooks/useJsonRpc.ts | 43 +++- ui/src/index.css | 8 + .../devices.$id.settings.general.reboot.tsx | 47 +++- ui/src/routes/devices.$id.settings.tsx | 28 ++- ui/src/routes/devices.$id.tsx | 22 +- ui/src/ui.config.ts | 2 + video.go | 1 + webrtc.go | 3 + 19 files changed, 644 insertions(+), 29 deletions(-) create mode 100644 failsafe.go create mode 100644 ui/src/components/FailSafeModeBanner.tsx create mode 100644 ui/src/components/FailSafeModeOverlay.tsx diff --git a/cmd/main.go b/cmd/main.go index d9636088..9a1e1899 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -74,7 +74,12 @@ func supervise() error { // run the child binary cmd := exec.Command(binPath) - cmd.Env = append(os.Environ(), []string{envChildID + "=" + kvm.GetBuiltAppVersion()}...) + lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile) + + cmd.Env = append(os.Environ(), []string{ + fmt.Sprintf("%s=%s", envChildID, kvm.GetBuiltAppVersion()), + fmt.Sprintf("JETKVM_LAST_ERROR_PATH=%s", lastFilePath), + }...) cmd.Args = os.Args logFile, err := os.CreateTemp("", "jetkvm-stdout.log") diff --git a/failsafe.go b/failsafe.go new file mode 100644 index 00000000..3c6b3d3a --- /dev/null +++ b/failsafe.go @@ -0,0 +1,107 @@ +package kvm + +import ( + "fmt" + "os" + "strings" + "sync" +) + +const ( + failsafeDefaultLastCrashPath = "/userdata/jetkvm/crashdump/last-crash.log" + failsafeFile = "/userdata/jetkvm/.enablefailsafe" + failsafeLastCrashEnv = "JETKVM_LAST_ERROR_PATH" + failsafeEnv = "JETKVM_FORCE_FAILSAFE" +) + +var ( + failsafeOnce sync.Once + failsafeCrashLog = "" + failsafeModeActive = false + failsafeModeReason = "" +) + +type FailsafeModeNotification struct { + Active bool `json:"active"` + Reason string `json:"reason"` +} + +// this function has side effects and can be only executed once +func checkFailsafeReason() { + failsafeOnce.Do(func() { + // check if the failsafe environment variable is set + if os.Getenv(failsafeEnv) == "1" { + failsafeModeActive = true + failsafeModeReason = "failsafe_env_set" + return + } + + // check if the failsafe file exists + if _, err := os.Stat(failsafeFile); err == nil { + failsafeModeActive = true + failsafeModeReason = "failsafe_file_exists" + _ = os.Remove(failsafeFile) + return + } + + // get the last crash log path from the environment variable + lastCrashPath := os.Getenv(failsafeLastCrashEnv) + if lastCrashPath == "" { + lastCrashPath = failsafeDefaultLastCrashPath + } + + // check if the last crash log file exists + l := failsafeLogger.With().Str("path", lastCrashPath).Logger() + fi, err := os.Lstat(lastCrashPath) + if err != nil { + if !os.IsNotExist(err) { + l.Warn().Err(err).Msg("failed to stat last crash log") + } + return + } + + if fi.Mode()&os.ModeSymlink != os.ModeSymlink { + l.Warn().Msg("last crash log is not a symlink, ignoring") + return + } + + // open the last crash log file and find if it contains the string "panic" + content, err := os.ReadFile(lastCrashPath) + if err != nil { + l.Warn().Err(err).Msg("failed to read last crash log") + return + } + + // unlink the last crash log file + failsafeCrashLog = string(content) + _ = os.Remove(lastCrashPath) + + // TODO: read the goroutine stack trace and check which goroutine is panicking + if strings.Contains(failsafeCrashLog, "runtime.cgocall") { + failsafeModeActive = true + failsafeModeReason = "video" + return + } + }) +} + +func notifyFailsafeMode(session *Session) { + if !failsafeModeActive || session == nil { + return + } + + jsonRpcLogger.Info().Str("reason", failsafeModeReason).Msg("sending failsafe mode notification") + + writeJSONRPCEvent("failsafeMode", FailsafeModeNotification{ + Active: true, + Reason: failsafeModeReason, + }, session) +} + +func rpcGetFailsafeLogs() (string, error) { + if !failsafeModeActive { + return "", fmt.Errorf("failsafe mode is not active") + } + + return failsafeCrashLog, nil +} diff --git a/internal/native/cgo_linux.go b/internal/native/cgo_linux.go index 850da0e8..be1a5a36 100644 --- a/internal/native/cgo_linux.go +++ b/internal/native/cgo_linux.go @@ -1,5 +1,8 @@ //go:build linux +// TODO: use a generator to generate the cgo code for the native functions +// there's too much boilerplate code to write manually + package native import ( @@ -46,7 +49,17 @@ static inline void jetkvm_cgo_setup_rpc_handler() { */ import "C" -var cgoLock sync.Mutex +var ( + cgoLock sync.Mutex + cgoDisabled bool +) + +func setCgoDisabled(disabled bool) { + cgoLock.Lock() + defer cgoLock.Unlock() + + cgoDisabled = disabled +} //export jetkvm_go_video_state_handler func jetkvm_go_video_state_handler(state *C.jetkvm_video_state_t) { @@ -91,6 +104,10 @@ func jetkvm_go_rpc_handler(method *C.cchar_t, params *C.cchar_t) { var eventCodeToNameMap = map[int]string{} func uiEventCodeToName(code int) string { + if cgoDisabled { + return "" + } + name, ok := eventCodeToNameMap[code] if !ok { cCode := C.int(code) @@ -103,6 +120,10 @@ func uiEventCodeToName(code int) string { } func setUpNativeHandlers() { + if cgoDisabled { + return + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -114,6 +135,10 @@ func setUpNativeHandlers() { } func uiInit(rotation uint16) { + if cgoDisabled { + return + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -123,6 +148,10 @@ func uiInit(rotation uint16) { } func uiTick() { + if cgoDisabled { + return + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -130,6 +159,10 @@ func uiTick() { } func videoInit(factor float64) error { + if cgoDisabled { + return nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -143,6 +176,10 @@ func videoInit(factor float64) error { } func videoShutdown() { + if cgoDisabled { + return + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -150,6 +187,10 @@ func videoShutdown() { } func videoStart() { + if cgoDisabled { + return + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -157,6 +198,10 @@ func videoStart() { } func videoStop() { + if cgoDisabled { + return + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -164,6 +209,10 @@ func videoStop() { } func videoLogStatus() string { + if cgoDisabled { + return "" + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -174,6 +223,10 @@ func videoLogStatus() string { } func uiSetVar(name string, value string) { + if cgoDisabled { + return + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -187,6 +240,10 @@ func uiSetVar(name string, value string) { } func uiGetVar(name string) string { + if cgoDisabled { + return "" + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -197,6 +254,10 @@ func uiGetVar(name string) string { } func uiSwitchToScreen(screen string) { + if cgoDisabled { + return + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -206,6 +267,10 @@ func uiSwitchToScreen(screen string) { } func uiGetCurrentScreen() string { + if cgoDisabled { + return "" + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -214,6 +279,10 @@ func uiGetCurrentScreen() string { } func uiObjAddState(objName string, state string) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -226,6 +295,10 @@ func uiObjAddState(objName string, state string) (bool, error) { } func uiObjClearState(objName string, state string) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -238,6 +311,10 @@ func uiObjClearState(objName string, state string) (bool, error) { } func uiGetLVGLVersion() string { + if cgoDisabled { + return "" + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -246,6 +323,10 @@ func uiGetLVGLVersion() string { // TODO: use Enum instead of string but it's not a hot path and performance is not a concern now func uiObjAddFlag(objName string, flag string) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -258,6 +339,10 @@ func uiObjAddFlag(objName string, flag string) (bool, error) { } func uiObjClearFlag(objName string, flag string) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -278,6 +363,10 @@ func uiObjShow(objName string) (bool, error) { } func uiObjSetOpacity(objName string, opacity int) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -289,6 +378,10 @@ func uiObjSetOpacity(objName string, opacity int) (bool, error) { } func uiObjFadeIn(objName string, duration uint32) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -301,6 +394,10 @@ func uiObjFadeIn(objName string, duration uint32) (bool, error) { } func uiObjFadeOut(objName string, duration uint32) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -313,6 +410,10 @@ func uiObjFadeOut(objName string, duration uint32) (bool, error) { } func uiLabelSetText(objName string, text string) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -330,6 +431,10 @@ func uiLabelSetText(objName string, text string) (bool, error) { } func uiImgSetSrc(objName string, src string) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -345,6 +450,10 @@ func uiImgSetSrc(objName string, src string) (bool, error) { } func uiDispSetRotation(rotation uint16) (bool, error) { + if cgoDisabled { + return false, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -357,6 +466,10 @@ func uiDispSetRotation(rotation uint16) (bool, error) { } func videoGetStreamQualityFactor() (float64, error) { + if cgoDisabled { + return 0, nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -365,6 +478,10 @@ func videoGetStreamQualityFactor() (float64, error) { } func videoSetStreamQualityFactor(factor float64) error { + if cgoDisabled { + return nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -373,6 +490,10 @@ func videoSetStreamQualityFactor(factor float64) error { } func videoGetEDID() (string, error) { + if cgoDisabled { + return "", nil + } + cgoLock.Lock() defer cgoLock.Unlock() @@ -381,6 +502,10 @@ func videoGetEDID() (string, error) { } func videoSetEDID(edid string) error { + if cgoDisabled { + return nil + } + cgoLock.Lock() defer cgoLock.Unlock() diff --git a/internal/native/native.go b/internal/native/native.go index 3b1cc0b4..cb8761cf 100644 --- a/internal/native/native.go +++ b/internal/native/native.go @@ -9,6 +9,7 @@ import ( ) type Native struct { + disable bool ready chan struct{} l *zerolog.Logger lD *zerolog.Logger @@ -27,6 +28,7 @@ type Native struct { } type NativeOptions struct { + Disable bool SystemVersion *semver.Version AppVersion *semver.Version DisplayRotation uint16 @@ -74,6 +76,7 @@ func NewNative(opts NativeOptions) *Native { } return &Native{ + disable: opts.Disable, ready: make(chan struct{}), l: nativeLogger, lD: displayLogger, @@ -92,6 +95,12 @@ func NewNative(opts NativeOptions) *Native { } func (n *Native) Start() { + if n.disable { + nativeLogger.Warn().Msg("native is disabled, skipping initialization") + setCgoDisabled(true) + return + } + // set up singleton setInstance(n) setUpNativeHandlers() diff --git a/jsonrpc.go b/jsonrpc.go index 46d03864..d30f6669 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1252,6 +1252,7 @@ var rpcHandlers = map[string]RPCHandler{ "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, + "getFailSafeLogs": {Func: rpcGetFailsafeLogs}, "getPublicIPAddresses": {Func: rpcGetPublicIPAddresses, Params: []string{"refresh"}}, "checkPublicIPAddresses": {Func: rpcCheckPublicIPAddresses}, } diff --git a/log.go b/log.go index 2047bbfa..9cd9188e 100644 --- a/log.go +++ b/log.go @@ -11,6 +11,7 @@ func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error { var ( logger = logging.GetSubsystemLogger("jetkvm") + failsafeLogger = logging.GetSubsystemLogger("failsafe") networkLogger = logging.GetSubsystemLogger("network") cloudLogger = logging.GetSubsystemLogger("cloud") websocketLogger = logging.GetSubsystemLogger("websocket") diff --git a/main.go b/main.go index 66545bf6..d7ecc3e1 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,12 @@ var appCtx context.Context func Main() { logger.Log().Msg("JetKVM Starting Up") + + checkFailsafeReason() + if failsafeModeActive { + logger.Warn().Str("reason", failsafeModeReason).Msg("failsafe mode activated") + } + LoadConfig() var cancel context.CancelFunc @@ -50,6 +56,7 @@ func Main() { // Initialize network if err := initNetwork(); err != nil { logger.Error().Err(err).Msg("failed to initialize network") + // TODO: reset config to default os.Exit(1) } @@ -60,7 +67,6 @@ func Main() { // Initialize mDNS if err := initMdns(); err != nil { logger.Error().Err(err).Msg("failed to initialize mDNS") - os.Exit(1) } initPrometheus() diff --git a/native.go b/native.go index 4a523bce..81a0e50d 100644 --- a/native.go +++ b/native.go @@ -17,6 +17,7 @@ var ( func initNative(systemVersion *semver.Version, appVersion *semver.Version) { nativeInstance = native.NewNative(native.NativeOptions{ + Disable: failsafeModeActive, SystemVersion: systemVersion, AppVersion: appVersion, DisplayRotation: config.GetDisplayRotation(), diff --git a/ui/src/components/FailSafeModeBanner.tsx b/ui/src/components/FailSafeModeBanner.tsx new file mode 100644 index 00000000..04fddc67 --- /dev/null +++ b/ui/src/components/FailSafeModeBanner.tsx @@ -0,0 +1,30 @@ +import { LuTriangleAlert } from "react-icons/lu"; + +import Card from "@components/Card"; + +interface FailsafeModeBannerProps { + reason: string; +} + +export function FailsafeModeBanner({ reason }: FailsafeModeBannerProps) { + const getReasonMessage = () => { + switch (reason) { + case "video": + return "Failsafe Mode Active: Video-related settings are currently unavailable"; + default: + return "Failsafe Mode Active: Some settings may be unavailable"; + } + }; + + return ( + +
+ +

+ {getReasonMessage()} +

+
+
+ ); +} + diff --git a/ui/src/components/FailSafeModeOverlay.tsx b/ui/src/components/FailSafeModeOverlay.tsx new file mode 100644 index 00000000..eadc5d9d --- /dev/null +++ b/ui/src/components/FailSafeModeOverlay.tsx @@ -0,0 +1,216 @@ +import { useState } from "react"; +import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; +import { motion, AnimatePresence } from "framer-motion"; +import { LuInfo } from "react-icons/lu"; + +import { Button } from "@/components/Button"; +import Card, { GridCard } from "@components/Card"; +import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; +import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; +import { useVersion } from "@/hooks/useVersion"; +import { useDeviceStore } from "@/hooks/stores"; +import notifications from "@/notifications"; +import { DOWNGRADE_VERSION } from "@/ui.config"; + +import { GitHubIcon } from "./Icons"; + + + +interface FailSafeModeOverlayProps { + reason: string; +} + +interface OverlayContentProps { + readonly children: React.ReactNode; +} + +function OverlayContent({ children }: OverlayContentProps) { + return ( + +
+ {children} +
+
+ ); +} + +interface TooltipProps { + readonly children: React.ReactNode; + readonly text: string; + readonly show: boolean; +} + +function Tooltip({ children, text, show }: TooltipProps) { + if (!show) { + return <>{children}; + } + + + return ( +
+ {children} +
+ +
+ + {text} +
+
+
+
+ ); +} + +export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) { + const { send } = useJsonRpc(); + const { navigateTo } = useDeviceUiNavigation(); + const { appVersion } = useVersion(); + const { systemVersion } = useDeviceStore(); + const [isDownloadingLogs, setIsDownloadingLogs] = useState(false); + const [hasDownloadedLogs, setHasDownloadedLogs] = useState(false); + + const getReasonCopy = () => { + switch (reason) { + case "video": + return { + message: + "We've detected an issue with the video capture process. Your device is still running and accessible, but video streaming is temporarily unavailable.", + }; + default: + return { + message: + "A critical process has encountered an issue. Your device is still accessible, but some functionality may be temporarily unavailable.", + }; + } + }; + + const { message } = getReasonCopy(); + + const handleReportAndDownloadLogs = () => { + setIsDownloadingLogs(true); + + send("getFailSafeLogs", {}, async (resp: JsonRpcResponse) => { + setIsDownloadingLogs(false); + + if ("error" in resp) { + notifications.error(`Failed to get recovery logs: ${resp.error.message}`); + return; + } + + // Download logs + const logContent = resp.result as string; + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const filename = `jetkvm-recovery-${reason}-${timestamp}.txt`; + + const blob = new Blob([logContent], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + await new Promise(resolve => setTimeout(resolve, 1000)); + a.click(); + await new Promise(resolve => setTimeout(resolve, 1000)); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + notifications.success("Crash logs downloaded successfully"); + setHasDownloadedLogs(true); + + // Open GitHub issue + const issueBody = `## Issue Description +The \`${reason}\` process encountered an error and failsafe mode was activated. + +**Reason:** \`${reason}\` +**Timestamp:** ${new Date().toISOString()} +**App Version:** ${appVersion || "Unknown"} +**System Version:** ${systemVersion || "Unknown"} + +## Logs +Please attach the recovery logs file that was downloaded to your computer: +\`${filename}\` + +> [!NOTE] +> Please remove any sensitive information from the logs. The reports are public and can be viewed by anyone. + +## Additional Context +[Please describe what you were doing when this occurred]`; + + const issueUrl = + `https://github.com/jetkvm/kvm/issues/new?` + + `title=${encodeURIComponent(`Recovery Mode: ${reason} process issue`)}&` + + `body=${encodeURIComponent(issueBody)}`; + + window.open(issueUrl, "_blank"); + }); + }; + + const handleDowngrade = () => { + navigateTo(`/settings/general/update?app=${DOWNGRADE_VERSION}`); + }; + + return ( + + + +
+ +
+
+
+

Fail safe mode activated

+

{message}

+
+
+
+
+ + +
+
+
+
+
+
+
+ ); +} + diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 56acc828..4c3252d6 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -1008,3 +1008,15 @@ export const useMacrosStore = create((set, get) => ({ } }, })); + +export interface FailsafeModeState { + isFailsafeMode: boolean; + reason: string; // "video", "network", etc. + setFailsafeMode: (active: boolean, reason: string) => void; +} + +export const useFailsafeModeStore = create(set => ({ + isFailsafeMode: false, + reason: "", + setFailsafeMode: (active, reason) => set({ isFailsafeMode: active, reason }), +})); diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts index 2ff862b9..e01dcbc7 100644 --- a/ui/src/hooks/useJsonRpc.ts +++ b/ui/src/hooks/useJsonRpc.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect } from "react"; -import { useRTCStore } from "@hooks/stores"; +import { useRTCStore, useFailsafeModeStore } from "@hooks/stores"; export interface JsonRpcRequest { jsonrpc: string; @@ -34,12 +34,51 @@ export const RpcMethodNotFound = -32601; const callbackStore = new Map void>(); let requestCounter = 0; +// Map of blocked RPC methods by failsafe reason +const blockedMethodsByReason: Record = { + video: [ + 'setStreamQualityFactor', + 'getEDID', + 'setEDID', + 'getVideoLogStatus', + 'setDisplayRotation', + 'getVideoSleepMode', + 'setVideoSleepMode', + 'getVideoState', + ], +}; + export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) { const { rpcDataChannel } = useRTCStore(); + const { isFailsafeMode, reason } = useFailsafeModeStore(); const send = useCallback( async (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => { if (rpcDataChannel?.readyState !== "open") return; + + // Check if method is blocked in failsafe mode + if (isFailsafeMode && reason) { + const blockedMethods = blockedMethodsByReason[reason] || []; + if (blockedMethods.includes(method)) { + console.warn(`RPC method "${method}" is blocked in failsafe mode (reason: ${reason})`); + + // Call callback with error if provided + if (callback) { + const errorResponse: JsonRpcErrorResponse = { + jsonrpc: "2.0", + error: { + code: -32000, + message: "Method unavailable in failsafe mode", + data: `This feature is unavailable while in failsafe mode (${reason})`, + }, + id: requestCounter + 1, + }; + callback(errorResponse); + } + return; + } + } + requestCounter++; const payload = { jsonrpc: "2.0", method, params, id: requestCounter }; // Store the callback if it exists @@ -47,7 +86,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) { rpcDataChannel.send(JSON.stringify(payload)); }, - [rpcDataChannel] + [rpcDataChannel, isFailsafeMode, reason] ); useEffect(() => { diff --git a/ui/src/index.css b/ui/src/index.css index 0e837875..bf1e4621 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -363,3 +363,11 @@ video::-webkit-media-controls { .hide-scrollbar::-webkit-scrollbar { display: none; } + +.diagonal-stripes { + background: repeating-linear-gradient( + 135deg, + rgba(255, 0, 0, 0.1) 0 12px, /* red-50 with 20% opacity */ + transparent 12px 24px + ); +} diff --git a/ui/src/routes/devices.$id.settings.general.reboot.tsx b/ui/src/routes/devices.$id.settings.general.reboot.tsx index fc0feeaa..b8c32b51 100644 --- a/ui/src/routes/devices.$id.settings.general.reboot.tsx +++ b/ui/src/routes/devices.$id.settings.general.reboot.tsx @@ -1,14 +1,24 @@ -import { useCallback } from "react"; +import { useCallback , useState } from "react"; import { useNavigate } from "react-router"; import { useJsonRpc } from "@hooks/useJsonRpc"; import { Button } from "@components/Button"; -import { m } from "@localizations/messages.js"; +import { useFailsafeModeStore } from "@/hooks/stores"; import { sleep } from "@/utils"; +import { m } from "@localizations/messages.js"; + +import LoadingSpinner from "../components/LoadingSpinner"; +import { useDeviceUiNavigation } from "../hooks/useAppNavigation"; + +// Time to wait after initiating reboot before redirecting to home +const REBOOT_REDIRECT_DELAY_MS = 5000; export default function SettingsGeneralRebootRoute() { const navigate = useNavigate(); const { send } = useJsonRpc(); + const [isRebooting, setIsRebooting] = useState(false); + const { navigateTo } = useDeviceUiNavigation(); + const { setFailsafeMode } = useFailsafeModeStore(); const onClose = useCallback(async () => { navigate(".."); // back to the devices.$id.settings page @@ -18,17 +28,24 @@ export default function SettingsGeneralRebootRoute() { }, [navigate]); - const onConfirmUpdate = useCallback(() => { - send("reboot", { force: true}); - }, [send]); + const onConfirmUpdate = useCallback(async () => { + setIsRebooting(true); + send("reboot", { force: true }); - return ; + await new Promise(resolve => setTimeout(resolve, REBOOT_REDIRECT_DELAY_MS)); + setFailsafeMode(false, ""); + navigateTo("/"); + }, [navigateTo, send, setFailsafeMode]); + + return ; } export function Dialog({ + isRebooting, onClose, onConfirmUpdate, }: Readonly<{ + isRebooting: boolean; onClose: () => void; onConfirmUpdate: () => void; }>) { @@ -37,6 +54,7 @@ export function Dialog({
@@ -46,9 +64,11 @@ export function Dialog({ } function ConfirmationBox({ + isRebooting, onYes, onNo, }: { + isRebooting: boolean; onYes: () => void; onNo: () => void; }) { @@ -61,11 +81,16 @@ function ConfirmationBox({

{m.general_reboot_description()}

- -
-
+ {isRebooting ? ( +
+ +
+ ) : ( +
+
+ )}
); diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index a8fced9d..f82b489b 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -16,10 +16,11 @@ import { } from "react-icons/lu"; import { cx } from "@/cva.config"; -import { useUiStore } from "@hooks/stores"; +import { useUiStore, useFailsafeModeStore } from "@hooks/stores"; import Card from "@components/Card"; -import { LinkButton } from "@components/Button"; +import { FailsafeModeBanner } from "@components/FailSafeModeBanner"; import { FeatureFlag } from "@components/FeatureFlag"; +import { LinkButton } from "@components/Button"; import { m } from "@localizations/messages.js"; /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ @@ -30,6 +31,8 @@ export default function SettingsRoute() { const [showLeftGradient, setShowLeftGradient] = useState(false); const [showRightGradient, setShowRightGradient] = useState(false); const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject }); + const { isFailsafeMode: isFailsafeMode, reason: failsafeReason } = useFailsafeModeStore(); + const isVideoDisabled = isFailsafeMode && failsafeReason === "video"; // Handle scroll position to show/hide gradients const handleScroll = () => { @@ -158,21 +161,28 @@ export default function SettingsRoute() {
-
+
(isActive ? "active" : "")} - > + className={({ isActive }) => cx(isActive ? "active" : "", { + "pointer-events-none": isVideoDisabled + })} >

{m.settings_video()}

-
+
(isActive ? "active" : "")} + className={({ isActive }) => cx(isActive ? "active" : "", { + "pointer-events-none": isVideoDisabled + })} >
@@ -238,8 +248,8 @@ export default function SettingsRoute() {
-
- {/* */} +
+ {isFailsafeMode && failsafeReason && }
import('@components/sidebar/connection const Terminal = lazy(() => import('@components/Terminal')); const UpdateInProgressStatusCard = lazy(() => import("@components/UpdateInProgressStatusCard")); import Modal from "@components/Modal"; +import { FailSafeModeOverlay } from "@components/FailSafeModeOverlay"; import { ConnectionFailedOverlay, LoadingConnectionOverlay, @@ -101,6 +103,7 @@ const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => { return isOnDevice ? deviceLoader() : cloudLoader(params); }; + export default function KvmIdRoute() { const loaderResp = useLoaderData(); // Depending on the mode, we set the appropriate variables @@ -612,14 +615,15 @@ export default function KvmIdRoute() { }); }, 10000); - const { setNetworkState } = useNetworkStateStore(); + const { setNetworkState } = useNetworkStateStore(); const { setHdmiState } = useVideoStore(); const { keyboardLedState, setKeyboardLedState, keysDownState, setKeysDownState, setUsbState, } = useHidStore(); - const { setHidRpcDisabled } = useRTCStore(); + const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled); + const { setFailsafeMode } = useFailsafeModeStore(); const [hasUpdated, setHasUpdated] = useState(false); const { navigateTo } = useDeviceUiNavigation(); @@ -696,6 +700,12 @@ export default function KvmIdRoute() { setRebootState({ isRebooting: true, postRebootAction }); navigateTo("/"); } + + if (resp.method === "failsafeMode") { + const { active, reason } = resp.params as { active: boolean; reason: string }; + console.debug("Setting failsafe mode", { active, reason }); + setFailsafeMode(active, reason); + } } const { send } = useJsonRpc(onJsonRpcRequest); @@ -794,6 +804,8 @@ export default function KvmIdRoute() { getLocalVersion(); }, [appVersion, getLocalVersion]); + const { isFailsafeMode, reason: failsafeReason } = useFailsafeModeStore(); + const ConnectionStatusElement = useMemo(() => { const isOtherSession = location.pathname.includes("other-session"); if (isOtherSession) return null; @@ -869,13 +881,15 @@ export default function KvmIdRoute() { />
- + {(isFailsafeMode && failsafeReason === "video") ? null : }
- {!!ConnectionStatusElement && ConnectionStatusElement} + {isFailsafeMode && failsafeReason ? ( + + ) : !!ConnectionStatusElement && ConnectionStatusElement}
diff --git a/ui/src/ui.config.ts b/ui/src/ui.config.ts index b76dd7c4..3cb3b58b 100644 --- a/ui/src/ui.config.ts +++ b/ui/src/ui.config.ts @@ -1,4 +1,6 @@ export const CLOUD_API = import.meta.env.VITE_CLOUD_API; +export const DOWNGRADE_VERSION = import.meta.env.VITE_DOWNGRADE_VERSION || "0.4.8"; + // In device mode, an empty string uses the current hostname (the JetKVM device's IP) as the API endpoint export const DEVICE_API = ""; diff --git a/video.go b/video.go index cd74e680..7ce342a7 100644 --- a/video.go +++ b/video.go @@ -27,6 +27,7 @@ func triggerVideoStateUpdate() { } func rpcGetVideoState() (native.VideoState, error) { + notifyFailsafeMode(currentSession) return lastVideoState, nil } diff --git a/webrtc.go b/webrtc.go index 37488f77..abe1aba7 100644 --- a/webrtc.go +++ b/webrtc.go @@ -289,6 +289,7 @@ func newSession(config SessionConfig) (*Session, error) { triggerOTAStateUpdate() triggerVideoStateUpdate() triggerUSBStateUpdate() + notifyFailsafeMode(session) case "terminal": handleTerminalChannel(d) case "serial": @@ -391,10 +392,12 @@ func newSession(config SessionConfig) (*Session, error) { } func onActiveSessionsChanged() { + notifyFailsafeMode(currentSession) requestDisplayUpdate(true, "active_sessions_changed") } func onFirstSessionConnected() { + notifyFailsafeMode(currentSession) _ = nativeInstance.VideoStart() stopVideoSleepModeTicker() }