Explore the GuruConnect dashboard and viewer implementation. I need to understand:

1. The current dashboard.html structure - what exists for viewing sessions
2. How the "Connect" button currently works
3. What JavaScript is already in place for WebSocket connections
4. The protobuf message structure for VideoFrame messages

Look in:
- server/static/dashboard.html
- Any viewer-related JavaScript
- proto/guruconnect.proto for VideoFrame structure

Provide details on what already exists and what needs to be built for a full viewer.


I'll explore the GuruConnect dashboard and viewer implementation to understand the current state and what needs to be built.


No files found


Now let me read the key files to understand the current implementation:


     1→syntax = "proto3";
     2→package guruconnect;
     3→
     4→// ============================================================================
     5→// Session Management
     6→// ============================================================================
     7→
     8→message SessionRequest {
     9→    string agent_id = 1;
    10→    string session_token = 2;
    11→    SessionType session_type = 3;
    12→    string client_version = 4;
    13→}
    14→
    15→message SessionResponse {
    16→    bool success = 1;
    17→    string session_id = 2;
    18→    string error = 3;
    19→    DisplayInfo display_info = 4;
    20→}
    21→
    22→enum SessionType {
    23→    SCREEN_CONTROL = 0;
    24→    VIEW_ONLY = 1;
    25→    BACKSTAGE = 2;
    26→    FILE_TRANSFER = 3;
    27→}
    28→
    29→// ============================================================================
    30→// Display Information
    31→// ============================================================================
    32→
    33→message DisplayInfo {
    34→    repeated Display displays = 1;
    35→    int32 primary_display = 2;
    36→}
    37→
    38→message Display {
    39→    int32 id = 1;
    40→    string name = 2;
    41→    int32 x = 3;
    42→    int32 y = 4;
    43→    int32 width = 5;
    44→    int32 height = 6;
    45→    bool is_primary = 7;
    46→}
    47→
    48→message SwitchDisplay {
    49→    int32 display_id = 1;
    50→}
    51→
    52→// ============================================================================
    53→// Video Frames
    54→// ============================================================================
    55→
    56→message VideoFrame {
    57→    int64 timestamp = 1;
    58→    int32 display_id = 2;
    59→    int32 sequence = 3;
    60→
    61→    oneof encoding {
    62→        RawFrame raw = 10;
    63→        EncodedFrame vp9 = 11;
    64→        EncodedFrame h264 = 12;
    65→        EncodedFrame h265 = 13;
    66→    }
    67→}
    68→
    69→message RawFrame {
    70→    int32 width = 1;
    71→    int32 height = 2;
    72→    bytes data = 3;             // Zstd compressed BGRA
    73→    bool compressed = 4;
    74→    repeated DirtyRect dirty_rects = 5;
    75→    bool is_keyframe = 6;       // Full frame vs incremental
    76→}
    77→
    78→message DirtyRect {
    79→    int32 x = 1;
    80→    int32 y = 2;
    81→    int32 width = 3;
    82→    int32 height = 4;
    83→}
    84→
    85→message EncodedFrame {
    86→    bytes data = 1;
    87→    bool keyframe = 2;
    88→    int64 pts = 3;
    89→    int64 dts = 4;
    90→}
    91→
    92→message VideoAck {
    93→    int32 sequence = 1;
    94→    int64 timestamp = 2;
    95→}
    96→
    97→// ============================================================================
    98→// Cursor
    99→// ============================================================================
   100→
   101→message CursorShape {
   102→    uint64 id = 1;
   103→    int32 hotspot_x = 2;
   104→    int32 hotspot_y = 3;
   105→    int32 width = 4;
   106→    int32 height = 5;
   107→    bytes data = 6;             // BGRA bitmap
   108→}
   109→
   110→message CursorPosition {
   111→    int32 x = 1;
   112→    int32 y = 2;
   113→    bool visible = 3;
   114→}
   115→
   116→// ============================================================================
   117→// Input Events
   118→// ============================================================================
   119→
   120→message MouseEvent {
   121→    int32 x = 1;
   122→    int32 y = 2;
   123→    MouseButtons buttons = 3;
   124→    int32 wheel_delta_x = 4;
   125→    int32 wheel_delta_y = 5;
   126→    MouseEventType event_type = 6;
   127→}
   128→
   129→enum MouseEventType {
   130→    MOUSE_MOVE = 0;
   131→    MOUSE_DOWN = 1;
   132→    MOUSE_UP = 2;
   133→    MOUSE_WHEEL = 3;
   134→}
   135→
   136→message MouseButtons {
   137→    bool left = 1;
   138→    bool right = 2;
   139→    bool middle = 3;
   140→    bool x1 = 4;
   141→    bool x2 = 5;
   142→}
   143→
   144→message KeyEvent {
   145→    bool down = 1;              // true = key down, false = key up
   146→    KeyEventType key_type = 2;
   147→    uint32 vk_code = 3;         // Virtual key code (Windows VK_*)
   148→    uint32 scan_code = 4;       // Hardware scan code
   149→    string unicode = 5;         // Unicode character (for text input)
   150→    Modifiers modifiers = 6;
   151→}
   152→
   153→enum KeyEventType {
   154→    KEY_VK = 0;                 // Virtual key code
   155→    KEY_SCAN = 1;               // Scan code
   156→    KEY_UNICODE = 2;            // Unicode character
   157→}
   158→
   159→message Modifiers {
   160→    bool ctrl = 1;
   161→    bool alt = 2;
   162→    bool shift = 3;
   163→    bool meta = 4;              // Windows key
   164→    bool caps_lock = 5;
   165→    bool num_lock = 6;
   166→}
   167→
   168→message SpecialKeyEvent {
   169→    SpecialKey key = 1;
   170→}
   171→
   172→enum SpecialKey {
   173→    CTRL_ALT_DEL = 0;
   174→    LOCK_SCREEN = 1;
   175→    PRINT_SCREEN = 2;
   176→}
   177→
   178→// ============================================================================
   179→// Clipboard
   180→// ============================================================================
   181→
   182→message ClipboardData {
   183→    ClipboardFormat format = 1;
   184→    bytes data = 2;
   185→    string mime_type = 3;
   186→}
   187→
   188→enum ClipboardFormat {
   189→    CLIPBOARD_TEXT = 0;
   190→    CLIPBOARD_HTML = 1;
   191→    CLIPBOARD_RTF = 2;
   192→    CLIPBOARD_IMAGE = 3;
   193→    CLIPBOARD_FILES = 4;
   194→}
   195→
   196→message ClipboardRequest {
   197→    // Request current clipboard content
   198→}
   199→
   200→// ============================================================================
   201→// Quality Control
   202→// ============================================================================
   203→
   204→message QualitySettings {
   205→    QualityPreset preset = 1;
   206→    int32 custom_fps = 2;       // 1-60
   207→    int32 custom_bitrate = 3;   // kbps
   208→    CodecPreference codec = 4;
   209→}
   210→
   211→enum QualityPreset {
   212→    QUALITY_AUTO = 0;
   213→    QUALITY_LOW = 1;            // Low bandwidth
   214→    QUALITY_BALANCED = 2;
   215→    QUALITY_HIGH = 3;           // Best quality
   216→}
   217→
   218→enum CodecPreference {
   219→    CODEC_AUTO = 0;
   220→    CODEC_RAW = 1;              // Raw + Zstd (LAN)
   221→    CODEC_VP9 = 2;
   222→    CODEC_H264 = 3;
   223→    CODEC_H265 = 4;
   224→}
   225→
   226→message LatencyReport {
   227→    int64 rtt_ms = 1;
   228→    int32 fps = 2;
   229→    int32 bitrate_kbps = 3;
   230→}
   231→
   232→// ============================================================================
   233→// Chat Messages
   234→// ============================================================================
   235→
   236→message ChatMessage {
   237→    string id = 1;              // Unique message ID
   238→    string sender = 2;          // "technician" or "client"
   239→    string content = 3;         // Message text
   240→    int64 timestamp = 4;        // Unix timestamp
   241→}
   242→
   243→// ============================================================================
   244→// Control Messages
   245→// ============================================================================
   246→
   247→message Heartbeat {
   248→    int64 timestamp = 1;
   249→}
   250→
   251→message HeartbeatAck {
   252→    int64 client_timestamp = 1;
   253→    int64 server_timestamp = 2;
   254→}
   255→
   256→message Disconnect {
   257→    string reason = 1;
   258→}
   259→
   260→// Server commands agent to start streaming video
   261→message StartStream {
   262→    string viewer_id = 1;           // ID of viewer requesting stream
   263→    int32 display_id = 2;           // Which display to stream (0 = primary)
   264→}
   265→
   266→// Server commands agent to stop streaming
   267→message StopStream {
   268→    string viewer_id = 1;           // Which viewer disconnected
   269→}
   270→
   271→// Agent reports its status periodically when idle
   272→message AgentStatus {
   273→    string hostname = 1;
   274→    string os_version = 2;
   275→    bool is_elevated = 3;
   276→    int64 uptime_secs = 4;
   277→    int32 display_count = 5;
   278→    bool is_streaming = 6;
   279→}
   280→
   281→// ============================================================================
   282→// Top-Level Message Wrapper
   283→// ============================================================================
   284→
   285→message Message {
   286→    oneof payload {
   287→        // Session
   288→        SessionRequest session_request = 1;
   289→        SessionResponse session_response = 2;
   290→
   291→        // Video
   292→        VideoFrame video_frame = 10;
   293→        VideoAck video_ack = 11;
   294→        SwitchDisplay switch_display = 12;
   295→
   296→        // Cursor
   297→        CursorShape cursor_shape = 15;
   298→        CursorPosition cursor_position = 16;
   299→
   300→        // Input
   301→        MouseEvent mouse_event = 20;
   302→        KeyEvent key_event = 21;
   303→        SpecialKeyEvent special_key = 22;
   304→
   305→        // Clipboard
   306→        ClipboardData clipboard_data = 30;
   307→        ClipboardRequest clipboard_request = 31;
   308→
   309→        // Quality
   310→        QualitySettings quality_settings = 40;
   311→        LatencyReport latency_report = 41;
   312→
   313→        // Control
   314→        Heartbeat heartbeat = 50;
   315→        HeartbeatAck heartbeat_ack = 51;
   316→        Disconnect disconnect = 52;
   317→        StartStream start_stream = 53;
   318→        StopStream stop_stream = 54;
   319→        AgentStatus agent_status = 55;
   320→
   321→        // Chat
   322→        ChatMessage chat_message = 60;
   323→    }
   324→}
   325→

<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>

syntax = "proto3";
package guruconnect;

// ============================================================================
// Session Management
// ============================================================================

message SessionRequest {
    string agent_id = 1;
    string session_token = 2;
    SessionType session_type = 3;
    string client_version = 4;
}

message SessionResponse {
    bool success = 1;
    string session_id = 2;
    string error = 3;
    DisplayInfo display_info = 4;
}

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

// ============================================================================
// Display Information
// ============================================================================

message DisplayInfo {
    repeated Display displays = 1;
    int32 primary_display = 2;
}

message Display {
    int32 id = 1;
    string name = 2;
    int32 x = 3;
    int32 y = 4;
    int32 width = 5;
    int32 height = 6;
    bool is_primary = 7;
}

message SwitchDisplay {
    int32 display_id = 1;
}

// ============================================================================
// Video Frames
// ============================================================================

message VideoFrame {
    int64 timestamp = 1;
    int32 display_id = 2;
    int32 sequence = 3;

    oneof encoding {
        RawFrame raw = 10;
        EncodedFrame vp9 = 11;
        EncodedFrame h264 = 12;
        EncodedFrame h265 = 13;
    }
}

message RawFrame {
    int32 width = 1;
    int32 height = 2;
    bytes data = 3;             // Zstd compressed BGRA
    bool compressed = 4;
    repeated DirtyRect dirty_rects = 5;
    bool is_keyframe = 6;       // Full frame vs incremental
}

