mirror of https://github.com/jetkvm/kvm.git
Compare commits
3 Commits
50e1cf06b8
...
ec0d5d7cb7
| Author | SHA1 | Date |
|---|---|---|
|
|
ec0d5d7cb7 | |
|
|
2e4a49feb6 | |
|
|
1671a7706b |
|
|
@ -96,11 +96,11 @@ func handleRequestSessionApprovalRPC(session *Session) (any, error) {
|
|||
}
|
||||
|
||||
func validateNickname(nickname string) error {
|
||||
if len(nickname) < 2 {
|
||||
return errors.New("nickname must be at least 2 characters")
|
||||
if len(nickname) < minNicknameLength {
|
||||
return fmt.Errorf("nickname must be at least %d characters", minNicknameLength)
|
||||
}
|
||||
if len(nickname) > 30 {
|
||||
return errors.New("nickname must be 30 characters or less")
|
||||
if len(nickname) > maxNicknameLength {
|
||||
return fmt.Errorf("nickname must be %d characters or less", maxNicknameLength)
|
||||
}
|
||||
if !isValidNickname(nickname) {
|
||||
return errors.New("nickname can only contain letters, numbers, spaces, and - _ . @")
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const (
|
|||
// Broadcast throttling (DoS protection)
|
||||
globalBroadcastDelay = 100 * time.Millisecond // Minimum time between global session broadcasts
|
||||
sessionBroadcastDelay = 50 * time.Millisecond // Minimum time between broadcasts to a single session
|
||||
broadcastQueueCapacity = 100 // Maximum pending broadcasts before drops occur
|
||||
|
||||
// Session timeout defaults
|
||||
defaultPendingSessionTimeout = 1 * time.Minute // Timeout for pending sessions (DoS protection)
|
||||
|
|
@ -154,7 +155,7 @@ func NewSessionManager(logger *zerolog.Logger) *SessionManager {
|
|||
logger: logger,
|
||||
maxSessions: maxSessions,
|
||||
primaryTimeout: primaryTimeout,
|
||||
broadcastQueue: make(chan struct{}, 100),
|
||||
broadcastQueue: make(chan struct{}, broadcastQueueCapacity),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
|
@ -166,13 +167,11 @@ func NewSessionManager(logger *zerolog.Logger) *SessionManager {
|
|||
}
|
||||
|
||||
func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSettings) error {
|
||||
// Basic input validation
|
||||
if session == nil {
|
||||
sm.logger.Error().Msg("AddSession: session is nil")
|
||||
return errors.New("session cannot be nil")
|
||||
}
|
||||
|
||||
// Validate nickname if provided (matching frontend validation)
|
||||
if session.Nickname != "" {
|
||||
if len(session.Nickname) < minNicknameLength {
|
||||
return fmt.Errorf("nickname must be at least %d characters", minNicknameLength)
|
||||
|
|
@ -180,7 +179,6 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
if len(session.Nickname) > 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 {
|
||||
return fmt.Errorf("identity too long (max %d characters)", maxIdentityLength)
|
||||
|
|
@ -224,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.Identity != session.Identity || existing.Source != session.Source {
|
||||
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 {
|
||||
existing.peerConnection.Close()
|
||||
}
|
||||
|
||||
// Update the existing session with new connection details
|
||||
existing.peerConnection = session.peerConnection
|
||||
existing.VideoTrack = session.VideoTrack
|
||||
existing.ControlChannel = session.ControlChannel
|
||||
existing.RPCChannel = session.RPCChannel
|
||||
existing.HidChannel = session.HidChannel
|
||||
existing.flushCandidates = session.flushCandidates
|
||||
// Preserve mode and nickname
|
||||
session.Mode = existing.Mode
|
||||
session.Nickname = existing.Nickname
|
||||
session.CreatedAt = existing.CreatedAt
|
||||
|
||||
// Ensure session has auto-generated nickname if needed
|
||||
sm.ensureNickname(session)
|
||||
|
||||
if !nicknameReserved && session.Nickname != "" {
|
||||
|
|
@ -256,17 +249,15 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
|
||||
sm.sessions[session.ID] = session
|
||||
|
||||
// If this was the primary, try to restore primary status
|
||||
if existing.Mode == SessionModePrimary {
|
||||
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
|
||||
if sm.lastPrimaryID == session.ID && !isBlacklisted && !primaryExists {
|
||||
sm.primarySessionID = session.ID
|
||||
sm.lastPrimaryID = ""
|
||||
delete(sm.reconnectGrace, session.ID)
|
||||
} else {
|
||||
// Grace period expired, another session took over, or primary already exists
|
||||
session.Mode = SessionModeObserver
|
||||
}
|
||||
}
|
||||
|
|
@ -279,22 +270,18 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
return ErrMaxSessionsReached
|
||||
}
|
||||
|
||||
// Generate ID if not set
|
||||
if session.ID == "" {
|
||||
session.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// Set nickname from client settings if provided
|
||||
if clientSettings != nil && clientSettings.Nickname != "" {
|
||||
session.Nickname = clientSettings.Nickname
|
||||
}
|
||||
|
||||
// Use global settings for requirements (not client-provided)
|
||||
globalSettings := currentSessionSettings
|
||||
|
||||
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
|
||||
if sm.lastPrimaryID != "" && sm.lastPrimaryID != session.ID {
|
||||
if graceTime, exists := sm.reconnectGrace[sm.lastPrimaryID]; exists {
|
||||
|
|
@ -311,7 +298,6 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
isBlacklisted := sm.isSessionBlacklisted(session.ID)
|
||||
isOnlySession := len(sm.sessions) == 0
|
||||
|
||||
// Determine if this session should become primary
|
||||
canBecomePrimary := !primaryExists && !hasActivePrimaryGracePeriod
|
||||
isReconnectingPrimary := wasWithinGracePeriod && wasPreviouslyPrimary
|
||||
isNewEligibleSession := !wasWithinGracePeriod && (!isBlacklisted || isOnlySession)
|
||||
|
|
@ -324,7 +310,7 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
sm.primarySessionID = session.ID
|
||||
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 {
|
||||
delete(sm.reconnectGrace, oldSessionID)
|
||||
}
|
||||
|
|
@ -346,7 +332,6 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
requiresNickname := globalSettings.RequireNickname
|
||||
hasNickname := session.Nickname != "" && len(session.Nickname) > 0
|
||||
|
||||
// Only send approval request if nickname is not required OR already provided
|
||||
if !requiresNickname || hasNickname {
|
||||
go func() {
|
||||
writeJSONRPCEvent("newSessionPending", map[string]interface{}{
|
||||
|
|
@ -357,12 +342,8 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
|||
}, primary)
|
||||
}()
|
||||
}
|
||||
// If nickname is required and missing, the approval request will be sent
|
||||
// later when updateSessionNickname is called (see jsonrpc.go:232-242)
|
||||
}
|
||||
} else {
|
||||
// No primary exists and approval is required, OR approval is not required
|
||||
// In either case, this session becomes an observer
|
||||
session.Mode = SessionModeObserver
|
||||
}
|
||||
|
||||
|
|
@ -624,12 +605,10 @@ func (sm *SessionManager) SetPrimarySession(sessionID string) error {
|
|||
// Sessions in pending state cannot receive video
|
||||
// Sessions that require nickname but don't have one also cannot receive video (if enforced)
|
||||
func (sm *SessionManager) CanReceiveVideo(session *Session, settings *SessionSettings) bool {
|
||||
// Check if session has video view permission
|
||||
if !session.HasPermission(PermissionVideoView) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If nickname is required and session doesn't have one, block video
|
||||
if settings != nil && settings.RequireNickname && session.Nickname == "" {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,11 +42,18 @@ import NicknameModal from "@components/NicknameModal";
|
|||
import AccessDeniedOverlay from "@components/AccessDeniedOverlay";
|
||||
import PendingApprovalOverlay from "@components/PendingApprovalOverlay";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectionStats'));
|
||||
const Terminal = lazy(() => import('@components/Terminal'));
|
||||
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
||||
const ConnectionStatsSidebar = lazy(() => import("@/components/sidebar/connectionStats"));
|
||||
const Terminal = lazy(() => import("@components/Terminal"));
|
||||
const UpdateInProgressStatusCard = lazy(
|
||||
() => import("@/components/UpdateInProgressStatusCard"),
|
||||
);
|
||||
import Modal from "@/components/Modal";
|
||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import {
|
||||
JsonRpcRequest,
|
||||
JsonRpcResponse,
|
||||
RpcMethodNotFound,
|
||||
useJsonRpc,
|
||||
} from "@/hooks/useJsonRpc";
|
||||
import {
|
||||
ConnectionFailedOverlay,
|
||||
LoadingConnectionOverlay,
|
||||
|
|
@ -135,15 +142,25 @@ export default function KvmIdRoute() {
|
|||
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
||||
|
||||
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 {
|
||||
peerConnection, setPeerConnection,
|
||||
peerConnectionState, setPeerConnectionState,
|
||||
peerConnection,
|
||||
setPeerConnection,
|
||||
peerConnectionState,
|
||||
setPeerConnectionState,
|
||||
setMediaStream,
|
||||
setRpcDataChannel,
|
||||
isTurnServerInUse, setTurnServerInUse,
|
||||
isTurnServerInUse,
|
||||
setTurnServerInUse,
|
||||
rpcDataChannel,
|
||||
setTransceiver,
|
||||
setRpcHidChannel,
|
||||
|
|
@ -162,12 +179,14 @@ export default function KvmIdRoute() {
|
|||
const { currentSessionId, currentMode, setCurrentSession } = useSessionStore();
|
||||
const { nickname, setNickname } = useSharedSessionStore();
|
||||
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 cleanupAndStopReconnecting = useCallback(
|
||||
function cleanupAndStopReconnecting() {
|
||||
|
||||
setConnectionFailed(true);
|
||||
if (peerConnection) {
|
||||
setPeerConnectionState(peerConnection.connectionState);
|
||||
|
|
@ -264,7 +283,7 @@ export default function KvmIdRoute() {
|
|||
},
|
||||
|
||||
onClose(_event) {
|
||||
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||
// Handled by onReconnectStop instead
|
||||
},
|
||||
|
||||
onError(event) {
|
||||
|
|
@ -309,7 +328,7 @@ export default function KvmIdRoute() {
|
|||
if (sessionSettings) {
|
||||
setGlobalSessionSettings({
|
||||
requireNickname: sessionSettings.requireNickname || false,
|
||||
requireApproval: sessionSettings.requireApproval || false
|
||||
requireApproval: sessionSettings.requireApproval || false,
|
||||
});
|
||||
// Also update the settings store for approval handling
|
||||
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 (!deviceVersion) {
|
||||
|
||||
// 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)
|
||||
isLegacySignalingEnabled.current = true;
|
||||
|
|
@ -342,7 +360,10 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (parsedMessage.type === "answer") {
|
||||
|
|
@ -366,15 +387,18 @@ export default function KvmIdRoute() {
|
|||
if (parsedMessage.sessionId && parsedMessage.mode) {
|
||||
handleSessionResponse({
|
||||
sessionId: parsedMessage.sessionId,
|
||||
mode: parsedMessage.mode
|
||||
mode: parsedMessage.mode,
|
||||
});
|
||||
|
||||
// Store sessionId via zustand (persists to sessionStorage for per-tab isolation)
|
||||
setCurrentSession(parsedMessage.sessionId, parsedMessage.mode);
|
||||
if (parsedMessage.requireNickname !== undefined && parsedMessage.requireApproval !== undefined) {
|
||||
if (
|
||||
parsedMessage.requireNickname !== undefined &&
|
||||
parsedMessage.requireApproval !== undefined
|
||||
) {
|
||||
setGlobalSessionSettings({
|
||||
requireNickname: parsedMessage.requireNickname,
|
||||
requireApproval: parsedMessage.requireApproval
|
||||
requireApproval: parsedMessage.requireApproval,
|
||||
});
|
||||
// Also update the settings store for approval handling
|
||||
setRequireSessionApproval(parsedMessage.requireApproval);
|
||||
|
|
@ -385,8 +409,10 @@ export default function KvmIdRoute() {
|
|||
// 1. Nickname is required by backend settings
|
||||
// 2. We don't already have a nickname
|
||||
// This happens even for pending sessions so the nickname is included in approval
|
||||
const hasNickname = parsedMessage.nickname && parsedMessage.nickname.length > 0;
|
||||
const requiresNickname = parsedMessage.requireNickname || globalSessionSettings?.requireNickname;
|
||||
const hasNickname =
|
||||
parsedMessage.nickname && parsedMessage.nickname.length > 0;
|
||||
const requiresNickname =
|
||||
parsedMessage.requireNickname || globalSessionSettings?.requireNickname;
|
||||
|
||||
if (requiresNickname && !hasNickname) {
|
||||
setShowNicknameModal(true);
|
||||
|
|
@ -427,18 +453,22 @@ export default function KvmIdRoute() {
|
|||
peerConnection?.iceConnectionState === "connected";
|
||||
|
||||
if (!isConnectionHealthy) {
|
||||
console.log(`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`);
|
||||
console.log(
|
||||
`[Websocket] Mode changed to ${newMode}, connection unhealthy, reconnecting...`,
|
||||
);
|
||||
setTimeout(() => {
|
||||
peerConnection?.close();
|
||||
setupPeerConnection();
|
||||
}, 500);
|
||||
} 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(
|
||||
|
|
@ -540,8 +570,8 @@ export default function KvmIdRoute() {
|
|||
sessionId: storeSessionId || undefined,
|
||||
userAgent: navigator.userAgent,
|
||||
sessionSettings: {
|
||||
nickname: storeNickname || undefined
|
||||
}
|
||||
nickname: storeNickname || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -605,10 +635,13 @@ export default function KvmIdRoute() {
|
|||
setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
|
||||
};
|
||||
|
||||
const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel("hidrpc-unreliable-nonordered", {
|
||||
const rpcHidUnreliableNonOrderedChannel = pc.createDataChannel(
|
||||
"hidrpc-unreliable-nonordered",
|
||||
{
|
||||
ordered: false,
|
||||
maxRetransmits: 0,
|
||||
});
|
||||
},
|
||||
);
|
||||
rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
|
||||
rpcHidUnreliableNonOrderedChannel.onopen = () => {
|
||||
setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
|
||||
|
|
@ -699,10 +732,12 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
|
||||
// Fire and forget
|
||||
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||
api
|
||||
.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||
bytesReceived: bytesReceivedDelta,
|
||||
bytesSent: bytesSentDelta,
|
||||
}).catch(() => {
|
||||
})
|
||||
.catch(() => {
|
||||
// we don't care about errors here, but we don't want unhandled promise rejections
|
||||
});
|
||||
}, 10000);
|
||||
|
|
@ -710,8 +745,11 @@ export default function KvmIdRoute() {
|
|||
const { setNetworkState } = useNetworkStateStore();
|
||||
const { setHdmiState } = useVideoStore();
|
||||
const {
|
||||
keyboardLedState, setKeyboardLedState,
|
||||
keysDownState, setKeysDownState, setUsbState,
|
||||
keyboardLedState,
|
||||
setKeyboardLedState,
|
||||
keysDownState,
|
||||
setKeysDownState,
|
||||
setUsbState,
|
||||
} = useHidStore();
|
||||
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
||||
|
||||
|
|
@ -720,7 +758,8 @@ export default function KvmIdRoute() {
|
|||
|
||||
function onJsonRpcRequest(resp: JsonRpcRequest) {
|
||||
// Handle session-related events
|
||||
if (resp.method === "sessionsUpdated" ||
|
||||
if (
|
||||
resp.method === "sessionsUpdated" ||
|
||||
resp.method === "modeChanged" ||
|
||||
resp.method === "connectionModeChanged" ||
|
||||
resp.method === "otherSessionConnected" ||
|
||||
|
|
@ -728,7 +767,8 @@ export default function KvmIdRoute() {
|
|||
resp.method === "primaryControlApproved" ||
|
||||
resp.method === "primaryControlDenied" ||
|
||||
resp.method === "newSessionPending" ||
|
||||
resp.method === "sessionAccessDenied") {
|
||||
resp.method === "sessionAccessDenied"
|
||||
) {
|
||||
handleRpcEvent(resp.method, resp.params);
|
||||
|
||||
// Show access denied overlay if our session was denied
|
||||
|
|
@ -809,7 +849,7 @@ export default function KvmIdRoute() {
|
|||
newSessionRequest,
|
||||
handleApproveNewSession,
|
||||
handleDenyNewSession,
|
||||
closeNewSessionRequest
|
||||
closeNewSessionRequest,
|
||||
} = useSessionManagement(send);
|
||||
|
||||
const { hasPermission, isLoading: isLoadingPermissions } = usePermissions();
|
||||
|
|
@ -823,7 +863,13 @@ export default function KvmIdRoute() {
|
|||
const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
|
||||
setHdmiState(hdmiState);
|
||||
});
|
||||
}, [rpcDataChannel?.readyState, hasPermission, isLoadingPermissions, send, setHdmiState]);
|
||||
}, [
|
||||
rpcDataChannel?.readyState,
|
||||
hasPermission,
|
||||
isLoadingPermissions,
|
||||
send,
|
||||
setHdmiState,
|
||||
]);
|
||||
|
||||
const [needLedState, setNeedLedState] = useState(true);
|
||||
|
||||
|
|
@ -842,7 +888,15 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
setNeedLedState(false);
|
||||
});
|
||||
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState, hasPermission, isLoadingPermissions]);
|
||||
}, [
|
||||
rpcDataChannel?.readyState,
|
||||
send,
|
||||
setKeyboardLedState,
|
||||
keyboardLedState,
|
||||
needLedState,
|
||||
hasPermission,
|
||||
isLoadingPermissions,
|
||||
]);
|
||||
|
||||
const [needKeyDownState, setNeedKeyDownState] = useState(true);
|
||||
|
||||
|
|
@ -854,7 +908,10 @@ export default function KvmIdRoute() {
|
|||
send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
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);
|
||||
} else {
|
||||
console.error("Failed to get key down state", resp.error);
|
||||
|
|
@ -865,7 +922,16 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
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
|
||||
useEffect(() => {
|
||||
|
|
@ -910,7 +976,9 @@ export default function KvmIdRoute() {
|
|||
|
||||
// Rebooting takes priority over connection status
|
||||
if (rebootState?.isRebooting) {
|
||||
return <RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />;
|
||||
return (
|
||||
<RebootingOverlay show={true} postRebootAction={rebootState.postRebootAction} />
|
||||
);
|
||||
}
|
||||
|
||||
const hasConnectionFailed =
|
||||
|
|
@ -937,7 +1005,16 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
|
||||
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 (
|
||||
<PermissionsProvider>
|
||||
|
|
@ -971,7 +1048,9 @@ export default function KvmIdRoute() {
|
|||
|
||||
<div className="grid h-full grid-rows-(--grid-headerBody) select-none">
|
||||
<DashboardNavbar
|
||||
primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
|
||||
primaryLinks={
|
||||
isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]
|
||||
}
|
||||
showConnectionStatus={true}
|
||||
isLoggedIn={authMode === "password" || !!user}
|
||||
userEmail={user?.email}
|
||||
|
|
@ -979,15 +1058,14 @@ export default function KvmIdRoute() {
|
|||
kvmName={deviceName ?? "JetKVM Device"}
|
||||
/>
|
||||
|
||||
|
||||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
{/* Only show video feed if nickname is set (when required) and not pending approval */}
|
||||
{(!showNicknameModal && currentMode !== "pending") ? (
|
||||
{!showNicknameModal && currentMode !== "pending" ? (
|
||||
<>
|
||||
<WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />
|
||||
<div
|
||||
style={{ animationDuration: "500ms" }}
|
||||
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
||||
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4"
|
||||
>
|
||||
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||
{!!ConnectionStatusElement && ConnectionStatusElement}
|
||||
|
|
@ -995,8 +1073,8 @@ export default function KvmIdRoute() {
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 bg-slate-900 flex items-center justify-center">
|
||||
<div className="text-slate-400 text-center">
|
||||
<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>
|
||||
|
|
@ -1025,7 +1103,7 @@ export default function KvmIdRoute() {
|
|||
|
||||
<NicknameModal
|
||||
isOpen={showNicknameModal}
|
||||
onSubmit={async (nickname) => {
|
||||
onSubmit={async nickname => {
|
||||
setNickname(nickname);
|
||||
setShowNicknameModal(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
|
|
@ -1081,19 +1159,13 @@ export default function KvmIdRoute() {
|
|||
: handleApproveNewSession
|
||||
}
|
||||
onDeny={
|
||||
primaryControlRequest
|
||||
? handleDenyPrimaryRequest
|
||||
: handleDenyNewSession
|
||||
primaryControlRequest ? handleDenyPrimaryRequest : handleDenyNewSession
|
||||
}
|
||||
onDismiss={
|
||||
primaryControlRequest
|
||||
? closePrimaryControlRequest
|
||||
: closeNewSessionRequest
|
||||
primaryControlRequest ? closePrimaryControlRequest : closeNewSessionRequest
|
||||
}
|
||||
onClose={
|
||||
primaryControlRequest
|
||||
? closePrimaryControlRequest
|
||||
: closeNewSessionRequest
|
||||
primaryControlRequest ? closePrimaryControlRequest : closeNewSessionRequest
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1112,9 +1184,7 @@ export default function KvmIdRoute() {
|
|||
}}
|
||||
/>
|
||||
|
||||
<PendingApprovalOverlay
|
||||
show={currentMode === "pending"}
|
||||
/>
|
||||
<PendingApprovalOverlay show={currentMode === "pending"} />
|
||||
</FeatureFlagProvider>
|
||||
</PermissionsProvider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue