[WIP] Optimizations: code readiness optimizations

This commit is contained in:
Alex P 2025-10-23 01:37:30 +03:00
parent 1671a7706b
commit 2e4a49feb6
2 changed files with 281 additions and 233 deletions

View File

@ -167,13 +167,11 @@ func NewSessionManager(logger *zerolog.Logger) *SessionManager {
} }
func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSettings) error { func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSettings) error {
// Basic input validation
if session == nil { if session == nil {
sm.logger.Error().Msg("AddSession: session is nil") sm.logger.Error().Msg("AddSession: session is nil")
return errors.New("session cannot be nil") return errors.New("session cannot be nil")
} }
// Validate nickname if provided (matching frontend validation)
if session.Nickname != "" { if session.Nickname != "" {
if len(session.Nickname) < minNicknameLength { if len(session.Nickname) < minNicknameLength {
return fmt.Errorf("nickname must be at least %d characters", minNicknameLength) return fmt.Errorf("nickname must be at least %d characters", minNicknameLength)
@ -181,7 +179,6 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
if len(session.Nickname) > maxNicknameLength { if len(session.Nickname) > maxNicknameLength {
return fmt.Errorf("nickname must be %d characters or less", maxNicknameLength) return fmt.Errorf("nickname must be %d characters or less", maxNicknameLength)
} }
// Note: Pattern validation is done in RPC layer, not here for performance
} }
if len(session.Identity) > maxIdentityLength { if len(session.Identity) > maxIdentityLength {
return fmt.Errorf("identity too long (max %d characters)", maxIdentityLength) return fmt.Errorf("identity too long (max %d characters)", maxIdentityLength)
@ -225,30 +222,25 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
} }
} }
// Check if a session with this ID already exists (reconnection)
if existing, exists := sm.sessions[session.ID]; exists { if existing, exists := sm.sessions[session.ID]; exists {
if existing.Identity != session.Identity || existing.Source != session.Source { if existing.Identity != session.Identity || existing.Source != session.Source {
return fmt.Errorf("session ID already in use by different user (identity mismatch)") return fmt.Errorf("session ID already in use by different user (identity mismatch)")
} }
// Close old connection to prevent multiple active connections for same session ID
if existing.peerConnection != nil { if existing.peerConnection != nil {
existing.peerConnection.Close() existing.peerConnection.Close()
} }
// Update the existing session with new connection details
existing.peerConnection = session.peerConnection existing.peerConnection = session.peerConnection
existing.VideoTrack = session.VideoTrack existing.VideoTrack = session.VideoTrack
existing.ControlChannel = session.ControlChannel existing.ControlChannel = session.ControlChannel
existing.RPCChannel = session.RPCChannel existing.RPCChannel = session.RPCChannel
existing.HidChannel = session.HidChannel existing.HidChannel = session.HidChannel
existing.flushCandidates = session.flushCandidates existing.flushCandidates = session.flushCandidates
// Preserve mode and nickname
session.Mode = existing.Mode session.Mode = existing.Mode
session.Nickname = existing.Nickname session.Nickname = existing.Nickname
session.CreatedAt = existing.CreatedAt session.CreatedAt = existing.CreatedAt
// Ensure session has auto-generated nickname if needed
sm.ensureNickname(session) sm.ensureNickname(session)
if !nicknameReserved && session.Nickname != "" { if !nicknameReserved && session.Nickname != "" {
@ -257,17 +249,15 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
sm.sessions[session.ID] = session sm.sessions[session.ID] = session
// If this was the primary, try to restore primary status
if existing.Mode == SessionModePrimary { if existing.Mode == SessionModePrimary {
isBlacklisted := sm.isSessionBlacklisted(session.ID) isBlacklisted := sm.isSessionBlacklisted(session.ID)
// SECURITY: Prevent dual-primary window - only restore if no other primary exists // SECURITY: Prevent dual-primary - only restore if no other primary exists
primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil
if sm.lastPrimaryID == session.ID && !isBlacklisted && !primaryExists { if sm.lastPrimaryID == session.ID && !isBlacklisted && !primaryExists {
sm.primarySessionID = session.ID sm.primarySessionID = session.ID
sm.lastPrimaryID = "" sm.lastPrimaryID = ""
delete(sm.reconnectGrace, session.ID) delete(sm.reconnectGrace, session.ID)
} else { } else {
// Grace period expired, another session took over, or primary already exists
session.Mode = SessionModeObserver session.Mode = SessionModeObserver
} }
} }
@ -280,22 +270,18 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
return ErrMaxSessionsReached return ErrMaxSessionsReached
} }
// Generate ID if not set
if session.ID == "" { if session.ID == "" {
session.ID = uuid.New().String() session.ID = uuid.New().String()
} }
// Set nickname from client settings if provided
if clientSettings != nil && clientSettings.Nickname != "" { if clientSettings != nil && clientSettings.Nickname != "" {
session.Nickname = clientSettings.Nickname session.Nickname = clientSettings.Nickname
} }
// Use global settings for requirements (not client-provided)
globalSettings := currentSessionSettings globalSettings := currentSessionSettings
primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil
// Check if there's an active grace period for a primary session (different from this session)
hasActivePrimaryGracePeriod := false hasActivePrimaryGracePeriod := false
if sm.lastPrimaryID != "" && sm.lastPrimaryID != session.ID { if sm.lastPrimaryID != "" && sm.lastPrimaryID != session.ID {
if graceTime, exists := sm.reconnectGrace[sm.lastPrimaryID]; exists { if graceTime, exists := sm.reconnectGrace[sm.lastPrimaryID]; exists {
@ -312,7 +298,6 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
isBlacklisted := sm.isSessionBlacklisted(session.ID) isBlacklisted := sm.isSessionBlacklisted(session.ID)
isOnlySession := len(sm.sessions) == 0 isOnlySession := len(sm.sessions) == 0
// Determine if this session should become primary
canBecomePrimary := !primaryExists && !hasActivePrimaryGracePeriod canBecomePrimary := !primaryExists && !hasActivePrimaryGracePeriod
isReconnectingPrimary := wasWithinGracePeriod && wasPreviouslyPrimary isReconnectingPrimary := wasWithinGracePeriod && wasPreviouslyPrimary
isNewEligibleSession := !wasWithinGracePeriod && (!isBlacklisted || isOnlySession) isNewEligibleSession := !wasWithinGracePeriod && (!isBlacklisted || isOnlySession)
@ -325,7 +310,7 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
sm.primarySessionID = session.ID sm.primarySessionID = session.ID
sm.lastPrimaryID = "" sm.lastPrimaryID = ""
// Clear all existing grace periods when a new primary is established // Clear grace periods when new primary is established
for oldSessionID := range sm.reconnectGrace { for oldSessionID := range sm.reconnectGrace {
delete(sm.reconnectGrace, oldSessionID) delete(sm.reconnectGrace, oldSessionID)
} }
@ -347,7 +332,6 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
requiresNickname := globalSettings.RequireNickname requiresNickname := globalSettings.RequireNickname
hasNickname := session.Nickname != "" && len(session.Nickname) > 0 hasNickname := session.Nickname != "" && len(session.Nickname) > 0
// Only send approval request if nickname is not required OR already provided
if !requiresNickname || hasNickname { if !requiresNickname || hasNickname {
go func() { go func() {
writeJSONRPCEvent("newSessionPending", map[string]interface{}{ writeJSONRPCEvent("newSessionPending", map[string]interface{}{
@ -358,12 +342,8 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
}, primary) }, primary)
}() }()
} }
// If nickname is required and missing, the approval request will be sent
// later when updateSessionNickname is called (see jsonrpc.go:232-242)
} }
} else { } else {
// No primary exists and approval is required, OR approval is not required
// In either case, this session becomes an observer
session.Mode = SessionModeObserver session.Mode = SessionModeObserver
} }
@ -625,12 +605,10 @@ func (sm *SessionManager) SetPrimarySession(sessionID string) error {
// Sessions in pending state cannot receive video // Sessions in pending state cannot receive video
// Sessions that require nickname but don't have one also cannot receive video (if enforced) // Sessions that require nickname but don't have one also cannot receive video (if enforced)
func (sm *SessionManager) CanReceiveVideo(session *Session, settings *SessionSettings) bool { func (sm *SessionManager) CanReceiveVideo(session *Session, settings *SessionSettings) bool {
// Check if session has video view permission
if !session.HasPermission(PermissionVideoView) { if !session.HasPermission(PermissionVideoView) {
return false return false
} }
// If nickname is required and session doesn't have one, block video
if settings != nil && settings.RequireNickname && session.Nickname == "" { if settings != nil && settings.RequireNickname && session.Nickname == "" {
return false return false
} }

View File

@ -42,11 +42,18 @@ import NicknameModal from "@components/NicknameModal";
import AccessDeniedOverlay from "@components/AccessDeniedOverlay"; import AccessDeniedOverlay from "@components/AccessDeniedOverlay";
import PendingApprovalOverlay from "@components/PendingApprovalOverlay"; import PendingApprovalOverlay from "@components/PendingApprovalOverlay";
import DashboardNavbar from "@components/Header"; import DashboardNavbar from "@components/Header";
const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectionStats')); const ConnectionStatsSidebar = lazy(() => import("@/components/sidebar/connectionStats"));
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 { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc"; import {
JsonRpcRequest,
JsonRpcResponse,
RpcMethodNotFound,
useJsonRpc,
} from "@/hooks/useJsonRpc";
import { import {
ConnectionFailedOverlay, ConnectionFailedOverlay,
LoadingConnectionOverlay, LoadingConnectionOverlay,
@ -135,15 +142,25 @@ export default function KvmIdRoute() {
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null; const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
const params = useParams() as { id: string }; const params = useParams() as { id: string };
const { sidebarView, setSidebarView, disableVideoFocusTrap, setDisableVideoFocusTrap, rebootState, setRebootState } = useUiStore(); const {
sidebarView,
setSidebarView,
disableVideoFocusTrap,
setDisableVideoFocusTrap,
rebootState,
setRebootState,
} = useUiStore();
const [queryParams, setQueryParams] = useSearchParams(); const [queryParams, setQueryParams] = useSearchParams();
const { const {
peerConnection, setPeerConnection, peerConnection,
peerConnectionState, setPeerConnectionState, setPeerConnection,
peerConnectionState,
setPeerConnectionState,
setMediaStream, setMediaStream,
setRpcDataChannel, setRpcDataChannel,
isTurnServerInUse, setTurnServerInUse, isTurnServerInUse,
setTurnServerInUse,
rpcDataChannel, rpcDataChannel,
setTransceiver, setTransceiver,
setRpcHidChannel, setRpcHidChannel,
@ -162,12 +179,14 @@ export default function KvmIdRoute() {
const { currentSessionId, currentMode, setCurrentSession } = useSessionStore(); const { currentSessionId, currentMode, setCurrentSession } = useSessionStore();
const { nickname, setNickname } = useSharedSessionStore(); const { nickname, setNickname } = useSharedSessionStore();
const { setRequireSessionApproval, setRequireSessionNickname } = useSettingsStore(); const { setRequireSessionApproval, setRequireSessionNickname } = useSettingsStore();
const [globalSessionSettings, setGlobalSessionSettings] = useState<{requireApproval: boolean, requireNickname: boolean} | null>(null); const [globalSessionSettings, setGlobalSessionSettings] = useState<{
requireApproval: boolean;
requireNickname: boolean;
} | null>(null);
const [loadingMessage, setLoadingMessage] = useState("Connecting to device..."); const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
const cleanupAndStopReconnecting = useCallback( const cleanupAndStopReconnecting = useCallback(
function cleanupAndStopReconnecting() { function cleanupAndStopReconnecting() {
setConnectionFailed(true); setConnectionFailed(true);
if (peerConnection) { if (peerConnection) {
setPeerConnectionState(peerConnection.connectionState); setPeerConnectionState(peerConnection.connectionState);
@ -264,7 +283,7 @@ export default function KvmIdRoute() {
}, },
onClose(_event) { onClose(_event) {
// We don't want to close everything down, we wait for the reconnect to stop instead // Handled by onReconnectStop instead
}, },
onError(event) { onError(event) {
@ -309,7 +328,7 @@ export default function KvmIdRoute() {
if (sessionSettings) { if (sessionSettings) {
setGlobalSessionSettings({ setGlobalSessionSettings({
requireNickname: sessionSettings.requireNickname || false, requireNickname: sessionSettings.requireNickname || false,
requireApproval: sessionSettings.requireApproval || false requireApproval: sessionSettings.requireApproval || false,
}); });
// Also update the settings store for approval handling // Also update the settings store for approval handling
setRequireSessionApproval(sessionSettings.requireApproval || false); setRequireSessionApproval(sessionSettings.requireApproval || false);
@ -318,7 +337,6 @@ export default function KvmIdRoute() {
// If the device version is not set, we can assume the device is using the legacy signaling // If the device version is not set, we can assume the device is using the legacy signaling
if (!deviceVersion) { if (!deviceVersion) {
// Now we don't need the websocket connection anymore, as we've established that we need to use the legacy signaling // Now we don't need the websocket connection anymore, as we've established that we need to use the legacy signaling
// which does everything over HTTP(at least from the perspective of the client) // which does everything over HTTP(at least from the perspective of the client)
isLegacySignalingEnabled.current = true; isLegacySignalingEnabled.current = true;
@ -342,7 +360,10 @@ export default function KvmIdRoute() {
} }
if (!peerConnection) { if (!peerConnection) {
console.warn("[Websocket] Ignoring message because peerConnection is not ready:", parsedMessage.type); console.warn(
"[Websocket] Ignoring message because peerConnection is not ready:",
parsedMessage.type,
);
return; return;
} }
if (parsedMessage.type === "answer") { if (parsedMessage.type === "answer") {
@ -366,15 +387,18 @@ export default function KvmIdRoute() {
if (parsedMessage.sessionId && parsedMessage.mode) { if (parsedMessage.sessionId && parsedMessage.mode) {
handleSessionResponse({ handleSessionResponse({
sessionId: parsedMessage.sessionId, sessionId: parsedMessage.sessionId,
mode: parsedMessage.mode mode: parsedMessage.mode,
}); });
// Store sessionId via zustand (persists to sessionStorage for per-tab isolation) // Store sessionId via zustand (persists to sessionStorage for per-tab isolation)
setCurrentSession(parsedMessage.sessionId, parsedMessage.mode); setCurrentSession(parsedMessage.sessionId, parsedMessage.mode);
if (parsedMessage.requireNickname !== undefined && parsedMessage.requireApproval !== undefined) { if (
parsedMessage.requireNickname !== undefined &&
parsedMessage.requireApproval !== undefined
) {
setGlobalSessionSettings({ setGlobalSessionSettings({
requireNickname: parsedMessage.requireNickname, requireNickname: parsedMessage.requireNickname,
requireApproval: parsedMessage.requireApproval requireApproval: parsedMessage.requireApproval,
}); });
// Also update the settings store for approval handling // Also update the settings store for approval handling
setRequireSessionApproval(parsedMessage.requireApproval); setRequireSessionApproval(parsedMessage.requireApproval);
@ -385,8 +409,10 @@ export default function KvmIdRoute() {
// 1. Nickname is required by backend settings // 1. Nickname is required by backend settings
// 2. We don't already have a nickname // 2. We don't already have a nickname
// This happens even for pending sessions so the nickname is included in approval // This happens even for pending sessions so the nickname is included in approval
const hasNickname = parsedMessage.nickname && parsedMessage.nickname.length > 0; const hasNickname =
const requiresNickname = parsedMessage.requireNickname || globalSessionSettings?.requireNickname; parsedMessage.nickname && parsedMessage.nickname.length > 0;
const requiresNickname =
parsedMessage.requireNickname || globalSessionSettings?.requireNickname;
if (requiresNickname && !hasNickname) { if (requiresNickname && !hasNickname) {
setShowNicknameModal(true); setShowNicknameModal(true);
@ -427,18 +453,22 @@ export default function KvmIdRoute() {
peerConnection?.iceConnectionState === "connected"; peerConnection?.iceConnectionState === "connected";
if (!isConnectionHealthy) { if (!isConnectionHealthy) {
console.log(`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`); console.log(
`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`,
);
setTimeout(() => { setTimeout(() => {
peerConnection?.close(); peerConnection?.close();
setupPeerConnection(); setupPeerConnection();
}, 500); }, 500);
} else { } else {
console.log(`[Websocket] Mode changed to ${newMode}, connection healthy, skipping reconnect`); console.log(
`[Websocket] Mode changed to ${newMode}, connection healthy, skipping reconnect`,
);
} }
} }
} }
}, },
} },
); );
const sendWebRTCSignal = useCallback( const sendWebRTCSignal = useCallback(
@ -540,8 +570,8 @@ export default function KvmIdRoute() {
sessionId: storeSessionId || undefined, sessionId: storeSessionId || undefined,
userAgent: navigator.userAgent, userAgent: navigator.userAgent,
sessionSettings: { sessionSettings: {
nickname: storeNickname || undefined nickname: storeNickname || undefined,
} },
}); });
} }
} catch (e) { } catch (e) {
@ -605,10 +635,13 @@ export default function KvmIdRoute() {
setRpcHidUnreliableChannel(rpcHidUnreliableChannel); setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
}; };
const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel("hidrpc-unreliable-nonordered", { const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel(
ordered: false, "hidrpc-unreliable-nonordered",
maxRetransmits: 0, {
}); ordered: false,
maxRetransmits: 0,
},
);
rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer"; rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
rpcHidUnreliableNonOrderedChannel.onopen = () => { rpcHidUnreliableNonOrderedChannel.onopen = () => {
setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel); setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
@ -699,19 +732,24 @@ export default function KvmIdRoute() {
} }
// Fire and forget // Fire and forget
api.POST(`${CLOUD_API}/webrtc/turn_activity`, { api
bytesReceived: bytesReceivedDelta, .POST(`${CLOUD_API}/webrtc/turn_activity`, {
bytesSent: bytesSentDelta, bytesReceived: bytesReceivedDelta,
}).catch(() => { bytesSent: bytesSentDelta,
// we don't care about errors here, but we don't want unhandled promise rejections })
}); .catch(() => {
// we don't care about errors here, but we don't want unhandled promise rejections
});
}, 10000); }, 10000);
const { setNetworkState } = useNetworkStateStore(); const { setNetworkState } = useNetworkStateStore();
const { setHdmiState } = useVideoStore(); const { setHdmiState } = useVideoStore();
const { const {
keyboardLedState, setKeyboardLedState, keyboardLedState,
keysDownState, setKeysDownState, setUsbState, setKeyboardLedState,
keysDownState,
setKeysDownState,
setUsbState,
} = useHidStore(); } = useHidStore();
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled); const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
@ -720,15 +758,17 @@ export default function KvmIdRoute() {
function onJsonRpcRequest(resp: JsonRpcRequest) { function onJsonRpcRequest(resp: JsonRpcRequest) {
// Handle session-related events // Handle session-related events
if (resp.method === "sessionsUpdated" || if (
resp.method === "modeChanged" || resp.method === "sessionsUpdated" ||
resp.method === "connectionModeChanged" || resp.method === "modeChanged" ||
resp.method === "otherSessionConnected" || resp.method === "connectionModeChanged" ||
resp.method === "primaryControlRequested" || resp.method === "otherSessionConnected" ||
resp.method === "primaryControlApproved" || resp.method === "primaryControlRequested" ||
resp.method === "primaryControlDenied" || resp.method === "primaryControlApproved" ||
resp.method === "newSessionPending" || resp.method === "primaryControlDenied" ||
resp.method === "sessionAccessDenied") { resp.method === "newSessionPending" ||
resp.method === "sessionAccessDenied"
) {
handleRpcEvent(resp.method, resp.params); handleRpcEvent(resp.method, resp.params);
// Show access denied overlay if our session was denied // Show access denied overlay if our session was denied
@ -809,7 +849,7 @@ export default function KvmIdRoute() {
newSessionRequest, newSessionRequest,
handleApproveNewSession, handleApproveNewSession,
handleDenyNewSession, handleDenyNewSession,
closeNewSessionRequest closeNewSessionRequest,
} = useSessionManagement(send); } = useSessionManagement(send);
const { hasPermission, isLoading: isLoadingPermissions } = usePermissions(); const { hasPermission, isLoading: isLoadingPermissions } = usePermissions();
@ -823,7 +863,13 @@ export default function KvmIdRoute() {
const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0]; const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
setHdmiState(hdmiState); setHdmiState(hdmiState);
}); });
}, [rpcDataChannel?.readyState, hasPermission, isLoadingPermissions, send, setHdmiState]); }, [
rpcDataChannel?.readyState,
hasPermission,
isLoadingPermissions,
send,
setHdmiState,
]);
const [needLedState, setNeedLedState] = useState(true); const [needLedState, setNeedLedState] = useState(true);
@ -842,7 +888,15 @@ export default function KvmIdRoute() {
} }
setNeedLedState(false); setNeedLedState(false);
}); });
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState, hasPermission, isLoadingPermissions]); }, [
rpcDataChannel?.readyState,
send,
setKeyboardLedState,
keyboardLedState,
needLedState,
hasPermission,
isLoadingPermissions,
]);
const [needKeyDownState, setNeedKeyDownState] = useState(true); const [needKeyDownState, setNeedKeyDownState] = useState(true);
@ -854,7 +908,10 @@ export default function KvmIdRoute() {
send("getKeyDownState", {}, (resp: JsonRpcResponse) => { send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
if (resp.error.code === RpcMethodNotFound) { if (resp.error.code === RpcMethodNotFound) {
console.warn("Failed to get key down state, switching to old-school", resp.error); console.warn(
"Failed to get key down state, switching to old-school",
resp.error,
);
setHidRpcDisabled(true); setHidRpcDisabled(true);
} else { } else {
console.error("Failed to get key down state", resp.error); console.error("Failed to get key down state", resp.error);
@ -865,7 +922,16 @@ export default function KvmIdRoute() {
} }
setNeedKeyDownState(false); setNeedKeyDownState(false);
}); });
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setKeysDownState, setHidRpcDisabled, hasPermission, isLoadingPermissions]); }, [
keysDownState,
needKeyDownState,
rpcDataChannel?.readyState,
send,
setKeysDownState,
setHidRpcDisabled,
hasPermission,
isLoadingPermissions,
]);
// When the update is successful, we need to refresh the client javascript and show a success modal // When the update is successful, we need to refresh the client javascript and show a success modal
useEffect(() => { useEffect(() => {
@ -910,7 +976,9 @@ export default function KvmIdRoute() {
// Rebooting takes priority over connection status // Rebooting takes priority over connection status
if (rebootState?.isRebooting) { if (rebootState?.isRebooting) {
return <RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />; return (
<RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />
);
} }
const hasConnectionFailed = const hasConnectionFailed =
@ -937,184 +1005,186 @@ export default function KvmIdRoute() {
} }
return null; return null;
}, [location.pathname, rebootState?.isRebooting, rebootState?.postRebootAction, connectionFailed, peerConnectionState, peerConnection, setupPeerConnection, loadingMessage]); }, [
location.pathname,
rebootState?.isRebooting,
rebootState?.postRebootAction,
connectionFailed,
peerConnectionState,
peerConnection,
setupPeerConnection,
loadingMessage,
]);
return ( return (
<PermissionsProvider> <PermissionsProvider>
<FeatureFlagProvider appVersion={appVersion}> <FeatureFlagProvider appVersion={appVersion}>
{!outlet && otaState.updating && ( {!outlet && otaState.updating && (
<AnimatePresence> <AnimatePresence>
<motion.div <motion.div
className="pointer-events-none fixed inset-0 top-16 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center" className="pointer-events-none fixed inset-0 top-16 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center"
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: "easeInOut" }} transition={{ duration: 0.3, ease: "easeInOut" }}
>
<UpdateInProgressStatusCard />
</motion.div>
</AnimatePresence>
)}
<div className="relative h-full">
<FocusTrap
paused={disableVideoFocusTrap}
focusTrapOptions={{
allowOutsideClick: true,
escapeDeactivates: false,
fallbackFocus: "#videoFocusTrap",
}}
> >
<UpdateInProgressStatusCard /> <div className="absolute top-0">
</motion.div> <button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
</AnimatePresence> </div>
)} </FocusTrap>
<div className="relative h-full">
<FocusTrap
paused={disableVideoFocusTrap}
focusTrapOptions={{
allowOutsideClick: true,
escapeDeactivates: false,
fallbackFocus: "#videoFocusTrap",
}}
>
<div className="absolute top-0">
<button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
</div>
</FocusTrap>
<div className="grid h-full grid-rows-(--grid-headerBody) select-none"> <div className="grid h-full grid-rows-(--grid-headerBody) select-none">
<DashboardNavbar <DashboardNavbar
primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]} primaryLinks={
showConnectionStatus={true} isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]
isLoggedIn={authMode === "password" || !!user} }
userEmail={user?.email} showConnectionStatus={true}
picture={user?.picture} isLoggedIn={authMode === "password" || !!user}
kvmName={deviceName ?? "JetKVM Device"} userEmail={user?.email}
/> picture={user?.picture}
kvmName={deviceName ?? "JetKVM Device"}
/>
<div className="relative flex h-full w-full overflow-hidden">
<div className="relative flex h-full w-full overflow-hidden"> {/* Only show video feed if nickname is set (when required) and not pending approval */}
{/* Only show video feed if nickname is set (when required) and not pending approval */} {!showNicknameModal && currentMode !== "pending" ? (
{(!showNicknameModal && currentMode !== "pending") ? ( <>
<> <WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />
<WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} /> <div
<div style={{ animationDuration: "500ms" }}
style={{ animationDuration: "500ms" }} className="pointer-events-none absolute inset-0 flex animate-slideUpFade 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} </div>
</div>
</>
) : (
<div className="flex flex-1 items-center justify-center bg-slate-900">
<div className="text-center text-slate-400">
{showNicknameModal && <p>Please set your nickname to continue</p>}
{currentMode === "pending" && <p>Waiting for session approval...</p>}
</div> </div>
</div> </div>
</> )}
) : ( <SidebarContainer sidebarView={sidebarView} />
<div className="flex-1 bg-slate-900 flex items-center justify-center"> </div>
<div className="text-slate-400 text-center">
{showNicknameModal && <p>Please set your nickname to continue</p>}
{currentMode === "pending" && <p>Waiting for session approval...</p>}
</div>
</div>
)}
<SidebarContainer sidebarView={sidebarView} />
</div> </div>
</div> </div>
</div>
<div <div
className="z-50" className="z-50"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
onMouseUp={e => e.stopPropagation()} onMouseUp={e => e.stopPropagation()}
onMouseDown={e => e.stopPropagation()} onMouseDown={e => e.stopPropagation()}
onKeyUp={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()}
onKeyDown={e => { onKeyDown={e => {
e.stopPropagation(); e.stopPropagation();
if (e.key === "Escape") navigateTo("/"); if (e.key === "Escape") navigateTo("/");
}} }}
> >
<Modal open={outlet !== null} onClose={onModalClose}> <Modal open={outlet !== null} onClose={onModalClose}>
{/* The 'used by other session' modal needs to have access to the connectWebRTC function */} {/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
<Outlet context={{ setupPeerConnection }} /> <Outlet context={{ setupPeerConnection }} />
</Modal> </Modal>
<NicknameModal <NicknameModal
isOpen={showNicknameModal} isOpen={showNicknameModal}
onSubmit={async (nickname) => { onSubmit={async nickname => {
setNickname(nickname); setNickname(nickname);
setShowNicknameModal(false); setShowNicknameModal(false);
setDisableVideoFocusTrap(false); setDisableVideoFocusTrap(false);
if (currentSessionId && send) { if (currentSessionId && send) {
try { try {
await sessionApi.updateNickname(send, currentSessionId, nickname); await sessionApi.updateNickname(send, currentSessionId, nickname);
} catch (error) { } catch (error) {
console.error("Failed to update nickname:", error); console.error("Failed to update nickname:", error);
}
} }
}}
onSkip={() => {
setShowNicknameModal(false);
setDisableVideoFocusTrap(false);
}}
/>
</div>
{kvmTerminal && (
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
)}
{serialConsole && (
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
)}
{/* Unified Session Request Dialog */}
{(primaryControlRequest || newSessionRequest) && (
<UnifiedSessionRequestDialog
request={
primaryControlRequest
? {
id: primaryControlRequest.requestId,
type: "primary_control",
source: primaryControlRequest.source,
identity: primaryControlRequest.identity,
nickname: primaryControlRequest.nickname,
}
: newSessionRequest
? {
id: newSessionRequest.sessionId,
type: "session_approval",
source: newSessionRequest.source,
identity: newSessionRequest.identity,
nickname: newSessionRequest.nickname,
}
: null
}
onApprove={
primaryControlRequest
? handleApprovePrimaryRequest
: handleApproveNewSession
}
onDeny={
primaryControlRequest ? handleDenyPrimaryRequest : handleDenyNewSession
}
onDismiss={
primaryControlRequest ? closePrimaryControlRequest : closeNewSessionRequest
}
onClose={
primaryControlRequest ? closePrimaryControlRequest : closeNewSessionRequest
}
/>
)}
<AccessDeniedOverlay
show={accessDenied}
message="Your session access was denied by the primary session"
onRequestApproval={async () => {
if (!send) return;
try {
await sessionApi.requestSessionApproval(send);
setAccessDenied(false);
} catch (error) {
console.error("Failed to re-request approval:", error);
} }
}} }}
onSkip={() => {
setShowNicknameModal(false);
setDisableVideoFocusTrap(false);
}}
/> />
</div>
{kvmTerminal && ( <PendingApprovalOverlay show={currentMode === "pending"} />
<Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
)}
{serialConsole && (
<Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
)}
{/* Unified Session Request Dialog */}
{(primaryControlRequest || newSessionRequest) && (
<UnifiedSessionRequestDialog
request={
primaryControlRequest
? {
id: primaryControlRequest.requestId,
type: "primary_control",
source: primaryControlRequest.source,
identity: primaryControlRequest.identity,
nickname: primaryControlRequest.nickname,
}
: newSessionRequest
? {
id: newSessionRequest.sessionId,
type: "session_approval",
source: newSessionRequest.source,
identity: newSessionRequest.identity,
nickname: newSessionRequest.nickname,
}
: null
}
onApprove={
primaryControlRequest
? handleApprovePrimaryRequest
: handleApproveNewSession
}
onDeny={
primaryControlRequest
? handleDenyPrimaryRequest
: handleDenyNewSession
}
onDismiss={
primaryControlRequest
? closePrimaryControlRequest
: closeNewSessionRequest
}
onClose={
primaryControlRequest
? closePrimaryControlRequest
: closeNewSessionRequest
}
/>
)}
<AccessDeniedOverlay
show={accessDenied}
message="Your session access was denied by the primary session"
onRequestApproval={async () => {
if (!send) return;
try {
await sessionApi.requestSessionApproval(send);
setAccessDenied(false);
} catch (error) {
console.error("Failed to re-request approval:", error);
}
}}
/>
<PendingApprovalOverlay
show={currentMode === "pending"}
/>
</FeatureFlagProvider> </FeatureFlagProvider>
</PermissionsProvider> </PermissionsProvider>
); );