message DirtyRect {
    int32 x = 1;
    int32 y = 2;
    int32 width = 3;
    int32 height = 4;
}

message EncodedFrame {
    bytes data = 1;
    bool keyframe = 2;
    int64 pts = 3;
    int64 dts = 4;
}

message VideoAck {
    int32 sequence = 1;
    int64 timestamp = 2;
}

// ============================================================================
// Cursor
// ============================================================================

message CursorShape {
    uint64 id = 1;
    int32 hotspot_x = 2;
    int32 hotspot_y = 3;
    int32 width = 4;
    int32 height = 5;
    bytes data = 6;             // BGRA bitmap
}

message CursorPosition {
    int32 x = 1;
    int32 y = 2;
    bool visible = 3;
}

// ============================================================================
// Input Events
// ============================================================================

message MouseEvent {
    int32 x = 1;
    int32 y = 2;
    MouseButtons buttons = 3;
    int32 wheel_delta_x = 4;
    int32 wheel_delta_y = 5;
    MouseEventType event_type = 6;
}

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

message MouseButtons {
    bool left = 1;
    bool right = 2;
    bool middle = 3;
    bool x1 = 4;
    bool x2 = 5;
}

message KeyEvent {
    bool down = 1;              // true = key down, false = key up
    KeyEventType key_type = 2;
    uint32 vk_code = 3;         // Virtual key code (Windows VK_*)
    uint32 scan_code = 4;       // Hardware scan code
    string unicode = 5;         // Unicode character (for text input)
    Modifiers modifiers = 6;
}

enum KeyEventType {
    KEY_VK = 0;                 // Virtual key code
    KEY_SCAN = 1;               // Scan code
    KEY_UNICODE = 2;            // Unicode character
}

message Modifiers {
    bool ctrl = 1;
    bool alt = 2;
    bool shift = 3;
    bool meta = 4;              // Windows key
    bool caps_lock = 5;
    bool num_lock = 6;
}

message SpecialKeyEvent {
    SpecialKey key = 1;
}

enum SpecialKey {
    CTRL_ALT_DEL = 0;
    LOCK_SCREEN = 1;
    PRINT_SCREEN = 2;
}

// ============================================================================
// Clipboard
// ============================================================================

