feat: enhance FailSafeModeOverlay with tooltip and log download improvements

This commit is contained in:
Adam Shiervani 2025-11-07 17:22:02 +01:00
parent 72f29df835
commit 82ad2a467f
3 changed files with 110 additions and 52 deletions

View File

@ -1,9 +1,10 @@
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 { GridCard } from "@components/Card";
import Card, { GridCard } from "@components/Card";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import { useVersion } from "@/hooks/useVersion";
@ -30,19 +31,47 @@ function OverlayContent({ children }: OverlayContentProps) {
);
}
interface TooltipProps {
readonly children: React.ReactNode;
readonly text: string;
readonly show: boolean;
}
function Tooltip({ children, text, show }: TooltipProps) {
if (!show) {
return <>{children}</>;
}
return (
<div className="group/tooltip relative">
{children}
<div className="pointer-events-none absolute bottom-full left-1/2 mb-2 hidden -translate-x-1/2 opacity-0 transition-opacity group-hover/tooltip:block group-hover/tooltip:opacity-100">
<Card>
<div className="whitespace-nowrap px-2 py-1 text-xs flex items-center gap-1 justify-center">
<LuInfo className="h-3 w-3 text-slate-700 dark:text-slate-300" />
{text}
</div>
</Card>
</div>
</div>
);
}
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. You can reboot to attempt recovery, report the issue, or downgrade to the last stable version.",
"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 {
@ -80,13 +109,14 @@ export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
document.body.removeChild(a);
URL.revokeObjectURL(url);
notifications.success("Recovery logs downloaded successfully");
notifications.success("Crash logs downloaded successfully");
setHasDownloadedLogs(true);
// Open GitHub issue
const issueBody = `## Issue Description
The ${reason} process encountered an error and recovery mode was activated.
The \`${reason}\` process encountered an error and fail safe mode was activated.
**Reason:** ${reason}
**Reason:** \`${reason}\`
**Timestamp:** ${new Date().toISOString()}
**App Version:** ${appVersion || "Unknown"}
**System Version:** ${systemVersion || "Unknown"}
@ -95,6 +125,9 @@ The ${reason} process encountered an error and recovery mode was activated.
Please attach the recovery logs file that was downloaded to your computer:
\`${filename}\`
> [!NOTE]
> Please omit any sensitive information from the logs.
## Additional Context
[Please describe what you were doing when this occurred]`;
@ -114,7 +147,7 @@ Please attach the recovery logs file that was downloaded to your computer:
return (
<AnimatePresence>
<motion.div
className="aspect-video h-full w-full"
className="aspect-video h-full w-full isolate"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0 } }}
@ -132,29 +165,40 @@ Please attach the recovery logs file that was downloaded to your computer:
<h2 className="text-xl font-bold">Fail safe mode activated</h2>
<p className="text-sm">{message}</p>
</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"}
/>
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<Button
onClick={handleReportAndDownloadLogs}
theme="primary"
size="SM"
disabled={isDownloadingLogs}
LeadingIcon={GitHubIcon}
text={isDownloadingLogs ? "Downloading Logs..." : "Download Logs & Report Issue"}
/>
<div className="h-8 w-px bg-slate-200 dark:bg-slate-700 block" />
<Tooltip text="Download logs first to unlock" show={!hasDownloadedLogs}>
<Button
onClick={() => navigateTo("/settings/general/reboot")}
theme="light"
size="SM"
text="Reboot Device"
disabled={!hasDownloadedLogs}
/>
</Tooltip>
<Tooltip text="Download logs first to unlock" show={!hasDownloadedLogs}>
<Button
size="SM"
onClick={handleDowngrade}
theme="light"
text="Downgrade to v0.4.8"
disabled={!hasDownloadedLogs}
/>
</Tooltip>
</div>
<Button
onClick={() => navigateTo("/settings/general/reboot")}
theme="light"
size="SM"
text="Reboot Device"
/>
<Button
size="SM"
onClick={handleDowngrade}
theme="light"
text="Downgrade to v0.4.8"
/>
</div>
</div>
</div>

View File

