Files
claudetools/projects/msp-tools/guru-connect/dashboard/src/hooks/useRemoteSession.ts
Mike Swanson 6c316aa701 Add VPN configuration tools and agent documentation
Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection
and enhanced agent documentation framework.

VPN Configuration (PST-NW-VPN):
- Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS
- Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24)
- Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment
- Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2
- Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic
- Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes)
- Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper
- vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts
- OpenVPN config files (Windows-compatible, abandoned for L2TP)

Key VPN Implementation Details:
- L2TP creates PPP adapter with connection name as interface description
- UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24
- Split-tunnel enabled (only remote traffic through VPN)
- All-user connection for pre-login auto-connect via scheduled task
- Authentication: CHAP + MSChapv2 for UniFi compatibility

Agent Documentation:
- AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents
- documentation-squire.md: Documentation and task management specialist agent
- Updated all agent markdown files with standardized formatting

Project Organization:
- Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs)
- Cleaned up old session JSONL files from projects/msp-tools/
- Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows)
- Added guru-rmm server components and deployment configs

Technical Notes:
- VPN IP pool: 192.168.4.x (client gets 192.168.4.6)
- Remote network: 192.168.0.0/24 (router at 192.168.0.10)
- PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7
- Credentials: pst-admin / 24Hearts$

Files: 15 VPN scripts, 2 agent docs, conversation log reorganization,
guru-connect/guru-rmm infrastructure additions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 11:51:47 -07:00

240 lines
6.3 KiB
TypeScript

/**
* React hook for managing remote desktop session connection
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import type { ConnectionStatus, VideoFrame, MouseEvent as ProtoMouseEvent, KeyEvent as ProtoKeyEvent, MouseEventType, KeyEventType, Modifiers } from '../types/protocol';
import { encodeMouseEvent, encodeKeyEvent, decodeVideoFrame } from '../lib/protobuf';
interface UseRemoteSessionOptions {
serverUrl: string;
sessionId: string;
onFrame?: (frame: VideoFrame) => void;
onStatusChange?: (status: ConnectionStatus) => void;
}
interface UseRemoteSessionReturn {
status: ConnectionStatus;
connect: () => void;
disconnect: () => void;
sendMouseEvent: (event: ProtoMouseEvent) => void;
sendKeyEvent: (event: ProtoKeyEvent) => void;
}
export function useRemoteSession(options: UseRemoteSessionOptions): UseRemoteSessionReturn {
const { serverUrl, sessionId, onFrame, onStatusChange } = options;
const [status, setStatus] = useState<ConnectionStatus>({
connected: false,
});
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<number | null>(null);
const frameCountRef = useRef(0);
const lastFpsUpdateRef = useRef(Date.now());
// Update status and notify
const updateStatus = useCallback((newStatus: Partial<ConnectionStatus>) => {
setStatus(prev => {
const updated = { ...prev, ...newStatus };
onStatusChange?.(updated);
return updated;
});
}, [onStatusChange]);
// Calculate FPS
const updateFps = useCallback(() => {
const now = Date.now();
const elapsed = now - lastFpsUpdateRef.current;
if (elapsed >= 1000) {
const fps = Math.round((frameCountRef.current * 1000) / elapsed);
updateStatus({ fps });
frameCountRef.current = 0;
lastFpsUpdateRef.current = now;
}
}, [updateStatus]);
// Handle incoming WebSocket messages
const handleMessage = useCallback((event: MessageEvent) => {
if (event.data instanceof Blob) {
event.data.arrayBuffer().then(buffer => {
const data = new Uint8Array(buffer);
const frame = decodeVideoFrame(data);
if (frame) {
frameCountRef.current++;
updateFps();
onFrame?.(frame);
}
});
} else if (event.data instanceof ArrayBuffer) {
const data = new Uint8Array(event.data);
const frame = decodeVideoFrame(data);
if (frame) {
frameCountRef.current++;
updateFps();
onFrame?.(frame);
}
}
}, [onFrame, updateFps]);
// Connect to server
const connect = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
return;
}
// Clear any pending reconnect
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
const wsUrl = `${serverUrl}/ws/viewer?session_id=${encodeURIComponent(sessionId)}`;
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
updateStatus({
connected: true,
sessionId,
});
};
ws.onmessage = handleMessage;
ws.onclose = (event) => {
updateStatus({
connected: false,
latencyMs: undefined,
fps: undefined,
});
// Auto-reconnect after 2 seconds
if (!event.wasClean) {
reconnectTimeoutRef.current = window.setTimeout(() => {
connect();
}, 2000);
}
};
ws.onerror = () => {
updateStatus({ connected: false });
};
wsRef.current = ws;
}, [serverUrl, sessionId, handleMessage, updateStatus]);
// Disconnect from server
const disconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
if (wsRef.current) {
wsRef.current.close(1000, 'User disconnected');
wsRef.current = null;
}
updateStatus({
connected: false,
sessionId: undefined,
latencyMs: undefined,
fps: undefined,
});
}, [updateStatus]);
// Send mouse event
const sendMouseEvent = useCallback((event: ProtoMouseEvent) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
const data = encodeMouseEvent(event);
wsRef.current.send(data);
}
}, []);
// Send key event
const sendKeyEvent = useCallback((event: ProtoKeyEvent) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
const data = encodeKeyEvent(event);
wsRef.current.send(data);
}
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
disconnect();
};
}, [disconnect]);
return {
status,
connect,
disconnect,
sendMouseEvent,
sendKeyEvent,
};
}
/**
* Helper to create mouse event from DOM mouse event
*/
export function createMouseEvent(
domEvent: React.MouseEvent<HTMLElement>,
canvasRect: DOMRect,
displayWidth: number,
displayHeight: number,
eventType: MouseEventType
): ProtoMouseEvent {
// Calculate position relative to canvas and scale to display coordinates
const scaleX = displayWidth / canvasRect.width;
const scaleY = displayHeight / canvasRect.height;
const x = Math.round((domEvent.clientX - canvasRect.left) * scaleX);
const y = Math.round((domEvent.clientY - canvasRect.top) * scaleY);
return {
x,
y,
buttons: {
left: (domEvent.buttons & 1) !== 0,
right: (domEvent.buttons & 2) !== 0,
middle: (domEvent.buttons & 4) !== 0,
x1: (domEvent.buttons & 8) !== 0,
x2: (domEvent.buttons & 16) !== 0,
},
wheelDeltaX: 0,
wheelDeltaY: 0,
eventType,
};
}
/**
* Helper to create key event from DOM keyboard event
*/
export function createKeyEvent(
domEvent: React.KeyboardEvent<HTMLElement>,
down: boolean
): ProtoKeyEvent {
const modifiers: Modifiers = {
ctrl: domEvent.ctrlKey,
alt: domEvent.altKey,
shift: domEvent.shiftKey,
meta: domEvent.metaKey,
capsLock: domEvent.getModifierState('CapsLock'),
numLock: domEvent.getModifierState('NumLock'),
};
// Use key code for special keys, unicode for regular characters
const isCharacter = domEvent.key.length === 1;
return {
down,
keyType: isCharacter ? 2 : 0, // KEY_UNICODE or KEY_VK
vkCode: domEvent.keyCode,
scanCode: 0, // Not available in browser
unicode: isCharacter ? domEvent.key : undefined,
modifiers,
};
}