message ClipboardData {
    ClipboardFormat format = 1;
    bytes data = 2;
    string mime_type = 3;
}

enum ClipboardFormat {
    CLIPBOARD_TEXT = 0;
    CLIPBOARD_HTML = 1;
    CLIPBOARD_RTF = 2;
    CLIPBOARD_IMAGE = 3;
    CLIPBOARD_FILES = 4;
}

message ClipboardRequest {
    // Request current clipboard content
}

// ============================================================================
// Quality Control
// ============================================================================

message QualitySettings {
    QualityPreset preset = 1;
    int32 custom_fps = 2;       // 1-60
    int32 custom_bitrate = 3;   // kbps
    CodecPreference codec = 4;
}

enum QualityPreset {
    QUALITY_AUTO = 0;
    QUALITY_LOW = 1;            // Low bandwidth
    QUALITY_BALANCED = 2;
    QUALITY_HIGH = 3;           // Best quality
}

enum CodecPreference {
    CODEC_AUTO = 0;
    CODEC_RAW = 1;              // Raw + Zstd (LAN)
    CODEC_VP9 = 2;
    CODEC_H264 = 3;
    CODEC_H265 = 4;
}

message LatencyReport {
    int64 rtt_ms = 1;
    int32 fps = 2;
    int32 bitrate_kbps = 3;
}

// ============================================================================
// Chat Messages
// ============================================================================

message ChatMessage {
    string id = 1;              // Unique message ID
    string sender = 2;          // "technician" or "client"
    string content = 3;         // Message text
    int64 timestamp = 4;        // Unix timestamp
}

// ============================================================================
// Control Messages
// ============================================================================

message Heartbeat {
    int64 timestamp = 1;
}

message HeartbeatAck {
    int64 client_timestamp = 1;
    int64 server_timestamp = 2;
}

message Disconnect {
    string reason = 1;
}

// Server commands agent to start streaming video
message StartStream {
    string viewer_id = 1;           // ID of viewer requesting stream
    int32 display_id = 2;           // Which display to stream (0 = primary)
}

// Server commands agent to stop streaming
message StopStream {
    string viewer_id = 1;           // Which viewer disconnected
}

// Agent reports its status periodically when idle
message AgentStatus {
    string hostname = 1;
    string os_version = 2;
    bool is_elevated = 3;
    int64 uptime_secs = 4;
    int32 display_count = 5;
    bool is_streaming = 6;
}

// ============================================================================
// Top-Level Message Wrapper
// ============================================================================

message Message {
    oneof payload {
        // Session
        SessionRequest session_request = 1;
        SessionResponse session_response = 2;

        // Video
        VideoFrame video_frame = 10;
        VideoAck video_ack = 11;
        SwitchDisplay switch_display = 12;

        // Cursor
        CursorShape cursor_shape = 15;
        CursorPosition cursor_position = 16;

        // Input
        MouseEvent mouse_event = 20;
        KeyEvent key_event = 21;
        SpecialKeyEvent special_key = 22;

        // Clipboard
        ClipboardData clipboard_data = 30;
        ClipboardRequest clipboard_request = 31;

        // Quality
        QualitySettings quality_settings = 40;
        LatencyReport latency_report = 41;

        // Control
        Heartbeat heartbeat = 50;
        HeartbeatAck heartbeat_ack = 51;
        Disconnect disconnect = 52;
        StartStream start_stream = 53;
        StopStream stop_stream = 54;
        AgentStatus agent_status = 55;

        // Chat
        ChatMessage chat_message = 60;
    }
}


1→<!DOCTYPE html>
     2→
     3→
     4→    <meta charset="UTF-8">
     5→    <meta name="viewport" content="width=device-width, initial-scale=1.0">
     6→    <title>GuruConnect - Dashboard</title>
     7→
   334→
   335→
   336→
   337→

   338→
GuruConnect

   339→

   340→

   341→            Loading...
   342→            <button class="logout-btn" id="logoutBtn">Sign Out</button>
   343→

   344→
   345→
   346→
   347→        <button class="tab active" data-tab="support">Support</button>
   348→        <button class="tab" data-tab="access">Access</button>
   349→        <button class="tab" data-tab="build">Build</button>
   350→        <button class="tab" data-tab="settings">Settings</button>
   351→
   352→
   353→
   354→
   355→

   356→

   357→

   358→

   359→
Active Support Sessions

   360→
Temporary sessions initiated by support codes

   361→

   362→                    <button class="btn btn-primary" id="generateCodeBtn">Generate Code</button>
   363→

   364→

   365→

   366→

   367→

   368→                                Code
   369→                                Status
   370→                                Created
   371→                                Technician
   372→                                Actions
   373→

   374→

   375→

   376→

   377→
   378→

   379→
No active sessions

   380→
