mirror of https://github.com/jetkvm/kvm.git
feat: implement fail-safe mode UI
This commit is contained in:
parent
542f9c0862
commit
fb3e57aa86
|
|
@ -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 (
|
||||||
|
<Card>
|
||||||
|
<div className="diagonal-stripes flex items-center gap-3 p-4 rounded">
|
||||||
|
<LuTriangleAlert className="h-5 w-5 flex-shrink-0 text-red-600 dark:text-red-400" />
|
||||||
|
<p className="text-sm font-medium text-red-800 dark:text-white">
|
||||||
|
{getReasonMessage()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { 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 { GitHubIcon } from "./Icons";
|
||||||
|
|
||||||
|
interface FailSafeModeOverlayProps {
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OverlayContentProps {
|
||||||
|
readonly children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverlayContent({ children }: OverlayContentProps) {
|
||||||
|
return (
|
||||||
|
<GridCard cardClassName="h-full pointer-events-auto outline-hidden!">
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-md border border-slate-800/30 dark:border-slate-300/20">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
|
||||||
|
const { send } = useJsonRpc();
|
||||||
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
const { appVersion } = useVersion();
|
||||||
|
const { systemVersion } = useDeviceStore();
|
||||||
|
const [showRebootConfirm, setShowRebootConfirm] = useState(false);
|
||||||
|
const [isDownloadingLogs, setIsDownloadingLogs] = 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. You can reboot to attempt recovery, report the issue, or downgrade to the last stable version.",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"A critical process has encountered an issue. Your device is still accessible, but some functionality may be temporarily unavailable.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { title, message } = getReasonCopy();
|
||||||
|
|
||||||
|
const handleReboot = () => {
|
||||||
|
if (!showRebootConfirm) {
|
||||||
|
setShowRebootConfirm(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
send("reboot", { force: true }, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to reboot: ${resp.error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReportAndDownloadLogs = () => {
|
||||||
|
setIsDownloadingLogs(true);
|
||||||
|
|
||||||
|
send("getFailSafeLogs", {}, (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);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
notifications.success("Recovery logs downloaded successfully");
|
||||||
|
|
||||||
|
// Open GitHub issue
|
||||||
|
const issueBody = `## Issue Description
|
||||||
|
The ${reason} process encountered an error and recovery 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}\`
|
||||||
|
|
||||||
|
## 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?appVersion=0.4.8");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
className="aspect-video h-full w-full"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0, transition: { duration: 0 } }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OverlayContent>
|
||||||
|
<div className="flex max-w-lg flex-col items-start gap-y-1">
|
||||||
|
<ExclamationTriangleIcon className="h-12 w-12 text-yellow-500" />
|
||||||
|
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2 text-black dark:text-white">
|
||||||
|
<h2 className="text-xl font-bold">Fail safe mode activated</h2>
|
||||||
|
<p className="text-sm">{message}</p>
|
||||||
|
</div>
|
||||||
|
{showRebootConfirm ? (
|
||||||
|
<div className="rounded-md bg-amber-50 p-3 dark:bg-amber-950/20">
|
||||||
|
<p className="mb-3 text-sm text-amber-900 dark:text-amber-200">
|
||||||
|
Rebooting will restart your device. This may resolve the issue. Continue?
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleReboot}
|
||||||
|
theme="primary"
|
||||||
|
size="SM"
|
||||||
|
text="Confirm Reboot"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowRebootConfirm(false)}
|
||||||
|
theme="light"
|
||||||
|
size="SM"
|
||||||
|
text="Cancel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleReportAndDownloadLogs}
|
||||||
|
theme="primary"
|
||||||
|
size="SM"
|
||||||
|
disabled={isDownloadingLogs}
|
||||||
|
LeadingIcon={GitHubIcon}
|
||||||
|
text={isDownloadingLogs ? "Downloading Logs..." : "Report Issue & Download Logs"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleReboot}
|
||||||
|
theme="light"
|
||||||
|
size="SM"
|
||||||
|
text="Reboot Device"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
onClick={handleDowngrade}
|
||||||
|
theme="light"
|
||||||
|
text="Downgrade to v0.4.8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</OverlayContent>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -926,3 +926,16 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export interface FailsafeModeState {
|
||||||
|
isFailsafeMode: boolean;
|
||||||
|
reason: string | null; // "video", "network", etc.
|
||||||
|
setFailsafeMode: (enabled: boolean, reason: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFailsafeModeStore = create<FailsafeModeState>(set => ({
|
||||||
|
isFailsafeMode: false,
|
||||||
|
reason: null,
|
||||||
|
setFailsafeMode: (enabled: boolean, reason: string | null) =>
|
||||||
|
set({ isFailsafeMode: enabled, reason }),
|
||||||
|
}));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { useRTCStore } from "@/hooks/stores";
|
import { useRTCStore, useFailsafeModeStore } from "@/hooks/stores";
|
||||||
|
|
||||||
export interface JsonRpcRequest {
|
export interface JsonRpcRequest {
|
||||||
jsonrpc: string;
|
jsonrpc: string;
|
||||||
|
|
@ -34,12 +34,51 @@ export const RpcMethodNotFound = -32601;
|
||||||
const callbackStore = new Map<number | string, (resp: JsonRpcResponse) => void>();
|
const callbackStore = new Map<number | string, (resp: JsonRpcResponse) => void>();
|
||||||
let requestCounter = 0;
|
let requestCounter = 0;
|
||||||
|
|
||||||
|
// Map of blocked RPC methods by failsafe reason
|
||||||
|
const blockedMethodsByReason: Record<string, string[]> = {
|
||||||
|
video: [
|
||||||
|
'setStreamQualityFactor',
|
||||||
|
'getEDID',
|
||||||
|
'setEDID',
|
||||||
|
'getVideoLogStatus',
|
||||||
|
'setDisplayRotation',
|
||||||
|
'getVideoSleepMode',
|
||||||
|
'setVideoSleepMode',
|
||||||
|
'getVideoState',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
const { rpcDataChannel } = useRTCStore();
|
const { rpcDataChannel } = useRTCStore();
|
||||||
|
const { isFailsafeMode: isFailsafeMode, reason } = useFailsafeModeStore();
|
||||||
|
|
||||||
const send = useCallback(
|
const send = useCallback(
|
||||||
async (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {
|
async (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
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++;
|
requestCounter++;
|
||||||
const payload = { jsonrpc: "2.0", method, params, id: requestCounter };
|
const payload = { jsonrpc: "2.0", method, params, id: requestCounter };
|
||||||
// Store the callback if it exists
|
// Store the callback if it exists
|
||||||
|
|
@ -47,7 +86,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
|
|
||||||
rpcDataChannel.send(JSON.stringify(payload));
|
rpcDataChannel.send(JSON.stringify(payload));
|
||||||
},
|
},
|
||||||
[rpcDataChannel]
|
[rpcDataChannel, isFailsafeMode, reason]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -354,3 +354,11 @@ video::-webkit-media-controls {
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diagonal-stripes {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 0, 0, 0.2) 0 12px, /* red-50 with 20% opacity */
|
||||||
|
transparent 12px 24px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ import { cx } from "@/cva.config";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { LinkButton } from "@components/Button";
|
import { LinkButton } from "@components/Button";
|
||||||
import { FeatureFlag } from "@components/FeatureFlag";
|
import { FeatureFlag } from "@components/FeatureFlag";
|
||||||
import { useUiStore } from "@/hooks/stores";
|
import { useUiStore, useFailsafeModeStore } from "@/hooks/stores";
|
||||||
|
import { FailsafeModeBanner } from "@components/FailSafeModeBanner";
|
||||||
|
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||||
export default function SettingsRoute() {
|
export default function SettingsRoute() {
|
||||||
|
|
@ -29,6 +30,8 @@ export default function SettingsRoute() {
|
||||||
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
||||||
const [showRightGradient, setShowRightGradient] = useState(false);
|
const [showRightGradient, setShowRightGradient] = useState(false);
|
||||||
const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject<HTMLDivElement> });
|
const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject<HTMLDivElement> });
|
||||||
|
const { isFailsafeMode: isFailsafeMode, reason: failsafeReason } = useFailsafeModeStore();
|
||||||
|
const isVideoDisabled = isFailsafeMode && failsafeReason === "video";
|
||||||
|
|
||||||
// Handle scroll position to show/hide gradients
|
// Handle scroll position to show/hide gradients
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
|
@ -157,7 +160,9 @@ export default function SettingsRoute() {
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</FeatureFlag>
|
</FeatureFlag>
|
||||||
<div className="shrink-0">
|
<div className={cx("shrink-0", {
|
||||||
|
"pointer-events-none opacity-50 cursor-not-allowed": isVideoDisabled
|
||||||
|
})}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="video"
|
to="video"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
|
@ -168,7 +173,9 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className={cx("shrink-0", {
|
||||||
|
"pointer-events-none opacity-50 cursor-not-allowed": isVideoDisabled
|
||||||
|
})}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="hardware"
|
to="hardware"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => (isActive ? "active" : "")}
|
||||||
|
|
@ -237,8 +244,8 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:col-span-6">
|
<div className="w-full md:col-span-6 space-y-4">
|
||||||
{/* <AutoHeight> */}
|
{isFailsafeMode && failsafeReason && <FailsafeModeBanner reason={failsafeReason} />}
|
||||||
<Card className="dark:bg-slate-800">
|
<Card className="dark:bg-slate-800">
|
||||||
<div
|
<div
|
||||||
className="space-y-4 px-8 py-6"
|
className="space-y-4 px-8 py-6"
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import {
|
||||||
useUpdateStore,
|
useUpdateStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
VideoState,
|
VideoState,
|
||||||
|
useFailsafeModeStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import WebRTCVideo from "@components/WebRTCVideo";
|
import WebRTCVideo from "@components/WebRTCVideo";
|
||||||
import DashboardNavbar from "@components/Header";
|
import DashboardNavbar from "@components/Header";
|
||||||
|
|
@ -40,6 +41,7 @@ const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectio
|
||||||
const Terminal = lazy(() => import('@components/Terminal'));
|
const Terminal = lazy(() => import('@components/Terminal'));
|
||||||
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
import { FailSafeModeOverlay } from "@components/FaileSafeModeOverlay";
|
||||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import {
|
import {
|
||||||
ConnectionFailedOverlay,
|
ConnectionFailedOverlay,
|
||||||
|
|
@ -113,6 +115,7 @@ const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function KvmIdRoute() {
|
export default function KvmIdRoute() {
|
||||||
const loaderResp = useLoaderData() as LocalLoaderResp | CloudLoaderResp;
|
const loaderResp = useLoaderData() as LocalLoaderResp | CloudLoaderResp;
|
||||||
// Depending on the mode, we set the appropriate variables
|
// Depending on the mode, we set the appropriate variables
|
||||||
|
|
@ -480,6 +483,11 @@ export default function KvmIdRoute() {
|
||||||
const rpcDataChannel = pc.createDataChannel("rpc");
|
const rpcDataChannel = pc.createDataChannel("rpc");
|
||||||
rpcDataChannel.onopen = () => {
|
rpcDataChannel.onopen = () => {
|
||||||
setRpcDataChannel(rpcDataChannel);
|
setRpcDataChannel(rpcDataChannel);
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
useFailsafeModeStore.setState({ isFailsafeMode: true, reason: "video" });
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rpcHidChannel = pc.createDataChannel("hidrpc");
|
const rpcHidChannel = pc.createDataChannel("hidrpc");
|
||||||
|
|
@ -604,6 +612,7 @@ export default function KvmIdRoute() {
|
||||||
keysDownState, setKeysDownState, setUsbState,
|
keysDownState, setKeysDownState, setUsbState,
|
||||||
} = useHidStore();
|
} = useHidStore();
|
||||||
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
||||||
|
const { setFailsafeMode } = useFailsafeModeStore();
|
||||||
|
|
||||||
const [hasUpdated, setHasUpdated] = useState(false);
|
const [hasUpdated, setHasUpdated] = useState(false);
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
@ -666,6 +675,12 @@ export default function KvmIdRoute() {
|
||||||
window.location.href = currentUrl.toString();
|
window.location.href = currentUrl.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.method === "failsafeMode") {
|
||||||
|
const { enabled, reason } = resp.params as { enabled: boolean; reason: string };
|
||||||
|
console.debug("Setting failsafe mode", { enabled, reason });
|
||||||
|
setFailsafeMode(enabled, reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { send } = useJsonRpc(onJsonRpcRequest);
|
const { send } = useJsonRpc(onJsonRpcRequest);
|
||||||
|
|
@ -764,6 +779,8 @@ export default function KvmIdRoute() {
|
||||||
getLocalVersion();
|
getLocalVersion();
|
||||||
}, [appVersion, getLocalVersion]);
|
}, [appVersion, getLocalVersion]);
|
||||||
|
|
||||||
|
const { isFailsafeMode, reason: failsafeReason } = useFailsafeModeStore();
|
||||||
|
|
||||||
const ConnectionStatusElement = useMemo(() => {
|
const ConnectionStatusElement = useMemo(() => {
|
||||||
const hasConnectionFailed =
|
const hasConnectionFailed =
|
||||||
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
||||||
|
|
@ -841,13 +858,16 @@ export default function KvmIdRoute() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative flex h-full w-full overflow-hidden">
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
<WebRTCVideo />
|
{!isFailsafeMode && <WebRTCVideo />}
|
||||||
<div
|
<div
|
||||||
style={{ animationDuration: "500ms" }}
|
style={{ animationDuration: "500ms" }}
|
||||||
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
||||||
>
|
>
|
||||||
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||||
{!!ConnectionStatusElement && ConnectionStatusElement}
|
{!!ConnectionStatusElement && ConnectionStatusElement}
|
||||||
|
{isFailsafeMode && failsafeReason && (
|
||||||
|
<FailSafeModeOverlay reason={failsafeReason} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SidebarContainer sidebarView={sidebarView} />
|
<SidebarContainer sidebarView={sidebarView} />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue