Explore the existing GuruConnect dashboard components at ~/claude-projects/guru-connect/dashboard/

Look at:
1. What components already exist
2. How they're structured
3. What dependencies they use
4. The hooks and types defined
5. What's missing for a full standalone app

Provide a summary of what exists and what would be needed to make it a full standalone dashboard.


I'll explore the GuruConnect dashboard components to understand the existing structure and identify what's needed for a full standalone app.


Bash

cd "C:\Users\MikeSwanson\claude-projects" && find . -type f -name "*.ts*" -o -name "*.json" | grep -i "guru-connect" | head -20

Find GuruConnect project files


Now let me explore the dashboard directory structure more specifically:


cd "C:\Users\MikeSwanson\claude-projects\guru-connect\dashboard" && find . -type f ! -path "./node_modules/*" ! -path "./.next/*" -name "*.ts*" -o -name "*.json" -o -name "*.md" | sort

List dashboard source files


     1→{
     2→  "name": "@guruconnect/dashboard",
     3→  "version": "0.1.0",
     4→  "description": "GuruConnect Remote Desktop Viewer Components",
     5→  "author": "AZ Computer Guru",
     6→  "license": "Proprietary",
     7→  "main": "src/components/index.ts",
     8→  "types": "src/components/index.ts",
     9→  "scripts": {
    10→    "typecheck": "tsc --noEmit",
    11→    "lint": "eslint src"
    12→  },
    13→  "peerDependencies": {
    14→    "react": "^18.0.0",
    15→    "react-dom": "^18.0.0"
    16→  },
    17→  "devDependencies": {
    18→    "@types/react": "^18.2.0",
    19→    "@types/react-dom": "^18.2.0",
    20→    "typescript": "^5.0.0"
    21→  },
    22→  "dependencies": {
    23→    "fzstd": "^0.1.1"
    24→  }
    25→}
    26→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→{
     2→  "compilerOptions": {
     3→    "target": "ES2020",
     4→    "lib": ["ES2020", "DOM", "DOM.Iterable"],
     5→    "module": "ESNext",
     6→    "moduleResolution": "bundler",
     7→    "jsx": "react-jsx",
     8→    "strict": true,
     9→    "noEmit": true,
    10→    "esModuleInterop": true,
    11→    "skipLibCheck": true,
    12→    "forceConsistentCasingInFileNames": true,
    13→    "resolveJsonModule": true,
    14→    "isolatedModules": true,
    15→    "noUnusedLocals": true,
    16→    "noUnusedParameters": true,
    17→    "noFallthroughCasesInSwitch": true
    18→  },
    19→  "include": ["src"],
    20→  "exclude": ["node_modules"]
    21→}
    22→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


./package.json
./src/components/index.ts
./src/components/RemoteViewer.tsx
./src/components/SessionControls.tsx
./src/hooks/useRemoteSession.ts
./src/lib/protobuf.ts
./src/types/protocol.ts
./tsconfig.json


Now let me read the actual component files:


     1→/**
     2→ * GuruConnect Dashboard Components
     3→ *
     4→ * Export all components for use in GuruRMM dashboard
     5→ */
     6→
     7→export { RemoteViewer } from './RemoteViewer';
     8→export { SessionControls } from './SessionControls';
     9→
    10→// Re-export types
    11→export type {
    12→  ConnectionStatus,
    13→  Display,
    14→  DisplayInfo,
    15→  QualitySettings,
    16→  VideoFrame,
    17→  MouseEvent as ProtoMouseEvent,
    18→  KeyEvent as ProtoKeyEvent,
    19→} from '../types/protocol';
    20→
    21→// Re-export hooks
    22→export { useRemoteSession, createMouseEvent, createKeyEvent } from '../hooks/useRemoteSession';
    23→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * GuruConnect Dashboard Components
 *
 * Export all components for use in GuruRMM dashboard
 */

export { RemoteViewer } from './RemoteViewer';
export { SessionControls } from './SessionControls';

// Re-export types
export type {
  ConnectionStatus,
  Display,
  DisplayInfo,
  QualitySettings,
  VideoFrame,
  MouseEvent as ProtoMouseEvent,
  KeyEvent as ProtoKeyEvent,
} from '../types/protocol';

// Re-export hooks
export { useRemoteSession, createMouseEvent, createKeyEvent } from '../hooks/useRemoteSession';


1→/**
     2→ * RemoteViewer Component
     3→ *
     4→ * Canvas-based remote desktop viewer that connects to a GuruConnect
     5→ * agent via the relay server. Handles frame rendering and input capture.
     6→ */
     7→
     8→import React, { useRef, useEffect, useCallback, useState } from 'react';
     9→import { useRemoteSession, createMouseEvent, createKeyEvent } from '../hooks/useRemoteSession';
    10→import type { VideoFrame, ConnectionStatus, MouseEventType } from '../types/protocol';
    11→
    12→interface RemoteViewerProps {
    13→  serverUrl: string;
    14→  sessionId: string;
    15→  className?: string;
    16→  onStatusChange?: (status: ConnectionStatus) => void;
    17→  autoConnect?: boolean;
    18→  showStatusBar?: boolean;
    19→}
    20→
    21→export const RemoteViewer: React.FC<RemoteViewerProps> = ({
    22→  serverUrl,
    23→  sessionId,
    24→  className = '',
    25→  onStatusChange,
    26→  autoConnect = true,
    27→  showStatusBar = true,
    28→}) => {
    29→  const canvasRef = useRef<HTMLCanvasElement>(null);
    30→  const containerRef = useRef<HTMLDivElement>(null);
    31→  const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
    32→
    33→  // Display dimensions from received frames
    34→  const [displaySize, setDisplaySize] = useState({ width: 1920, height: 1080 });
    35→
    36→  // Frame buffer for rendering
    37→  const frameBufferRef = useRef<ImageData | null>(null);
    38→
    39→  // Handle incoming video frames
    40→  const handleFrame = useCallback((frame: VideoFrame) => {
    41→    if (!frame.raw || !canvasRef.current) return;
    42→
    43→    const { width, height, data, compressed, isKeyframe } = frame.raw;
    44→
    45→    // Update display size if changed
    46→    if (width !== displaySize.width || height !== displaySize.height) {
    47→      setDisplaySize({ width, height });
    48→    }
    49→
    50→    // Get or create context
    51→    if (!ctxRef.current) {
    52→      ctxRef.current = canvasRef.current.getContext('2d', {
    53→        alpha: false,
    54→        desynchronized: true,
    55→      });
    56→    }
    57→
    58→    const ctx = ctxRef.current;
    59→    if (!ctx) return;
    60→
    61→    // For MVP, we assume raw BGRA frames
    62→    // In production, handle compressed frames with fzstd
    63→    let frameData = data;
    64→
    65→    // Create or reuse ImageData
    66→    if (!frameBufferRef.current ||
    67→        frameBufferRef.current.width !== width ||
    68→        frameBufferRef.current.height !== height) {
    69→      frameBufferRef.current = ctx.createImageData(width, height);
    70→    }
    71→
    72→    const imageData = frameBufferRef.current;
    73→
    74→    // Convert BGRA to RGBA for canvas
    75→    const pixels = imageData.data;
    76→    const len = Math.min(frameData.length, pixels.length);
    77→
    78→    for (let i = 0; i < len; i += 4) {
    79→      pixels[i] = frameData[i + 2];     // R <- B
    80→      pixels[i + 1] = frameData[i + 1]; // G <- G
    81→      pixels[i + 2] = frameData[i];     // B <- R
    82→      pixels[i + 3] = 255;              // A (opaque)
    83→    }
    84→
    85→    // Draw to canvas
    86→    ctx.putImageData(imageData, 0, 0);
    87→  }, [displaySize]);
    88→
    89→  // Set up session
    90→  const { status, connect, disconnect, sendMouseEvent, sendKeyEvent } = useRemoteSession({
    91→    serverUrl,
    92→    sessionId,
    93→    onFrame: handleFrame,
    94→    onStatusChange,
    95→  });
    96→
    97→  // Auto-connect on mount
    98→  useEffect(() => {
    99→    if (autoConnect) {
   100→      connect();
   101→    }
   102→    return () => {
   103→      disconnect();
   104→    };
   105→  }, [autoConnect, connect, disconnect]);
   106→
   107→  // Update canvas size when display size changes
   108→  useEffect(() => {
   109→    if (canvasRef.current) {
   110→      canvasRef.current.width = displaySize.width;
   111→      canvasRef.current.height = displaySize.height;
   112→      // Reset context reference
   113→      ctxRef.current = null;
   114→      frameBufferRef.current = null;
   115→    }
   116→  }, [displaySize]);
   117→
   118→  // Get canvas rect for coordinate translation
   119→  const getCanvasRect = useCallback(() => {
   120→    return canvasRef.current?.getBoundingClientRect() ?? new DOMRect();
   121→  }, []);
   122→
   123→  // Mouse event handlers
   124→  const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
   125→    const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 0);
   126→    sendMouseEvent(event);
   127→  }, [getCanvasRect, displaySize, sendMouseEvent]);
   128→
   129→  const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
   130→    e.preventDefault();
   131→    const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 1);
   132→    sendMouseEvent(event);
   133→  }, [getCanvasRect, displaySize, sendMouseEvent]);
   134→
   135→  const handleMouseUp = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
   136→    const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 2);
   137→    sendMouseEvent(event);
   138→  }, [getCanvasRect, displaySize, sendMouseEvent]);
   139→
   140→  const handleWheel = useCallback((e: React.WheelEvent<HTMLCanvasElement>) => {
   141→    e.preventDefault();
   142→    const baseEvent = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 3);
   143→    sendMouseEvent({
   144→      ...baseEvent,
   145→      wheelDeltaX: Math.round(e.deltaX),
   146→      wheelDeltaY: Math.round(e.deltaY),
   147→    });
   148→  }, [getCanvasRect, displaySize, sendMouseEvent]);
   149→
   150→  const handleContextMenu = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
   151→    e.preventDefault(); // Prevent browser context menu
   152→  }, []);
   153→
   154→  // Keyboard event handlers
   155→  const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLCanvasElement>) => {
   156→    e.preventDefault();
   157→    const event = createKeyEvent(e, true);
   158→    sendKeyEvent(event);
   159→  }, [sendKeyEvent]);
   160→
   161→  const handleKeyUp = useCallback((e: React.KeyboardEvent<HTMLCanvasElement>) => {
   162→    e.preventDefault();
   163→    const event = createKeyEvent(e, false);
   164→    sendKeyEvent(event);
   165→  }, [sendKeyEvent]);
   166→
   167→  return (
   168→    <div ref={containerRef} className={`remote-viewer ${className}`}>
   169→      <canvas
   170→        ref={canvasRef}
   171→        tabIndex={0}
   172→        onMouseMove={handleMouseMove}
   173→        onMouseDown={handleMouseDown}
   174→        onMouseUp={handleMouseUp}
   175→        onWheel={handleWheel}
   176→        onContextMenu={handleContextMenu}
   177→        onKeyDown={handleKeyDown}
   178→        onKeyUp={handleKeyUp}
   179→        style={{
   180→          width: '100%',
   181→          height: 'auto',
   182→          aspectRatio: `${displaySize.width} / ${displaySize.height}`,
   183→          cursor: 'none', // Hide cursor, remote cursor is shown in frame
   184→          outline: 'none',
   185→          backgroundColor: '#1a1a1a',
   186→        }}
   187→      />
   188→
   189→      {showStatusBar && (
   190→

   199→
   200→            {status.connected ? (
   201→              Connected
   202→            ) : (
   203→              Disconnected
   204→            )}
   205→
   206→          {displaySize.width}x{displaySize.height}
   207→          {status.fps !== undefined && {status.fps} FPS}
   208→          {status.latencyMs !== undefined && {status.latencyMs}ms}
   209→

   210→      )}
   211→

   212→  );
   213→};
   214→
   215→export default RemoteViewer;
   216→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * RemoteViewer Component
 *
 * Canvas-based remote desktop viewer that connects to a GuruConnect
 * agent via the relay server. Handles frame rendering and input capture.
 */

import React, { useRef, useEffect, useCallback, useState } from 'react';
import { useRemoteSession, createMouseEvent, createKeyEvent } from '../hooks/useRemoteSession';
import type { VideoFrame, ConnectionStatus, MouseEventType } from '../types/protocol';

interface RemoteViewerProps {
  serverUrl: string;
  sessionId: string;
  className?: string;
  onStatusChange?: (status: ConnectionStatus) => void;
  autoConnect?: boolean;
  showStatusBar?: boolean;
}

export const RemoteViewer: React.FC<RemoteViewerProps> = ({
  serverUrl,
  sessionId,
  className = '',
  onStatusChange,
  autoConnect = true,
  showStatusBar = true,
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const ctxRef = useRef<CanvasRenderingContext2D | null>(null);

  // Display dimensions from received frames
  const [displaySize, setDisplaySize] = useState({ width: 1920, height: 1080 });

  // Frame buffer for rendering
  const frameBufferRef = useRef<ImageData | null>(null);

  // Handle incoming video frames
  const handleFrame = useCallback((frame: VideoFrame) => {
    if (!frame.raw || !canvasRef.current) return;

    const { width, height, data, compressed, isKeyframe } = frame.raw;

    // Update display size if changed
    if (width !== displaySize.width || height !== displaySize.height) {
      setDisplaySize({ width, height });
    }

    // Get or create context
    if (!ctxRef.current) {
      ctxRef.current = canvasRef.current.getContext('2d', {
        alpha: false,
        desynchronized: true,
      });
    }

    const ctx = ctxRef.current;
    if (!ctx) return;

    // For MVP, we assume raw BGRA frames
    // In production, handle compressed frames with fzstd
    let frameData = data;

    // Create or reuse ImageData
    if (!frameBufferRef.current ||
        frameBufferRef.current.width !== width ||
        frameBufferRef.current.height !== height) {
      frameBufferRef.current = ctx.createImageData(width, height);
    }

    const imageData = frameBufferRef.current;

    // Convert BGRA to RGBA for canvas
    const pixels = imageData.data;
    const len = Math.min(frameData.length, pixels.length);

    for (let i = 0; i < len; i += 4) {
      pixels[i] = frameData[i + 2];     // R <- B
      pixels[i + 1] = frameData[i + 1]; // G <- G
      pixels[i + 2] = frameData[i];     // B <- R
      pixels[i + 3] = 255;              // A (opaque)
    }

    // Draw to canvas
    ctx.putImageData(imageData, 0, 0);
  }, [displaySize]);

  // Set up session
  const { status, connect, disconnect, sendMouseEvent, sendKeyEvent } = useRemoteSession({
    serverUrl,
    sessionId,
    onFrame: handleFrame,
    onStatusChange,
  });

  // Auto-connect on mount
  useEffect(() => {
    if (autoConnect) {
      connect();
    }
    return () => {
      disconnect();
    };
  }, [autoConnect, connect, disconnect]);

  // Update canvas size when display size changes
  useEffect(() => {
    if (canvasRef.current) {
      canvasRef.current.width = displaySize.width;
      canvasRef.current.height = displaySize.height;
      // Reset context reference
      ctxRef.current = null;
      frameBufferRef.current = null;
    }
  }, [displaySize]);

  // Get canvas rect for coordinate translation
  const getCanvasRect = useCallback(() => {
    return canvasRef.current?.getBoundingClientRect() ?? new DOMRect();
  }, []);

  // Mouse event handlers
  const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
    const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 0);
    sendMouseEvent(event);
  }, [getCanvasRect, displaySize, sendMouseEvent]);

  const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
    e.preventDefault();
    const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 1);
    sendMouseEvent(event);
  }, [getCanvasRect, displaySize, sendMouseEvent]);

  const handleMouseUp = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
    const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 2);
    sendMouseEvent(event);
  }, [getCanvasRect, displaySize, sendMouseEvent]);

  const handleWheel = useCallback((e: React.WheelEvent<HTMLCanvasElement>) => {
    e.preventDefault();
    const baseEvent = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 3);
    sendMouseEvent({
      ...baseEvent,
      wheelDeltaX: Math.round(e.deltaX),
      wheelDeltaY: Math.round(e.deltaY),
    });
  }, [getCanvasRect, displaySize, sendMouseEvent]);

  const handleContextMenu = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
    e.preventDefault(); // Prevent browser context menu
  }, []);

  // Keyboard event handlers
  const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLCanvasElement>) => {
    e.preventDefault();
    const event = createKeyEvent(e, true);
    sendKeyEvent(event);
  }, [sendKeyEvent]);

  const handleKeyUp = useCallback((e: React.KeyboardEvent<HTMLCanvasElement>) => {
    e.preventDefault();
    const event = createKeyEvent(e, false);
    sendKeyEvent(event);
  }, [sendKeyEvent]);

  return (
    <div ref={containerRef} className={`remote-viewer ${className}`}>
      <canvas
        ref={canvasRef}
        tabIndex={0}
        onMouseMove={handleMouseMove}
        onMouseDown={handleMouseDown}
        onMouseUp={handleMouseUp}
        onWheel={handleWheel}
        onContextMenu={handleContextMenu}
        onKeyDown={handleKeyDown}
        onKeyUp={handleKeyUp}
        style={{
          width: '100%',
          height: 'auto',
          aspectRatio: `${displaySize.width} / ${displaySize.height}`,
          cursor: 'none', // Hide cursor, remote cursor is shown in frame
          outline: 'none',
          backgroundColor: '#1a1a1a',
        }}
      />

      {showStatusBar && (

            {status.connected ? (
              Connected
            ) : (
              Disconnected
            )}

          {displaySize.width}x{displaySize.height}
          {status.fps !== undefined && {status.fps} FPS}
          {status.latencyMs !== undefined && {status.latencyMs}ms}

      )}

  );
};