Generate a code to start a support session

   381→

   382→
   383→

   384→

   385→

   386→

   387→

   388→

   389→
   390→
   391→

   392→

   393→

   394→

   395→
Status

   396→

   397→                            All Machines
   398→                            0
   399→

   400→

   401→                            Online
   402→                            0
   403→

   404→

   405→                            Offline
   406→                            0
   407→

   408→

   409→

   410→
By Company

   411→

   412→
No machines installed

   413→

   414→

   415→

   416→

   417→

   418→
No machines

   419→
Install the agent on a machine to see it here

   420→

   421→

   422→

   423→

   424→
Select a machine

   425→
Click a machine to view details

   426→

   427→

   428→

   429→

   430→
   431→
   432→

   433→

   434→

   435→

   436→
Installer Builder

   437→
Create customized agent installers for unattended access

   438→

   439→

   440→

   441→

   442→                        Name
   443→                        <input type="text" id="buildName" placeholder="Machine name (auto if blank)">
   444→

   445→

   446→                        Company
   447→                        <input type="text" id="buildCompany" placeholder="Client organization">
   448→

   449→

   450→                        Site
   451→                        <input type="text" id="buildSite" placeholder="Physical location">
   452→

   453→

   454→                        Department
   455→                        <input type="text" id="buildDepartment" placeholder="Business unit">
   456→

   457→

   458→                        Device Type
   459→                        <select id="buildDeviceType">
   460→                            <option value="workstation">Workstation</option>
   461→                            <option value="laptop">Laptop</option>
   462→                            <option value="server">Server</option>
   463→                        </select>
   464→

   465→

   466→                        Tag
   467→                        <input type="text" id="buildTag" placeholder="Custom label">
   468→

   469→

   470→

   471→                    <button class="btn btn-primary" disabled>Build EXE (64-bit)</button>
   472→                    <button class="btn btn-outline" disabled>Build EXE (32-bit)</button>
   473→                    <button class="btn btn-outline" disabled>Build MSI</button>
   474→

   475→

   476→                    Agent builds will be available once the agent is compiled.
   477→

   478→

   479→

   480→
   481→
   482→

   483→

   484→

   485→

   486→
Settings

   487→
Configure your GuruConnect preferences

   488→

   489→

   490→

   491→

   492→                        Theme
   493→                        <select disabled>
   494→                            <option>Dark (Default)</option>
   495→                            <option>Light</option>
   496→                            <option>System</option>
   497→                        </select>
   498→

   499→

   500→                        Notifications
   501→                        <select disabled>
   502→                            <option>All notifications</option>
   503→                            <option>Important only</option>
   504→                            <option>None</option>
   505→                        </select>
   506→

   507→

   508→

   509→                    Additional settings coming soon.
   510→

   511→

   512→

   513→
   514→
   515→
   516→

   517→

   518→

   519→
Chat with Client

   520→                <button class="modal-close" id="chatClose">×</button>
   521→

   522→

   523→
No messages yet. Start the conversation!

   524→

   525→

   526→                <input type="text" class="chat-input" id="chatInput" placeholder="Type a message..." />
   527→                <button class="chat-send" id="chatSend">Send</button>
   528→

   529→

   530→

   531→
   532→
  1044→
  1045→
  1046→

<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>

<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GuruConnect - Dashboard</title>

GuruConnect

            Loading...
            <button class="logout-btn" id="logoutBtn">Sign Out</button>

        <button class="tab active" data-tab="support">Support</button>
        <button class="tab" data-tab="access">Access</button>
        <button class="tab" data-tab="build">Build</button>
        <button class="tab" data-tab="settings">Settings</button>

Active Support Sessions

Temporary sessions initiated by support codes

                    <button class="btn btn-primary" id="generateCodeBtn">Generate Code</button>

                                Code
                                Status
                                Created
                                Technician
                                Actions

No active sessions

Generate a code to start a support session

Status

                            All Machines
                            0

                            Online
                            0

                            Offline
                            0

By Company

No machines installed

No machines

Install the agent on a machine to see it here

Select a machine

Click a machine to view details

Installer Builder

Create customized agent installers for unattended access

                        Name
                        <input type="text" id="buildName" placeholder="Machine name (auto if blank)">

                        Company
                        <input type="text" id="buildCompany" placeholder="Client organization">

                        Site
                        <input type="text" id="buildSite" placeholder="Physical location">

                        Department
                        <input type="text" id="buildDepartment" placeholder="Business unit">

                        Device Type
                        <select id="buildDeviceType">
                            <option value="workstation">Workstation</option>
                            <option value="laptop">Laptop</option>
                            <option value="server">Server</option>
                        </select>

                        Tag
                        <input type="text" id="buildTag" placeholder="Custom label">

                    <button class="btn btn-primary" disabled>Build EXE (64-bit)</button>
                    <button class="btn btn-outline" disabled>Build EXE (32-bit)</button>
                    <button class="btn btn-outline" disabled>Build MSI</button>

                    Agent builds will be available once the agent is compiled.