@ -1,28 +1,39 @@
import { useNavigate } from "react-router";
import { useCallback } from "react";
import { useCallback, useState } from "react";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button";
import LoadingSpinner from "../components/LoadingSpinner";
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate();
const { send } = useJsonRpc();
const [isRebooting, setIsRebooting] = useState(false);
const { navigateTo } = useDeviceUiNavigation();
const onConfirmUpdate = useCallback(() => {
const onConfirmUpdate = useCallback(async () => {
setIsRebooting(true);
// This is where we send the RPC to the golang binary
send("reboot", {force: true});
}, [send]);
send("reboot", { force: true });
await new Promise(resolve => setTimeout(resolve, 5000));
navigateTo("/");
}, [navigateTo, send]);
{
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
}
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
return <Dialog isRebooting={isRebooting} onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
}
export function Dialog({
isRebooting,
onClose,
onConfirmUpdate,
}: {
isRebooting: boolean;
onClose: () => void;
onConfirmUpdate: () => void;
}) {
@ -30,19 +41,22 @@ export function Dialog({
return (
<div className="pointer-events-auto relative mx-auto text-left">
<div>
<ConfirmationBox
onYes={onConfirmUpdate}
onNo={onClose}
/>
<ConfirmationBox
isRebooting={isRebooting}
onYes={onConfirmUpdate}
onNo={onClose}
/>
</div>
</div>
);
}
function ConfirmationBox({
isRebooting,
onYes,
onNo,
}: {
isRebooting: boolean;
onYes: () => void;
onNo: () => void;
}) {
@ -55,11 +69,16 @@ function ConfirmationBox({
<p className="text-sm text-slate-600 dark:text-slate-300">
Do you want to proceed with rebooting the system?
</p>
<div className="mt-4 flex gap-x-2">
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
<Button size="SM" theme="blank" text="No" onClick={onNo} />
</div>
{isRebooting ? (
<div className="mt-4 flex items-center justify-center">
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
</div>
) : (
<div className="mt-4 flex gap-x-2">
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
<Button size="SM" theme="blank" text="No" onClick={onNo} />
</div>
)}
</div>
</div>
);

View File

@ -126,7 +126,7 @@ export default function KvmIdRoute() {
const params = useParams() as { id: string };
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
const [ queryParams, setQueryParams ] = useSearchParams();
const [queryParams, setQueryParams] = useSearchParams();
const {
peerConnection, setPeerConnection,
@ -483,10 +483,6 @@ export default function KvmIdRoute() {
const rpcDataChannel = pc.createDataChannel("rpc");
rpcDataChannel.onopen = () => {
setRpcDataChannel(rpcDataChannel);
// setTimeout(() => {
// useFailsafeModeStore.setState({ isFailsafeMode: true, reason: "video" });
// }, 1000);
};
const rpcHidChannel = pc.createDataChannel("hidrpc");
@ -604,10 +600,10 @@ export default function KvmIdRoute() {
});
}, 10000);
const { setNetworkState} = useNetworkStateStore();
const { setNetworkState } = useNetworkStateStore();
const { setHdmiState } = useVideoStore();
const {
keyboardLedState, setKeyboardLedState,
keyboardLedState, setKeyboardLedState,
keysDownState, setKeysDownState, setUsbState,
} = useHidStore();
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
@ -770,7 +766,7 @@ export default function KvmIdRoute() {
if (location.pathname !== "/other-session") navigateTo("/");
}, [navigateTo, location.pathname]);
const { appVersion, getLocalVersion} = useVersion();
const { appVersion, getLocalVersion } = useVersion();
useEffect(() => {
if (appVersion) return;
@ -863,10 +859,9 @@ export default function KvmIdRoute() {
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">
{!!ConnectionStatusElement && ConnectionStatusElement}
{isFailsafeMode && failsafeReason && (
{isFailsafeMode && failsafeReason ? (
<FailSafeModeOverlay reason={failsafeReason} />
)}
) : !!ConnectionStatusElement && ConnectionStatusElement}
</div>
</div>
<SidebarContainer sidebarView={sidebarView} />