export default RemoteViewer;


1→/**
     2→ * Session Controls Component
     3→ *
     4→ * Toolbar for controlling the remote session (quality, displays, special keys)
     5→ */
     6→
     7→import React, { useState } from 'react';
     8→import type { QualitySettings, Display } from '../types/protocol';
     9→
    10→interface SessionControlsProps {
    11→  displays?: Display[];
    12→  currentDisplay?: number;
    13→  onDisplayChange?: (displayId: number) => void;
    14→  quality?: QualitySettings;
    15→  onQualityChange?: (settings: QualitySettings) => void;
    16→  onSpecialKey?: (key: 'ctrl-alt-del' | 'lock-screen' | 'print-screen') => void;
    17→  onDisconnect?: () => void;
    18→}
    19→
    20→export const SessionControls: React.FC<SessionControlsProps> = ({
    21→  displays = [],
    22→  currentDisplay = 0,
    23→  onDisplayChange,
    24→  quality,
    25→  onQualityChange,
    26→  onSpecialKey,
    27→  onDisconnect,
    28→}) => {
    29→  const [showQuality, setShowQuality] = useState(false);
    30→
    31→  const handleQualityPreset = (preset: 'auto' | 'low' | 'balanced' | 'high') => {
    32→    onQualityChange?.({
    33→      preset,
    34→      codec: 'auto',
    35→    });
    36→  };
    37→
    38→  return (
    39→

    46→      {/* Display selector */}
    47→      {displays.length > 1 && (
    48→        <select
    49→          value={currentDisplay}
    50→          onChange={(e) => onDisplayChange?.(Number(e.target.value))}
    51→          style={{
    52→            padding: '4px 8px',
    53→            backgroundColor: '#333',
    54→            color: '#fff',
    55→            border: '1px solid #555',
    56→            borderRadius: '4px',
    57→          }}
    58→        >
    59→          {displays.map((d) => (
    60→            <option key={d.id} value={d.id}>
    61→              {d.name || `Display ${d.id + 1}`}
    62→              {d.isPrimary ? ' (Primary)' : ''}
    63→            </option>
    64→          ))}
    65→        </select>
    66→      )}
    67→
    68→      {/* Quality dropdown */}
    69→

    70→        <button
    71→          onClick={() => setShowQuality(!showQuality)}
    72→          style={{
    73→            padding: '4px 12px',
    74→            backgroundColor: '#333',
    75→            color: '#fff',
    76→            border: '1px solid #555',
    77→            borderRadius: '4px',
    78→            cursor: 'pointer',
    79→          }}
    80→        >
    81→          Quality: {quality?.preset || 'auto'}
    82→        </button>
    83→
    84→        {showQuality && (
    85→

    95→            {(['auto', 'low', 'balanced', 'high'] as const).map((preset) => (
    96→              <button
    97→                key={preset}
    98→                onClick={() => {
    99→                  handleQualityPreset(preset);
   100→                  setShowQuality(false);
   101→                }}
   102→                style={{
   103→                  display: 'block',
   104→                  width: '100%',
   105→                  padding: '8px 16px',
   106→                  backgroundColor: quality?.preset === preset ? '#444' : 'transparent',
   107→                  color: '#fff',
   108→                  border: 'none',
   109→                  textAlign: 'left',
   110→                  cursor: 'pointer',
   111→                }}
   112→              >
   113→                {preset.charAt(0).toUpperCase() + preset.slice(1)}
   114→              </button>
   115→            ))}
   116→

   117→        )}
   118→

   119→
   120→      {/* Special keys */}
   121→      <button
   122→        onClick={() => onSpecialKey?.('ctrl-alt-del')}
   123→        title="Send Ctrl+Alt+Delete"
   124→        style={{
   125→          padding: '4px 12px',
   126→          backgroundColor: '#333',
   127→          color: '#fff',
   128→          border: '1px solid #555',
   129→          borderRadius: '4px',
   130→          cursor: 'pointer',
   131→        }}
   132→      >
   133→        Ctrl+Alt+Del
   134→      </button>
   135→
   136→      <button
   137→        onClick={() => onSpecialKey?.('lock-screen')}
   138→        title="Lock Screen (Win+L)"
   139→        style={{
   140→          padding: '4px 12px',
   141→          backgroundColor: '#333',
   142→          color: '#fff',
   143→          border: '1px solid #555',
   144→          borderRadius: '4px',
   145→          cursor: 'pointer',
   146→        }}
   147→      >
   148→        Lock
   149→      </button>
   150→
   151→      <button
   152→        onClick={() => onSpecialKey?.('print-screen')}
   153→        title="Print Screen"
   154→        style={{
   155→          padding: '4px 12px',
   156→          backgroundColor: '#333',
   157→          color: '#fff',
   158→          border: '1px solid #555',
   159→          borderRadius: '4px',
   160→          cursor: 'pointer',
   161→        }}
   162→      >
   163→        PrtSc
   164→      </button>
   165→
   166→      {/* Spacer */}
   167→

   168→
   169→      {/* Disconnect */}
   170→      <button
   171→        onClick={onDisconnect}
   172→        style={{
   173→          padding: '4px 12px',
   174→          backgroundColor: '#dc2626',
   175→          color: '#fff',
   176→          border: 'none',
   177→          borderRadius: '4px',
   178→          cursor: 'pointer',
   179→        }}
   180→      >
   181→        Disconnect
   182→      </button>
   183→

   184→  );
   185→};
   186→
   187→export default SessionControls;
   188→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * Session Controls Component
 *
 * Toolbar for controlling the remote session (quality, displays, special keys)
 */

import React, { useState } from 'react';
import type { QualitySettings, Display } from '../types/protocol';

interface SessionControlsProps {
  displays?: Display[];
  currentDisplay?: number;
  onDisplayChange?: (displayId: number) => void;
  quality?: QualitySettings;
  onQualityChange?: (settings: QualitySettings) => void;
  onSpecialKey?: (key: 'ctrl-alt-del' | 'lock-screen' | 'print-screen') => void;
  onDisconnect?: () => void;
}