Settings

Configure your GuruConnect preferences

                        Theme
                        <select disabled>
                            <option>Dark (Default)</option>
                            <option>Light</option>
                            <option>System</option>
                        </select>

                        Notifications
                        <select disabled>
                            <option>All notifications</option>
                            <option>Important only</option>
                            <option>None</option>
                        </select>

                    Additional settings coming soon.

Chat with Client

                <button class="modal-close" id="chatClose">×</button>

No messages yet. Start the conversation!

                <input type="text" class="chat-input" id="chatInput" placeholder="Type a message..." />
                <button class="chat-send" id="chatSend">Send</button>


Now let me search for any other JavaScript files or viewer-related code:


Bash

find "C:\Users\MikeSwanson\Claude\guru-connect" -type f -name "*.js" -o -name "*.ts" -o -name "*.tsx" 2>/dev/null | head -20

Find JavaScript/TypeScript files in guru-connect project


Perfect! There are React components already. Let me read those:


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→ * 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→ * 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;
}


     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;
}


Now let me check the SessionControls component and see the overall structure:


find "C:\Users\MikeSwanson\Claude\guru-connect" -name "*.rs" -type f | grep -E "(server|agent)" | head -20

Find Rust source files in server/agent directories


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;


Let me check the server-side implementation for the WebSocket viewer endpoint:


find "C:\Users\MikeSwanson\Claude\guru-connect\server\src" -name "*.rs" -type f | head -20

