Compare commits

..

4 Commits

Author SHA1 Message Date
Marc Brooks 3632bfff5d
Added force page reload to the onClose events of update/reboot
Updated the text about reboot/update and used a smaller button.
Ensure we get the correct UI version.
Also fixed comment about the system update progress
2025-09-30 17:45:00 -05:00
Marc Brooks 78d4a0275b
Fix comment 2025-09-30 17:40:53 -05:00
Marc Brooks 4ff617a679
Added exponential backoff to reconnection
Also made the number of reconnect attempts settable
Doesn't attempt a reconnection if we intentionally disconnect
Make sure the fire-and-forget for TURN activity doesn't result in unhandled promise rejection.
2025-09-30 17:40:37 -05:00
Marc Brooks 39b23b8bd5
Add ability to request a reload to LinkButton and Link 2025-09-30 17:33:42 -05:00
5 changed files with 51 additions and 25 deletions

View File

@ -213,7 +213,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
Button.displayName = "Button";
type LinkPropsType = Pick<LinkProps, "to"> &
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: boolean };
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
const classes = cx(
"group outline-hidden",
@ -231,7 +231,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
);
} else {
return (
<Link to={to} className={classes}>
<Link to={to} reloadDocument={props.reloadDocument} className={classes}>
<ButtonContent {...props} />
</Link>
);

View File

@ -98,7 +98,7 @@ export default function SettingsAccessIndexRoute() {
}
getCloudState();
// In cloud mode, we need to navigate to the device overview page, as we don't a connection anymore
// In cloud mode, we need to navigate to the device overview page, as we don't have a connection anymore
if (!isOnDevice) navigate("/");
return;
});

View File

@ -7,6 +7,12 @@ import { Button } from "@components/Button";
export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate();
const { send } = useJsonRpc();
const onClose = useCallback(() => {
navigate(".."); // back to the devices.$id.settings page
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
}, [navigate]);
const onConfirmUpdate = useCallback(() => {
// This is where we send the RPC to the golang binary
@ -16,7 +22,7 @@ export default function SettingsGeneralRebootRoute() {
{
/* 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 onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
}
export function Dialog({

View File

@ -1,5 +1,5 @@
import { useLocation, useNavigate } from "react-router";
import { useCallback, useEffect, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { MdConnectWithoutContact, MdRestartAlt } from "react-icons/md";
@ -19,6 +19,11 @@ export default function SettingsGeneralUpdateRoute() {
const { setModalView, otaState } = useUpdateStore();
const { send } = useJsonRpc();
const onClose = useCallback(() => {
navigate(".."); // back to the devices.$id.settings page
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
}, [navigate]);
const onConfirmUpdate = useCallback(() => {
send("tryUpdate", {});
setModalView("updating");
@ -39,7 +44,7 @@ export default function SettingsGeneralUpdateRoute() {
{
/* 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 onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
}
export function Dialog({
@ -223,7 +228,7 @@ function UpdatingDeviceState({
100,
);
} else {
// System: 10% download, 90% update
// System: 10% download, 10% verification, 80% update
return Math.min(
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
100,
@ -287,17 +292,19 @@ function UpdatingDeviceState({
<span className="font-medium text-black dark:text-white">
Rebooting the device to complete the update...
</span>
<p>
<p className="font-medium text-black dark:text-white">
This may take a few minutes. The device will automatically
reconnect once it is back online. If it doesn{"'"}t, you can
manually reconnect.
reconnect once it is back online.<br/>
If it doesn{"'"}t reconnect automatically, you can manually
reconnect by clicking here:
<LinkButton
size="SM"
size="XS"
theme="light"
text="Reconnect to KVM"
LeadingIcon={MdConnectWithoutContact}
textAlign="center"
to={".."}
reloadDocument={true}
to={"/"}
/>
</p>
</div>
@ -307,15 +314,17 @@ function UpdatingDeviceState({
<span className="font-medium text-black dark:text-white">
Device reboot is pending...
</span>
<p>
The JetKVM is preparing to reboot. This may take a while. If it doesn{"'"}t automatically reboot
after a few minutes, you can manually request a reboot.
<p className="font-medium text-black dark:text-white">
The JetKVM is preparing to reboot. This may take a while.<br/>
If it doesn{"'"}t automatically reboot after a few minutes, you
can manually request a reboot by clicking here:
<LinkButton
size="SM"
size="XS"
theme="light"
text="Reboot the KVM"
LeadingIcon={MdRestartAlt}
textAlign="center"
reloadDocument={true}
to={"../reboot"}
/>
</p>

View File

@ -146,6 +146,7 @@ export default function KvmIdRoute() {
const { otaState, setOtaState, setModalView } = useUpdateStore();
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
const cleanupAndStopReconnecting = useCallback(
function cleanupAndStopReconnecting() {
console.log("Closing peer connection");
@ -182,11 +183,11 @@ export default function KvmIdRoute() {
pc: RTCPeerConnection,
remoteDescription: RTCSessionDescriptionInit,
) {
setLoadingMessage("Setting remote description");
setLoadingMessage("Setting remote description type:" + remoteDescription.type);
try {
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
console.log("[setRemoteSessionDescription] Remote description set successfully");
console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp);
setLoadingMessage("Establishing secure connection...");
} catch (error) {
console.error(
@ -231,9 +232,15 @@ export default function KvmIdRoute() {
const ignoreOffer = useRef(false);
const isSettingRemoteAnswerPending = useRef(false);
const makingOffer = useRef(false);
const reconnectAttemptsRef = useRef(20);
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const reconnectInterval = (attempt: number) => {
// Exponential backoff with a max of 10 seconds between attempts
return Math.min(500 * 2 ** attempt, 10000);
}
const { sendMessage, getWebSocket } = useWebSocket(
isOnDevice
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
@ -241,17 +248,16 @@ export default function KvmIdRoute() {
{
heartbeat: true,
retryOnError: true,
reconnectAttempts: 15,
reconnectInterval: 1000,
onReconnectStop: () => {
console.debug("Reconnect stopped");
reconnectAttempts: reconnectAttemptsRef.current,
reconnectInterval: reconnectInterval,
onReconnectStop: (attempt: number) => {
console.debug("Reconnect stopped after ", attempt, "attempts");
cleanupAndStopReconnecting();
},
shouldReconnect(event) {
console.debug("[Websocket] shouldReconnect", event);
// TODO: Why true?
return true;
return !connectionFailed; // we always want to try to reconnect unless we're explicitly stopped
},
onClose(event) {
@ -284,6 +290,7 @@ export default function KvmIdRoute() {
*/
const parsedMessage = JSON.parse(message.data);
if (parsedMessage.type === "device-metadata") {
const { deviceVersion } = parsedMessage.data;
console.debug("[Websocket] Received device-metadata message");
@ -300,10 +307,12 @@ export default function KvmIdRoute() {
console.log("[Websocket] Device is using new signaling");
isLegacySignalingEnabled.current = false;
}
setupPeerConnection();
}
if (!peerConnection) return;
if (parsedMessage.type === "answer") {
console.debug("[Websocket] Received answer");
const readyForOffer =
@ -594,7 +603,9 @@ export default function KvmIdRoute() {
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
bytesReceived: bytesReceivedDelta,
bytesSent: bytesSentDelta,
});
}).catch(()=>{
// we don't care about errors here, but we don't want unhandled promise rejections
});
}, 10000);
const { setNetworkState} = useNetworkStateStore();