export const SessionControls: React.FC<SessionControlsProps> = ({
  displays = [],
  currentDisplay = 0,
  onDisplayChange,
  quality,
  onQualityChange,
  onSpecialKey,
  onDisconnect,
}) => {
  const [showQuality, setShowQuality] = useState(false);

  const handleQualityPreset = (preset: 'auto' | 'low' | 'balanced' | 'high') => {
    onQualityChange?.({
      preset,
      codec: 'auto',
    });
  };

  return (

      {/* Display selector */}
      {displays.length > 1 && (
        <select
          value={currentDisplay}
          onChange={(e) => onDisplayChange?.(Number(e.target.value))}
          style={{
            padding: '4px 8px',
            backgroundColor: '#333',
            color: '#fff',
            border: '1px solid #555',
            borderRadius: '4px',
          }}
        >
          {displays.map((d) => (
            <option key={d.id} value={d.id}>
              {d.name || `Display ${d.id + 1}`}
              {d.isPrimary ? ' (Primary)' : ''}
            </option>
          ))}
        </select>
      )}

      {/* Quality dropdown */}

        <button
          onClick={() => setShowQuality(!showQuality)}
          style={{
            padding: '4px 12px',
            backgroundColor: '#333',
            color: '#fff',
            border: '1px solid #555',
            borderRadius: '4px',
            cursor: 'pointer',
          }}
        >
          Quality: {quality?.preset || 'auto'}
        </button>

        {showQuality && (

            {(['auto', 'low', 'balanced', 'high'] as const).map((preset) => (
              <button
                key={preset}
                onClick={() => {
                  handleQualityPreset(preset);
                  setShowQuality(false);
                }}
                style={{
                  display: 'block',
                  width: '100%',
                  padding: '8px 16px',
                  backgroundColor: quality?.preset === preset ? '#444' : 'transparent',
                  color: '#fff',
                  border: 'none',
                  textAlign: 'left',
                  cursor: 'pointer',
                }}
              >
                {preset.charAt(0).toUpperCase() + preset.slice(1)}
              </button>
            ))}

        )}

      {/* Special keys */}
      <button
        onClick={() => onSpecialKey?.('ctrl-alt-del')}
        title="Send Ctrl+Alt+Delete"
        style={{
          padding: '4px 12px',
          backgroundColor: '#333',
          color: '#fff',
          border: '1px solid #555',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        Ctrl+Alt+Del
      </button>

      <button
        onClick={() => onSpecialKey?.('lock-screen')}
        title="Lock Screen (Win+L)"
        style={{
          padding: '4px 12px',
          backgroundColor: '#333',
          color: '#fff',
          border: '1px solid #555',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        Lock
      </button>

      <button
        onClick={() => onSpecialKey?.('print-screen')}
        title="Print Screen"
        style={{
          padding: '4px 12px',
          backgroundColor: '#333',
          color: '#fff',
          border: '1px solid #555',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        PrtSc
      </button>

      {/* Spacer */}

      {/* Disconnect */}
      <button
        onClick={onDisconnect}
        style={{
          padding: '4px 12px',
          backgroundColor: '#dc2626',
          color: '#fff',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        Disconnect
      </button>

  );
};

export default SessionControls;


Now let me read the hooks and types files:


     1→/**
     2→ * React hook for managing remote desktop session connection
     3→ */
     4→
     5→import { useState, useEffect, useCallback, useRef } from 'react';
     6→import type { ConnectionStatus, VideoFrame, MouseEvent as ProtoMouseEvent, KeyEvent as ProtoKeyEvent, MouseEventType, KeyEventType, Modifiers } from '../types/protocol';
     7→import { encodeMouseEvent, encodeKeyEvent, decodeVideoFrame } from '../lib/protobuf';
     8→
     9→interface UseRemoteSessionOptions {
    10→  serverUrl: string;
    11→  sessionId: string;
    12→  onFrame?: (frame: VideoFrame) => void;
    13→  onStatusChange?: (status: ConnectionStatus) => void;
    14→}
    15→
    16→interface UseRemoteSessionReturn {
    17→  status: ConnectionStatus;
    18→  connect: () => void;
    19→  disconnect: () => void;
    20→  sendMouseEvent: (event: ProtoMouseEvent) => void;
    21→  sendKeyEvent: (event: ProtoKeyEvent) => void;
    22→}
    23→
    24→export function useRemoteSession(options: UseRemoteSessionOptions): UseRemoteSessionReturn {
    25→  const { serverUrl, sessionId, onFrame, onStatusChange } = options;
    26→
    27→  const [status, setStatus] = useState<ConnectionStatus>({
    28→    connected: false,
    29→  });
    30→
    31→  const wsRef = useRef<WebSocket | null>(null);
    32→  const reconnectTimeoutRef = useRef<number | null>(null);
    33→  const frameCountRef = useRef(0);
    34→  const lastFpsUpdateRef = useRef(Date.now());
    35→
    36→  // Update status and notify
    37→  const updateStatus = useCallback((newStatus: Partial<ConnectionStatus>) => {
    38→    setStatus(prev => {
    39→      const updated = { ...prev, ...newStatus };
    40→      onStatusChange?.(updated);
    41→      return updated;
    42→    });
    43→  }, [onStatusChange]);
    44→
    45→  // Calculate FPS
    46→  const updateFps = useCallback(() => {
    47→    const now = Date.now();
    48→    const elapsed = now - lastFpsUpdateRef.current;
    49→    if (elapsed >= 1000) {
    50→      const fps = Math.round((frameCountRef.current * 1000) / elapsed);
    51→      updateStatus({ fps });
    52→      frameCountRef.current = 0;
    53→      lastFpsUpdateRef.current = now;
    54→    }
    55→  }, [updateStatus]);
    56→
    57→  // Handle incoming WebSocket messages
    58→  const handleMessage = useCallback((event: MessageEvent) => {
    59→    if (event.data instanceof Blob) {
    60→      event.data.arrayBuffer().then(buffer => {
    61→        const data = new Uint8Array(buffer);
    62→        const frame = decodeVideoFrame(data);
    63→        if (frame) {
    64→          frameCountRef.current++;
    65→          updateFps();
    66→          onFrame?.(frame);
    67→        }
    68→      });
    69→    } else if (event.data instanceof ArrayBuffer) {
    70→      const data = new Uint8Array(event.data);
    71→      const frame = decodeVideoFrame(data);
    72→      if (frame) {
    73→        frameCountRef.current++;
    74→        updateFps();
    75→        onFrame?.(frame);
    76→      }
    77→    }
    78→  }, [onFrame, updateFps]);
    79→
    80→  // Connect to server
    81→  const connect = useCallback(() => {
    82→    if (wsRef.current?.readyState === WebSocket.OPEN) {
    83→      return;
    84→    }
    85→
    86→    // Clear any pending reconnect
    87→    if (reconnectTimeoutRef.current) {
    88→      window.clearTimeout(reconnectTimeoutRef.current);
    89→      reconnectTimeoutRef.current = null;
    90→    }
    91→
    92→    const wsUrl = `${serverUrl}/ws/viewer?session_id=${encodeURIComponent(sessionId)}`;
    93→    const ws = new WebSocket(wsUrl);
    94→    ws.binaryType = 'arraybuffer';
    95→
    96→    ws.onopen = () => {
    97→      updateStatus({
    98→        connected: true,
    99→        sessionId,
   100→      });
   101→    };
   102→
   103→    ws.onmessage = handleMessage;
   104→
   105→    ws.onclose = (event) => {
   106→      updateStatus({
   107→        connected: false,
   108→        latencyMs: undefined,
   109→        fps: undefined,
   110→      });
   111→
   112→      // Auto-reconnect after 2 seconds
   113→      if (!event.wasClean) {
   114→        reconnectTimeoutRef.current = window.setTimeout(() => {
   115→          connect();
   116→        }, 2000);
   117→      }
   118→    };
   119→
   120→    ws.onerror = () => {
   121→      updateStatus({ connected: false });
   122→    };
   123→
   124→    wsRef.current = ws;
   125→  }, [serverUrl, sessionId, handleMessage, updateStatus]);
   126→
   127→  // Disconnect from server
   128→  const disconnect = useCallback(() => {
   129→    if (reconnectTimeoutRef.current) {
   130→      window.clearTimeout(reconnectTimeoutRef.current);
   131→      reconnectTimeoutRef.current = null;
   132→    }
   133→
   134→    if (wsRef.current) {
   135→      wsRef.current.close(1000, 'User disconnected');
   136→      wsRef.current = null;
   137→    }
   138→
   139→    updateStatus({
   140→      connected: false,
   141→      sessionId: undefined,
   142→      latencyMs: undefined,
   143→      fps: undefined,
   144→    });
   145→  }, [updateStatus]);
   146→
   147→  // Send mouse event
   148→  const sendMouseEvent = useCallback((event: ProtoMouseEvent) => {
   149→    if (wsRef.current?.readyState === WebSocket.OPEN) {
   150→      const data = encodeMouseEvent(event);
   151→      wsRef.current.send(data);
   152→    }
   153→  }, []);
   154→
   155→  // Send key event
   156→  const sendKeyEvent = useCallback((event: ProtoKeyEvent) => {
   157→    if (wsRef.current?.readyState === WebSocket.OPEN) {
   158→      const data = encodeKeyEvent(event);
   159→      wsRef.current.send(data);
   160→    }
   161→  }, []);
   162→
   163→  // Cleanup on unmount
   164→  useEffect(() => {
   165→    return () => {
   166→      disconnect();
   167→    };
   168→  }, [disconnect]);
   169→
   170→  return {
   171→    status,
   172→    connect,
   173→    disconnect,
   174→    sendMouseEvent,
   175→    sendKeyEvent,
   176→  };
   177→}
   178→
   179→/**
   180→ * Helper to create mouse event from DOM mouse event
   181→ */
   182→export function createMouseEvent(
   183→  domEvent: React.MouseEvent<HTMLElement>,
   184→  canvasRect: DOMRect,
   185→  displayWidth: number,
   186→  displayHeight: number,
   187→  eventType: MouseEventType
   188→): ProtoMouseEvent {
   189→  // Calculate position relative to canvas and scale to display coordinates
   190→  const scaleX = displayWidth / canvasRect.width;
   191→  const scaleY = displayHeight / canvasRect.height;
   192→
   193→  const x = Math.round((domEvent.clientX - canvasRect.left) * scaleX);
   194→  const y = Math.round((domEvent.clientY - canvasRect.top) * scaleY);
   195→
   196→  return {
   197→    x,
   198→    y,
   199→    buttons: {
   200→      left: (domEvent.buttons & 1) !== 0,
   201→      right: (domEvent.buttons & 2) !== 0,
   202→      middle: (domEvent.buttons & 4) !== 0,
   203→      x1: (domEvent.buttons & 8) !== 0,
   204→      x2: (domEvent.buttons & 16) !== 0,
   205→    },
   206→    wheelDeltaX: 0,
   207→    wheelDeltaY: 0,
   208→    eventType,
   209→  };
   210→}
   211→
   212→/**
   213→ * Helper to create key event from DOM keyboard event
   214→ */
   215→export function createKeyEvent(
   216→  domEvent: React.KeyboardEvent<HTMLElement>,
   217→  down: boolean
   218→): ProtoKeyEvent {
   219→  const modifiers: Modifiers = {
   220→    ctrl: domEvent.ctrlKey,
   221→    alt: domEvent.altKey,
   222→    shift: domEvent.shiftKey,
   223→    meta: domEvent.metaKey,
   224→    capsLock: domEvent.getModifierState('CapsLock'),
   225→    numLock: domEvent.getModifierState('NumLock'),
   226→  };
   227→
   228→  // Use key code for special keys, unicode for regular characters
   229→  const isCharacter = domEvent.key.length === 1;
   230→
   231→  return {
   232→    down,
   233→    keyType: isCharacter ? 2 : 0, // KEY_UNICODE or KEY_VK
   234→    vkCode: domEvent.keyCode,
   235→    scanCode: 0, // Not available in browser
   236→    unicode: isCharacter ? domEvent.key : undefined,
   237→    modifiers,
   238→  };
   239→}
   240→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * 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,
  };
}


     1→/**
     2→ * TypeScript types matching guruconnect.proto definitions
     3→ * These are used for WebSocket message handling in the viewer
     4→ */
     5→
     6→export enum SessionType {
     7→  SCREEN_CONTROL = 0,
     8→  VIEW_ONLY = 1,
     9→  BACKSTAGE = 2,
    10→  FILE_TRANSFER = 3,
    11→}
    12→
    13→export interface SessionRequest {
    14→  agentId: string;
    15→  sessionToken: string;
    16→  sessionType: SessionType;
    17→  clientVersion: string;
    18→}
    19→
    20→export interface SessionResponse {
    21→  success: boolean;
    22→  sessionId: string;
    23→  error?: string;
    24→  displayInfo?: DisplayInfo;
    25→}
    26→
    27→export interface DisplayInfo {
    28→  displays: Display[];
    29→  primaryDisplay: number;
    30→}
    31→
    32→export interface Display {
    33→  id: number;
    34→  name: string;
    35→  x: number;
    36→  y: number;
    37→  width: number;
    38→  height: number;
    39→  isPrimary: boolean;
    40→}
    41→
    42→export interface DirtyRect {
    43→  x: number;
    44→  y: number;
    45→  width: number;
    46→  height: number;
    47→}
    48→
    49→export interface RawFrame {
    50→  width: number;
    51→  height: number;
    52→  data: Uint8Array;
    53→  compressed: boolean;
    54→  dirtyRects: DirtyRect[];
    55→  isKeyframe: boolean;
    56→}
    57→
    58→export interface EncodedFrame {
    59→  data: Uint8Array;
    60→  keyframe: boolean;
    61→  pts: number;
    62→  dts: number;
    63→}
    64→
    65→export interface VideoFrame {
    66→  timestamp: number;
    67→  displayId: number;
    68→  sequence: number;
    69→  encoding: 'raw' | 'vp9' | 'h264' | 'h265';
    70→  raw?: RawFrame;
    71→  encoded?: EncodedFrame;
    72→}
    73→
    74→export enum MouseEventType {
    75→  MOUSE_MOVE = 0,
    76→  MOUSE_DOWN = 1,
    77→  MOUSE_UP = 2,
    78→  MOUSE_WHEEL = 3,
    79→}
    80→
    81→export interface MouseButtons {
    82→  left: boolean;
    83→  right: boolean;
    84→  middle: boolean;
    85→  x1: boolean;
    86→  x2: boolean;
    87→}
    88→
    89→export interface MouseEvent {
    90→  x: number;
    91→  y: number;
    92→  buttons: MouseButtons;
    93→  wheelDeltaX: number;
    94→  wheelDeltaY: number;
    95→  eventType: MouseEventType;
    96→}
    97→
    98→export enum KeyEventType {
    99→  KEY_VK = 0,
   100→  KEY_SCAN = 1,
   101→  KEY_UNICODE = 2,
   102→}
   103→
   104→export interface Modifiers {
   105→  ctrl: boolean;
   106→  alt: boolean;
   107→  shift: boolean;
   108→  meta: boolean;
   109→  capsLock: boolean;
   110→  numLock: boolean;
   111→}
   112→
   113→export interface KeyEvent {
   114→  down: boolean;
   115→  keyType: KeyEventType;
   116→  vkCode: number;
   117→  scanCode: number;
   118→  unicode?: string;
   119→  modifiers: Modifiers;
   120→}
   121→
   122→export interface QualitySettings {
   123→  preset: 'auto' | 'low' | 'balanced' | 'high';
   124→  customFps?: number;
   125→  customBitrate?: number;
   126→  codec: 'auto' | 'raw' | 'vp9' | 'h264' | 'h265';
   127→}
   128→
   129→export interface ConnectionStatus {
   130→  connected: boolean;
   131→  sessionId?: string;
   132→  latencyMs?: number;
   133→  fps?: number;
   134→  bitrateKbps?: number;
   135→}
   136→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * TypeScript types matching guruconnect.proto definitions
 * These are used for WebSocket message handling in the viewer
 */

export enum SessionType {
  SCREEN_CONTROL = 0,
  VIEW_ONLY = 1,
  BACKSTAGE = 2,
  FILE_TRANSFER = 3,
}

export interface SessionRequest {
  agentId: string;
  sessionToken: string;
  sessionType: SessionType;
  clientVersion: string;
}

export interface SessionResponse {
  success: boolean;
  sessionId: string;
  error?: string;
  displayInfo?: DisplayInfo;
}

export interface DisplayInfo {
  displays: Display[];
  primaryDisplay: number;
}

export interface Display {
  id: number;
  name: string;
  x: number;
  y: number;
  width: number;
  height: number;
  isPrimary: boolean;
}

export interface DirtyRect {
  x: number;
  y: number;
  width: number;
  height: number;
}

export interface RawFrame {
  width: number;
  height: number;
  data: Uint8Array;
  compressed: boolean;
  dirtyRects: DirtyRect[];
  isKeyframe: boolean;
}

export interface EncodedFrame {
  data: Uint8Array;
  keyframe: boolean;
  pts: number;
  dts: number;
}

export interface VideoFrame {
  timestamp: number;
  displayId: number;
  sequence: number;
  encoding: 'raw' | 'vp9' | 'h264' | 'h265';
  raw?: RawFrame;
  encoded?: EncodedFrame;
}

export enum MouseEventType {
  MOUSE_MOVE = 0,
  MOUSE_DOWN = 1,
  MOUSE_UP = 2,
  MOUSE_WHEEL = 3,
}

export interface MouseButtons {
  left: boolean;
  right: boolean;
  middle: boolean;
  x1: boolean;
  x2: boolean;
}

export interface MouseEvent {
  x: number;
  y: number;
  buttons: MouseButtons;
  wheelDeltaX: number;
  wheelDeltaY: number;
  eventType: MouseEventType;
}

export enum KeyEventType {
  KEY_VK = 0,
  KEY_SCAN = 1,
  KEY_UNICODE = 2,
}

export interface Modifiers {
  ctrl: boolean;
  alt: boolean;
  shift: boolean;
  meta: boolean;
  capsLock: boolean;
  numLock: boolean;
}

export interface KeyEvent {
  down: boolean;
  keyType: KeyEventType;
  vkCode: number;
  scanCode: number;
  unicode?: string;
  modifiers: Modifiers;
}

export interface QualitySettings {
  preset: 'auto' | 'low' | 'balanced' | 'high';
  customFps?: number;
  customBitrate?: number;
  codec: 'auto' | 'raw' | 'vp9' | 'h264' | 'h265';
}

export interface ConnectionStatus {
  connected: boolean;
  sessionId?: string;
  latencyMs?: number;
  fps?: number;
  bitrateKbps?: number;
}


     1→/**
     2→ * Minimal protobuf encoder/decoder for GuruConnect messages
     3→ *
     4→ * For MVP, we use a simplified binary format. In production,
     5→ * this would use a proper protobuf library like protobufjs.
     6→ */
     7→
     8→import type { MouseEvent, KeyEvent, MouseEventType, KeyEventType, VideoFrame, RawFrame } from '../types/protocol';
     9→
    10→// Message type identifiers (matching proto field numbers)
    11→const MSG_VIDEO_FRAME = 10;
    12→const MSG_MOUSE_EVENT = 20;
    13→const MSG_KEY_EVENT = 21;
    14→
    15→/**
    16→ * Encode a mouse event to binary format
    17→ */
    18→export function encodeMouseEvent(event: MouseEvent): Uint8Array {
    19→  const buffer = new ArrayBuffer(32);
    20→  const view = new DataView(buffer);
    21→
    22→  // Message type
    23→  view.setUint8(0, MSG_MOUSE_EVENT);
    24→
    25→  // Event type
    26→  view.setUint8(1, event.eventType);
    27→
    28→  // Coordinates (scaled to 16-bit for efficiency)
    29→  view.setInt16(2, event.x, true);
    30→  view.setInt16(4, event.y, true);
    31→
    32→  // Buttons bitmask
    33→  let buttons = 0;
    34→  if (event.buttons.left) buttons |= 1;
    35→  if (event.buttons.right) buttons |= 2;
    36→  if (event.buttons.middle) buttons |= 4;
    37→  if (event.buttons.x1) buttons |= 8;
    38→  if (event.buttons.x2) buttons |= 16;
    39→  view.setUint8(6, buttons);
    40→
    41→  // Wheel deltas
    42→  view.setInt16(7, event.wheelDeltaX, true);
    43→  view.setInt16(9, event.wheelDeltaY, true);
    44→
    45→  return new Uint8Array(buffer, 0, 11);
    46→}
    47→
    48→/**
    49→ * Encode a key event to binary format
    50→ */
    51→export function encodeKeyEvent(event: KeyEvent): Uint8Array {
    52→  const buffer = new ArrayBuffer(32);
    53→  const view = new DataView(buffer);
    54→
    55→  // Message type
    56→  view.setUint8(0, MSG_KEY_EVENT);
    57→
    58→  // Key down/up
    59→  view.setUint8(1, event.down ? 1 : 0);
    60→
    61→  // Key type
    62→  view.setUint8(2, event.keyType);
    63→
    64→  // Virtual key code
    65→  view.setUint16(3, event.vkCode, true);
    66→
    67→  // Scan code
    68→  view.setUint16(5, event.scanCode, true);
    69→
    70→  // Modifiers bitmask
    71→  let mods = 0;
    72→  if (event.modifiers.ctrl) mods |= 1;
    73→  if (event.modifiers.alt) mods |= 2;
    74→  if (event.modifiers.shift) mods |= 4;
    75→  if (event.modifiers.meta) mods |= 8;
    76→  if (event.modifiers.capsLock) mods |= 16;
    77→  if (event.modifiers.numLock) mods |= 32;
    78→  view.setUint8(7, mods);
    79→
    80→  // Unicode character (if present)
    81→  if (event.unicode && event.unicode.length > 0) {
    82→    const charCode = event.unicode.charCodeAt(0);
    83→    view.setUint16(8, charCode, true);
    84→    return new Uint8Array(buffer, 0, 10);
    85→  }
    86→
    87→  return new Uint8Array(buffer, 0, 8);
    88→}
    89→
    90→/**
    91→ * Decode a video frame from binary format
    92→ */
    93→export function decodeVideoFrame(data: Uint8Array): VideoFrame | null {
    94→  if (data.length < 2) return null;
    95→
    96→  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
    97→  const msgType = view.getUint8(0);
    98→
    99→  if (msgType !== MSG_VIDEO_FRAME) return null;
   100→
   101→  const encoding = view.getUint8(1);
   102→  const displayId = view.getUint8(2);
   103→  const sequence = view.getUint32(3, true);
   104→  const timestamp = Number(view.getBigInt64(7, true));
   105→
   106→  // Frame dimensions
   107→  const width = view.getUint16(15, true);
   108→  const height = view.getUint16(17, true);
   109→
   110→  // Compressed flag
   111→  const compressed = view.getUint8(19) === 1;
   112→
   113→  // Is keyframe
   114→  const isKeyframe = view.getUint8(20) === 1;
   115→
   116→  // Frame data starts at offset 21
   117→  const frameData = data.slice(21);
   118→
   119→  const encodingStr = ['raw', 'vp9', 'h264', 'h265'][encoding] as 'raw' | 'vp9' | 'h264' | 'h265';
   120→
   121→  if (encodingStr === 'raw') {
   122→    return {
   123→      timestamp,
   124→      displayId,
   125→      sequence,
   126→      encoding: 'raw',
   127→      raw: {
   128→        width,
   129→        height,
   130→        data: frameData,
   131→        compressed,
   132→        dirtyRects: [], // TODO: Parse dirty rects
   133→        isKeyframe,
   134→      },
   135→    };
   136→  }
   137→
   138→  return {
   139→    timestamp,
   140→    displayId,
   141→    sequence,
   142→    encoding: encodingStr,
   143→    encoded: {
   144→      data: frameData,
   145→      keyframe: isKeyframe,
   146→      pts: timestamp,
   147→      dts: timestamp,
   148→    },
   149→  };
   150→}
   151→
   152→/**
   153→ * Simple zstd decompression placeholder
   154→ * In production, use a proper zstd library like fzstd
   155→ */
   156→export async function decompressZstd(data: Uint8Array): Promise<Uint8Array> {
   157→  // For MVP, assume uncompressed frames or use fzstd library
   158→  // This is a placeholder - actual implementation would use:
   159→  // import { decompress } from 'fzstd';
   160→  // return decompress(data);
   161→  return data;
   162→}
   163→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * Minimal protobuf encoder/decoder for GuruConnect messages
 *
 * For MVP, we use a simplified binary format. In production,
 * this would use a proper protobuf library like protobufjs.
 */

import type { MouseEvent, KeyEvent, MouseEventType, KeyEventType, VideoFrame, RawFrame } from '../types/protocol';

// Message type identifiers (matching proto field numbers)
const MSG_VIDEO_FRAME = 10;
const MSG_MOUSE_EVENT = 20;
const MSG_KEY_EVENT = 21;

/**
 * Encode a mouse event to binary format
 */
export function encodeMouseEvent(event: MouseEvent): Uint8Array {
  const buffer = new ArrayBuffer(32);
  const view = new DataView(buffer);

  // Message type
  view.setUint8(0, MSG_MOUSE_EVENT);

  // Event type
  view.setUint8(1, event.eventType);

  // Coordinates (scaled to 16-bit for efficiency)
  view.setInt16(2, event.x, true);
  view.setInt16(4, event.y, true);

  // Buttons bitmask
  let buttons = 0;
  if (event.buttons.left) buttons |= 1;
  if (event.buttons.right) buttons |= 2;
  if (event.buttons.middle) buttons |= 4;
  if (event.buttons.x1) buttons |= 8;
  if (event.buttons.x2) buttons |= 16;
  view.setUint8(6, buttons);

  // Wheel deltas
  view.setInt16(7, event.wheelDeltaX, true);
  view.setInt16(9, event.wheelDeltaY, true);

  return new Uint8Array(buffer, 0, 11);
}

/**
 * Encode a key event to binary format
 */
export function encodeKeyEvent(event: KeyEvent): Uint8Array {
  const buffer = new ArrayBuffer(32);
  const view = new DataView(buffer);

  // Message type
  view.setUint8(0, MSG_KEY_EVENT);

  // Key down/up
  view.setUint8(1, event.down ? 1 : 0);

  // Key type
  view.setUint8(2, event.keyType);

  // Virtual key code
  view.setUint16(3, event.vkCode, true);

  // Scan code
  view.setUint16(5, event.scanCode, true);

  // Modifiers bitmask
  let mods = 0;
  if (event.modifiers.ctrl) mods |= 1;
  if (event.modifiers.alt) mods |= 2;
  if (event.modifiers.shift) mods |= 4;
  if (event.modifiers.meta) mods |= 8;
  if (event.modifiers.capsLock) mods |= 16;
  if (event.modifiers.numLock) mods |= 32;
  view.setUint8(7, mods);

  // Unicode character (if present)
  if (event.unicode && event.unicode.length > 0) {
    const charCode = event.unicode.charCodeAt(0);
    view.setUint16(8, charCode, true);
    return new Uint8Array(buffer, 0, 10);
  }

  return new Uint8Array(buffer, 0, 8);
}

/**
 * Decode a video frame from binary format
 */
export function decodeVideoFrame(data: Uint8Array): VideoFrame | null {
  if (data.length < 2) return null;

  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  const msgType = view.getUint8(0);

  if (msgType !== MSG_VIDEO_FRAME) return null;

  const encoding = view.getUint8(1);
  const displayId = view.getUint8(2);
  const sequence = view.getUint32(3, true);
  const timestamp = Number(view.getBigInt64(7, true));

  // Frame dimensions
  const width = view.getUint16(15, true);
  const height = view.getUint16(17, true);

  // Compressed flag
  const compressed = view.getUint8(19) === 1;

  // Is keyframe
  const isKeyframe = view.getUint8(20) === 1;

  // Frame data starts at offset 21
  const frameData = data.slice(21);

  const encodingStr = ['raw', 'vp9', 'h264', 'h265'][encoding] as 'raw' | 'vp9' | 'h264' | 'h265';

  if (encodingStr === 'raw') {
    return {
      timestamp,
      displayId,
      sequence,
      encoding: 'raw',
      raw: {
        width,
        height,
        data: frameData,
        compressed,
        dirtyRects: [], // TODO: Parse dirty rects
        isKeyframe,
      },
    };
  }

  return {
    timestamp,
    displayId,
    sequence,
    encoding: encodingStr,
    encoded: {
      data: frameData,
      keyframe: isKeyframe,
      pts: timestamp,
      dts: timestamp,
    },
  };
}

/**
 * Simple zstd decompression placeholder
 * In production, use a proper zstd library like fzstd
 */
export async function decompressZstd(data: Uint8Array): Promise<Uint8Array> {
  // For MVP, assume uncompressed frames or use fzstd library
  // This is a placeholder - actual implementation would use:
  // import { decompress } from 'fzstd';
  // return decompress(data);
  return data;
}


Now let me check the root directory structure and look for any additional configuration files:


Bash

cd "C:\Users\MikeSwanson\claude-projects\guru-connect\dashboard" && dir /a

List all files in dashboard root


cd "C:\Users\MikeSwanson\claude-projects\guru-connect" && find . -maxdepth 1 -type f \( -name "*.md" -o -name "*.toml" -o -name "*.json" \)

List project root config files


Error: Exit code 2
dir: cannot access '/a': No such file or directory


     1→# GuruConnect
     2→
     3→Remote desktop solution similar to ScreenConnect, integrated with GuruRMM.
     4→
     5→## Project Overview
     6→
     7→GuruConnect provides remote screen control and backstage tools for Windows systems.
     8→It's designed to be fast, secure, and enterprise-ready.
     9→
    10→## Architecture
    11→
    12→```
    13→┌─────────────────┐         ┌─────────────────┐         ┌─────────────────┐
    14→│   Dashboard     │◄───────►│  GuruConnect    │◄───────►│  GuruConnect    │
    15→│   (React)       │  WSS    │  Server (Rust)  │  WSS    │  Agent (Rust)   │
    16→└─────────────────┘         └─────────────────┘         └─────────────────┘
    17→```
    18→
    19→## Directory Structure
    20→
    21→- `agent/` - Windows remote desktop agent (Rust)
    22→- `server/` - Relay server (Rust + Axum)
    23→- `dashboard/` - Web viewer (React, to be integrated with GuruRMM)
    24→- `proto/` - Protobuf protocol definitions
    25→
    26→## Building
    27→
    28→### Prerequisites
    29→
    30→- Rust 1.75+ (install via rustup)
    31→- Windows SDK (for agent)
    32→- protoc (Protocol Buffers compiler)
    33→
    34→### Build Commands
    35→
    36→```bash
    37→# Build all (from workspace root)
    38→cargo build --release
    39→
    40→# Build agent only
    41→cargo build -p guruconnect-agent --release
    42→
    43→# Build server only
    44→cargo build -p guruconnect-server --release
    45→```
    46→
    47→### Cross-compilation (Agent for Windows)
    48→
    49→From Linux build server:
    50→```bash
    51→# Install Windows target
    52→rustup target add x86_64-pc-windows-msvc
    53→
    54→# Build (requires cross or appropriate linker)
    55→cross build -p guruconnect-agent --target x86_64-pc-windows-msvc --release
    56→```
    57→
    58→## Development
    59→
    60→### Running the Server
    61→
    62→```bash
    63→# Development
    64→cargo run -p guruconnect-server
    65→
    66→# With environment variables
    67→DATABASE_URL=postgres://... JWT_SECRET=... cargo run -p guruconnect-server
    68→```
    69→
    70→### Testing the Agent
    71→
    72→The agent must be run on Windows:
    73→```powershell
    74→# Run from Windows
    75→.\target\release\guruconnect-agent.exe
    76→```
    77→
    78→## Protocol
    79→
    80→Uses Protocol Buffers for efficient message serialization.
    81→See `proto/guruconnect.proto` for message definitions.
    82→
    83→Key message types:
    84→- `VideoFrame` - Screen frames (raw+zstd, VP9, H264)
    85→- `MouseEvent` - Mouse input
    86→- `KeyEvent` - Keyboard input
    87→- `SessionRequest/Response` - Session management
    88→
    89→## Encoding Strategy
    90→
    91→| Scenario | Encoding |
    92→|----------|----------|
    93→| LAN (<20ms RTT) | Raw BGRA + Zstd + dirty rects |
    94→| WAN + GPU | H264 hardware |
    95→| WAN - GPU | VP9 software |
    96→
    97→## Key References
    98→
    99→- RustDesk source: `~/claude-projects/reference/rustdesk/`
   100→- GuruRMM: `~/claude-projects/gururmm/`
   101→- Plan: `~/.claude/plans/shimmering-wandering-crane.md`
   102→
   103→## Phase 1 MVP Goals
   104→
   105→1. DXGI screen capture with GDI fallback
   106→2. Raw + Zstd encoding with dirty rectangle detection
   107→3. Mouse and keyboard input injection
   108→4. WebSocket relay through server
   109→5. Basic React viewer
   110→
   111→## Security Considerations
   112→
   113→- All connections use TLS
   114→- JWT authentication for dashboard users
   115→- API key authentication for agents
   116→- Session audit logging
   117→- Optional session recording (Phase 4)
   118→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

# GuruConnect

Remote desktop solution similar to ScreenConnect, integrated with GuruRMM.

## Project Overview

GuruConnect provides remote screen control and backstage tools for Windows systems.
It's designed to be fast, secure, and enterprise-ready.

## Architecture

```
┌─────────────────┐         ┌─────────────────┐         ┌─────────────────┐
│   Dashboard     │◄───────►│  GuruConnect    │◄───────►│  GuruConnect    │
│   (React)       │  WSS    │  Server (Rust)  │  WSS    │  Agent (Rust)   │
└─────────────────┘         └─────────────────┘         └─────────────────┘
```

## Directory Structure

- `agent/` - Windows remote desktop agent (Rust)
- `server/` - Relay server (Rust + Axum)
- `dashboard/` - Web viewer (React, to be integrated with GuruRMM)
- `proto/` - Protobuf protocol definitions

## Building

### Prerequisites

- Rust 1.75+ (install via rustup)
- Windows SDK (for agent)
- protoc (Protocol Buffers compiler)

### Build Commands

```bash
# Build all (from workspace root)
cargo build --release

# Build agent only
cargo build -p guruconnect-agent --release

# Build server only
cargo build -p guruconnect-server --release
```

### Cross-compilation (Agent for Windows)

From Linux build server:
```bash
# Install Windows target
rustup target add x86_64-pc-windows-msvc

# Build (requires cross or appropriate linker)
cross build -p guruconnect-agent --target x86_64-pc-windows-msvc --release
```

## Development

### Running the Server

```bash
# Development
cargo run -p guruconnect-server

# With environment variables
DATABASE_URL=postgres://... JWT_SECRET=... cargo run -p guruconnect-server
```

### Testing the Agent

The agent must be run on Windows:
```powershell
# Run from Windows
.\target\release\guruconnect-agent.exe
```

## Protocol

Uses Protocol Buffers for efficient message serialization.
See `proto/guruconnect.proto` for message definitions.

Key message types:
- `VideoFrame` - Screen frames (raw+zstd, VP9, H264)
- `MouseEvent` - Mouse input
- `KeyEvent` - Keyboard input
- `SessionRequest/Response` - Session management

## Encoding Strategy

| Scenario | Encoding |
|----------|----------|
| LAN (<20ms RTT) | Raw BGRA + Zstd + dirty rects |
| WAN + GPU | H264 hardware |
| WAN - GPU | VP9 software |

## Key References

- RustDesk source: `~/claude-projects/reference/rustdesk/`
- GuruRMM: `~/claude-projects/gururmm/`
- Plan: `~/.claude/plans/shimmering-wandering-crane.md`

## Phase 1 MVP Goals

1. DXGI screen capture with GDI fallback
2. Raw + Zstd encoding with dirty rectangle detection
3. Mouse and keyboard input injection
4. WebSocket relay through server
5. Basic React viewer

## Security Considerations

- All connections use TLS
- JWT authentication for dashboard users
- API key authentication for agents
- Session audit logging
- Optional session recording (Phase 4)


./Cargo.toml
./CLAUDE.md


Let me check if there are any example or app files in the dashboard:


cd "C:\Users\MikeSwanson\claude-projects\guru-connect\dashboard" && find . -type f -name "*.tsx" -o -name "*.ts" -o -name "*.html" -o -name "*.css" 2>/dev/null | grep -v node_modules | sort

Find all TypeScript and layout files in dashboard


List dashboard directory contents


Error: Exit code 2
/usr/bin/bash: eval: line 1: unexpected EOF while looking for matching `"'


./src/components/index.ts
./src/components/RemoteViewer.tsx
./src/components/SessionControls.tsx
./src/hooks/useRemoteSession.ts
./src/lib/protobuf.ts
./src/types/protocol.ts


Let me try a different approach:


find "C:\Users\MikeSwanson\claude-projects\guru-connect\dashboard" -maxdepth 1 -type f 2>/dev/null

List files in dashboard root only


find "C:\Users\MikeSwanson\claude-projects\guru-connect\dashboard\src" -type f 2>/dev/null | sort

List all source files


C:\Users\MikeSwanson\claude-projects\guru-connect\dashboard/package.json
C:\Users\MikeSwanson\claude-projects\guru-connect\dashboard/tsconfig.json


Perfect! Now I have a complete picture. Let me provide a comprehensive summary:

## GuruConnect Dashboard Analysis

I've explored the entire GuruConnect dashboard codebase. Here's what exists and what's needed for a full standalone app:

### EXISTING COMPONENTS

**1. RemoteViewer.tsx** (Canvas-based viewer)
- Connects to GuruConnect server via WebSocket
- Renders incoming video frames to HTML canvas
- Handles BGRA-to-RGBA color conversion
- Supports frame resizing and display dimension updates
- Full mouse event support (move, down, up, wheel)
- Full keyboard event support (down, up with modifiers)
- Built-in status bar showing connection state, resolution, FPS, and latency
- Auto-connect and disconnect lifecycle management

**2. SessionControls.tsx** (Control toolbar)
- Display selector for multi-monitor systems
- Quality settings dropdown (auto, low, balanced, high)
- Special key buttons (Ctrl+Alt+Del, Win+L, Print Screen)
- Disconnect button
- Styled with dark theme

**3. useRemoteSession Hook**
- WebSocket connection management with auto-reconnect (2-second retry)
- Frame decoding and handling
- Mouse event encoding and sending
- Key event encoding and sending
- FPS calculation and status updates
- Connection status tracking (connected, sessionId, fps, latency)

**4. Protocol Layer**
- Type definitions matching protobuf schema (TypeScript)
- Message type enums (SessionType, MouseEventType, KeyEventType)
- Binary encoder/decoder for mouse/key events
- Video frame decoder with raw/VP9/H264/H265 support
- Modifiers tracking (Ctrl, Alt, Shift, Meta, CapsLock, NumLock)

**5. Types & Interfaces**
- SessionRequest/Response for session management
- DisplayInfo with multi-monitor support
- VideoFrame with raw and encoded variants
- QualitySettings with codec selection
- ConnectionStatus for UI feedback

### DEPENDENCIES
- React 18.0+
- React DOM 18.0+
- TypeScript 5.0+
- fzstd (for Zstd decompression, placeholder implementation)

### WHAT'S MISSING FOR A FULL STANDALONE APP

**Critical Application Files:**
1. **Entry point** - No `index.tsx`, `main.tsx`, or `App.tsx` file
2. **HTML template** - No `index.html` file with root div
3. **Build configuration** - No webpack, Vite, or bundler config
4. **Build scripts** - Package.json only has typecheck and lint, no build script

**UI/UX Components:**
5. **Login/Authentication** - No session token acquisition flow
6. **Session browser/list** - No way to discover available sessions
7. **Connection dialog** - No form to enter serverUrl or sessionId
8. **Error handling UI** - No error messages or reconnect indicators
9. **Settings panel** - No bitrate, FPS, or codec selection UI
10. **Responsive layout** - No responsive container or mobile support
11. **Fullscreen toggle** - No fullscreen functionality
12. **Zoom/pan controls** - No scaling or panning for large displays

**Infrastructure:**
13. **CSS styling** - Only inline styles, no stylesheet
14. **Icons** - No icon library
15. **State management** - No global state (Redux, Zustand, etc.)
16. **Error boundaries** - No React error boundaries

**Features:**
17. **Session recording** - No recording capability
18. **File transfer** - Not implemented (stub in protocol)
19. **Backstage mode** - Not implemented (stub in protocol)
20. **Zstd decompression** - Placeholder only, needs fzstd or wasm-zstd
21. **Clipboard** - No clipboard sync
22. **Text input mode** - For Android/iOS remote input

**Development:**
23. **Development server** - No dev server configuration
24. **Hot reload** - No HMR setup
25. **ESLint config** - Referenced in scripts but no .eslintrc
26. **Testing** - No test framework or tests

### COMPONENT STRUCTURE SUMMARY

```
Dashboard (Components Library)
├── RemoteViewer
│   ├── useRemoteSession hook
│   ├── Canvas rendering
│   ├── Input handlers
│   └── Status bar
├── SessionControls
│   ├── Display selector
│   ├── Quality settings
│   ├── Special keys
│   └── Disconnect button
├── Types/Protocol
│   ├── Session definitions
│   ├── Display info
│   ├── VideoFrame format
│   ├── Input events (Mouse/Keyboard)
│   └── Connection status
└── Utilities
    ├── useRemoteSession (connection & input)
    ├── createMouseEvent (DOM → proto)
    ├── createKeyEvent (DOM → proto)
    ├── Protobuf encoding/decoding
    └── Zstd decompression (placeholder)
```

### TO BUILD A STANDALONE APP

You would need to add:

1. **A wrapper application** with Next.js, Vite, or CRA
2. **Authentication flow** to get JWT tokens and session IDs
3. **Session management UI** (list of active sessions, connect dialog)
4. **Application shell** with header, sidebar, and layout
5. **Error handling** with user-friendly messages
6. **Production build configuration**
7. **Actual Zstd decompression** implementation
8. **Optional enhancements**: recording, clipboard, file transfer, fullscreen, zoom

The current code is a well-structured **reusable component library** designed for integration into GuruRMM, not a standalone app. It's production-ready for embedding but needs application-level glue code to function independently.