List Rust files in server source directory


     1→//! GuruConnect Server - WebSocket Relay Server
     2→//!
     3→//! Handles connections from both agents and dashboard viewers,
     4→//! relaying video frames and input events between them.
     5→
     6→mod config;
     7→mod relay;
     8→mod session;
     9→mod auth;
    10→mod api;
    11→mod db;
    12→mod support_codes;
    13→
    14→pub mod proto {
    15→    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
    16→}
    17→
    18→use anyhow::Result;
    19→use axum::{
    20→    Router,
    21→    routing::{get, post},
    22→    extract::{Path, State, Json},
    23→    response::{Html, IntoResponse},
    24→    http::StatusCode,
    25→};
    26→use std::net::SocketAddr;
    27→use tower_http::cors::{Any, CorsLayer};
    28→use tower_http::trace::TraceLayer;
    29→use tower_http::services::ServeDir;
    30→use tracing::{info, Level};
    31→use tracing_subscriber::FmtSubscriber;
    32→use serde::Deserialize;
    33→
    34→use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};
    35→
    36→/// Application state
    37→#[derive(Clone)]
    38→pub struct AppState {
    39→    sessions: session::SessionManager,
    40→    support_codes: SupportCodeManager,
    41→}
    42→
    43→#[tokio::main]
    44→async fn main() -> Result<()> {
    45→    // Initialize logging
    46→    let _subscriber = FmtSubscriber::builder()
    47→        .with_max_level(Level::INFO)
    48→        .with_target(true)
    49→        .init();
    50→
    51→    info!("GuruConnect Server v{}", env!("CARGO_PKG_VERSION"));
    52→
    53→    // Load configuration
    54→    let config = config::Config::load()?;
    55→
    56→    // Use port 3002 for GuruConnect
    57→    let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
    58→    info!("Loaded configuration, listening on {}", listen_addr);
    59→
    60→    // Create application state
    61→    let state = AppState {
    62→        sessions: session::SessionManager::new(),
    63→        support_codes: SupportCodeManager::new(),
    64→    };
    65→
    66→    // Build router
    67→    let app = Router::new()
    68→        // Health check
    69→        .route("/health", get(health))
    70→
    71→        // Portal API - Support codes
    72→        .route("/api/codes", post(create_code))
    73→        .route("/api/codes", get(list_codes))
    74→        .route("/api/codes/:code/validate", get(validate_code))
    75→        .route("/api/codes/:code/cancel", post(cancel_code))
    76→
    77→        // WebSocket endpoints
    78→        .route("/ws/agent", get(relay::agent_ws_handler))
    79→        .route("/ws/viewer", get(relay::viewer_ws_handler))
    80→
    81→        // REST API - Sessions
    82→        .route("/api/sessions", get(list_sessions))
    83→        .route("/api/sessions/:id", get(get_session))
    84→        .route("/api/sessions/:id", axum::routing::delete(disconnect_session))
    85→
    86→        // HTML page routes (clean URLs)
    87→        .route("/login", get(serve_login))
    88→        .route("/dashboard", get(serve_dashboard))
    89→
    90→        // State
    91→        .with_state(state)
    92→
    93→        // Serve static files for portal (fallback)
    94→        .fallback_service(ServeDir::new("static").append_index_html_on_directories(true))
    95→
    96→        // Middleware
    97→        .layer(TraceLayer::new_for_http())
    98→        .layer(
    99→            CorsLayer::new()
   100→                .allow_origin(Any)
   101→                .allow_methods(Any)
   102→                .allow_headers(Any),
   103→        );
   104→
   105→    // Start server
   106→    let addr: SocketAddr = listen_addr.parse()?;
   107→    let listener = tokio::net::TcpListener::bind(addr).await?;
   108→
   109→    info!("Server listening on {}", addr);
   110→
   111→    axum::serve(listener, app).await?;
   112→
   113→    Ok(())
   114→}
   115→
   116→async fn health() -> &'static str {
   117→    "OK"
   118→}
   119→
   120→// Support code API handlers
   121→
   122→async fn create_code(
   123→    State(state): State<AppState>,
   124→    Json(request): Json<CreateCodeRequest>,
   125→) -> Json<SupportCode> {
   126→    let code = state.support_codes.create_code(request).await;
   127→    info!("Created support code: {}", code.code);
   128→    Json(code)
   129→}
   130→
   131→async fn list_codes(
   132→    State(state): State<AppState>,
   133→) -> Json<Vec<SupportCode>> {
   134→    Json(state.support_codes.list_active_codes().await)
   135→}
   136→
   137→#[derive(Deserialize)]
   138→struct ValidateParams {
   139→    code: String,
   140→}
   141→
   142→async fn validate_code(
   143→    State(state): State<AppState>,
   144→    Path(code): Path<String>,
   145→) -> Json<CodeValidation> {
   146→    Json(state.support_codes.validate_code(&code).await)
   147→}
   148→
   149→async fn cancel_code(
   150→    State(state): State<AppState>,
   151→    Path(code): Path<String>,
   152→) -> impl IntoResponse {
   153→    if state.support_codes.cancel_code(&code).await {
   154→        (StatusCode::OK, "Code cancelled")
   155→    } else {
   156→        (StatusCode::BAD_REQUEST, "Cannot cancel code")
   157→    }
   158→}
   159→
   160→// Session API handlers (updated to use AppState)
   161→
   162→async fn list_sessions(
   163→    State(state): State<AppState>,
   164→) -> Json<Vec<api::SessionInfo>> {
   165→    let sessions = state.sessions.list_sessions().await;
   166→    Json(sessions.into_iter().map(api::SessionInfo::from).collect())
   167→}
   168→
   169→async fn get_session(
   170→    State(state): State<AppState>,
   171→    Path(id): Path<String>,
   172→) -> Result<Json<api::SessionInfo>, (StatusCode, &'static str)> {
   173→    let session_id = uuid::Uuid::parse_str(&id)
   174→        .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID"))?;
   175→
   176→    let session = state.sessions.get_session(session_id).await
   177→        .ok_or((StatusCode::NOT_FOUND, "Session not found"))?;
   178→
   179→    Ok(Json(api::SessionInfo::from(session)))
   180→}
   181→
   182→async fn disconnect_session(
   183→    State(state): State<AppState>,
   184→    Path(id): Path<String>,
   185→) -> impl IntoResponse {
   186→    let session_id = match uuid::Uuid::parse_str(&id) {
   187→        Ok(id) => id,
   188→        Err(_) => return (StatusCode::BAD_REQUEST, "Invalid session ID"),
   189→    };
   190→
   191→    if state.sessions.disconnect_session(session_id, "Disconnected by administrator").await {
   192→        info!("Session {} disconnected by admin", session_id);
   193→        (StatusCode::OK, "Session disconnected")
   194→    } else {
   195→        (StatusCode::NOT_FOUND, "Session not found")
   196→    }
   197→}
   198→
   199→// Static page handlers
   200→async fn serve_login() -> impl IntoResponse {
   201→    match tokio::fs::read_to_string("static/login.html").await {
   202→        Ok(content) => Html(content).into_response(),
   203→        Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
   204→    }
   205→}
   206→
   207→async fn serve_dashboard() -> impl IntoResponse {
   208→    match tokio::fs::read_to_string("static/dashboard.html").await {
   209→        Ok(content) => Html(content).into_response(),
   210→        Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
   211→    }
   212→}
   213→

<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 Server - WebSocket Relay Server
//!
//! Handles connections from both agents and dashboard viewers,
//! relaying video frames and input events between them.

mod config;
mod relay;
mod session;
mod auth;
mod api;
mod db;
mod support_codes;

pub mod proto {
    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
}

use anyhow::Result;
use axum::{
    Router,
    routing::{get, post},
    extract::{Path, State, Json},
    response::{Html, IntoResponse},
    http::StatusCode,
};
use std::net::SocketAddr;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tower_http::services::ServeDir;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
use serde::Deserialize;

use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};

/// Application state
#[derive(Clone)]
pub struct AppState {
    sessions: session::SessionManager,
    support_codes: SupportCodeManager,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    let _subscriber = FmtSubscriber::builder()
        .with_max_level(Level::INFO)
        .with_target(true)
        .init();

    info!("GuruConnect Server v{}", env!("CARGO_PKG_VERSION"));

    // Load configuration
    let config = config::Config::load()?;

    // Use port 3002 for GuruConnect
    let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
    info!("Loaded configuration, listening on {}", listen_addr);

    // Create application state
    let state = AppState {
        sessions: session::SessionManager::new(),
        support_codes: SupportCodeManager::new(),
    };

    // Build router
    let app = Router::new()
        // Health check
        .route("/health", get(health))

        // Portal API - Support codes
        .route("/api/codes", post(create_code))
        .route("/api/codes", get(list_codes))
        .route("/api/codes/:code/validate", get(validate_code))
        .route("/api/codes/:code/cancel", post(cancel_code))

        // WebSocket endpoints
        .route("/ws/agent", get(relay::agent_ws_handler))
        .route("/ws/viewer", get(relay::viewer_ws_handler))

        // REST API - Sessions
        .route("/api/sessions", get(list_sessions))
        .route("/api/sessions/:id", get(get_session))
        .route("/api/sessions/:id", axum::routing::delete(disconnect_session))

        // HTML page routes (clean URLs)
        .route("/login", get(serve_login))
        .route("/dashboard", get(serve_dashboard))

        // State
        .with_state(state)

        // Serve static files for portal (fallback)
        .fallback_service(ServeDir::new("static").append_index_html_on_directories(true))

        // Middleware
        .layer(TraceLayer::new_for_http())
        .layer(
            CorsLayer::new()
                .allow_origin(Any)
                .allow_methods(Any)
                .allow_headers(Any),
        );

    // Start server
    let addr: SocketAddr = listen_addr.parse()?;
    let listener = tokio::net::TcpListener::bind(addr).await?;

    info!("Server listening on {}", addr);

    axum::serve(listener, app).await?;

    Ok(())
}

async fn health() -> &'static str {
    "OK"
}

// Support code API handlers

async fn create_code(
    State(state): State<AppState>,
    Json(request): Json<CreateCodeRequest>,
) -> Json<SupportCode> {
    let code = state.support_codes.create_code(request).await;
    info!("Created support code: {}", code.code);
    Json(code)
}

async fn list_codes(
    State(state): State<AppState>,
) -> Json<Vec<SupportCode>> {
    Json(state.support_codes.list_active_codes().await)
}

#[derive(Deserialize)]
struct ValidateParams {
    code: String,
}

async fn validate_code(
    State(state): State<AppState>,
    Path(code): Path<String>,
) -> Json<CodeValidation> {
    Json(state.support_codes.validate_code(&code).await)
}

async fn cancel_code(
    State(state): State<AppState>,
    Path(code): Path<String>,
) -> impl IntoResponse {
    if state.support_codes.cancel_code(&code).await {
        (StatusCode::OK, "Code cancelled")
    } else {
        (StatusCode::BAD_REQUEST, "Cannot cancel code")
    }
}

// Session API handlers (updated to use AppState)

async fn list_sessions(
    State(state): State<AppState>,
) -> Json<Vec<api::SessionInfo>> {
    let sessions = state.sessions.list_sessions().await;
    Json(sessions.into_iter().map(api::SessionInfo::from).collect())
}

async fn get_session(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Json<api::SessionInfo>, (StatusCode, &'static str)> {
    let session_id = uuid::Uuid::parse_str(&id)
        .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID"))?;

    let session = state.sessions.get_session(session_id).await
        .ok_or((StatusCode::NOT_FOUND, "Session not found"))?;

    Ok(Json(api::SessionInfo::from(session)))
}

async fn disconnect_session(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> impl IntoResponse {
    let session_id = match uuid::Uuid::parse_str(&id) {
        Ok(id) => id,
        Err(_) => return (StatusCode::BAD_REQUEST, "Invalid session ID"),
    };

    if state.sessions.disconnect_session(session_id, "Disconnected by administrator").await {
        info!("Session {} disconnected by admin", session_id);
        (StatusCode::OK, "Session disconnected")
    } else {
        (StatusCode::NOT_FOUND, "Session not found")
    }
}

// Static page handlers
async fn serve_login() -> impl IntoResponse {
    match tokio::fs::read_to_string("static/login.html").await {
        Ok(content) => Html(content).into_response(),
        Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
    }
}

async fn serve_dashboard() -> impl IntoResponse {
    match tokio::fs::read_to_string("static/dashboard.html").await {
        Ok(content) => Html(content).into_response(),
        Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
    }
}