     1→//! WebSocket relay handlers
     2→//!
     3→//! Handles WebSocket connections from agents and viewers,
     4→//! relaying video frames and input events between them.
     5→
     6→use axum::{
     7→    extract::{
     8→        ws::{Message, WebSocket, WebSocketUpgrade},
     9→        Query, State,
    10→    },
    11→    response::IntoResponse,
    12→};
    13→use futures_util::{SinkExt, StreamExt};
    14→use prost::Message as ProstMessage;
    15→use serde::Deserialize;
    16→use tracing::{error, info, warn};
    17→use uuid::Uuid;
    18→
    19→use crate::proto;
    20→use crate::session::SessionManager;
    21→use crate::AppState;
    22→
    23→#[derive(Debug, Deserialize)]
    24→pub struct AgentParams {
    25→    agent_id: String,
    26→    #[serde(default)]
    27→    agent_name: Option<String>,
    28→    #[serde(default)]
    29→    support_code: Option<String>,
    30→    #[serde(default)]
    31→    hostname: Option<String>,
    32→}
    33→
    34→#[derive(Debug, Deserialize)]
    35→pub struct ViewerParams {
    36→    session_id: String,
    37→}
    38→
    39→/// WebSocket handler for agent connections
    40→pub async fn agent_ws_handler(
    41→    ws: WebSocketUpgrade,
    42→    State(state): State<AppState>,
    43→    Query(params): Query<AgentParams>,
    44→) -> impl IntoResponse {
    45→    let agent_id = params.agent_id;
    46→    let agent_name = params.hostname.or(params.agent_name).unwrap_or_else(|| agent_id.clone());
    47→    let support_code = params.support_code;
    48→    let sessions = state.sessions.clone();
    49→    let support_codes = state.support_codes.clone();
    50→
    51→    ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, agent_id, agent_name, support_code))
    52→}
    53→
    54→/// WebSocket handler for viewer connections
    55→pub async fn viewer_ws_handler(
    56→    ws: WebSocketUpgrade,
    57→    State(state): State<AppState>,
    58→    Query(params): Query<ViewerParams>,
    59→) -> impl IntoResponse {
    60→    let session_id = params.session_id;
    61→    let sessions = state.sessions.clone();
    62→
    63→    ws.on_upgrade(move |socket| handle_viewer_connection(socket, sessions, session_id))
    64→}
    65→
    66→/// Handle an agent WebSocket connection
    67→async fn handle_agent_connection(
    68→    socket: WebSocket,
    69→    sessions: SessionManager,
    70→    support_codes: crate::support_codes::SupportCodeManager,
    71→    agent_id: String,
    72→    agent_name: String,
    73→    support_code: Option<String>,
    74→) {
    75→    info!("Agent connected: {} ({})", agent_name, agent_id);
    76→
    77→    let (mut ws_sender, mut ws_receiver) = socket.split();
    78→
    79→    // If a support code was provided, check if it's valid
    80→    if let Some(ref code) = support_code {
    81→        // Check if the code is cancelled or invalid
    82→        if support_codes.is_cancelled(code).await {
    83→            warn!("Agent tried to connect with cancelled code: {}", code);
    84→            // Send disconnect message to agent
    85→            let disconnect_msg = proto::Message {
    86→                payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
    87→                    reason: "Support session was cancelled by technician".to_string(),
    88→                })),
    89→            };
    90→            let mut buf = Vec::new();
    91→            if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() {
    92→                let _ = ws_sender.send(Message::Binary(buf.into())).await;
    93→            }
    94→            let _ = ws_sender.close().await;
    95→            return;
    96→        }
    97→    }
    98→
    99→    // Register the agent and get channels
   100→    let (session_id, frame_tx, mut input_rx) = sessions.register_agent(agent_id.clone(), agent_name.clone()).await;
   101→
   102→    info!("Session created: {} (agent in idle mode)", session_id);
   103→
   104→    // If a support code was provided, mark it as connected
   105→    if let Some(ref code) = support_code {
   106→        info!("Linking support code {} to session {}", code, session_id);
   107→        support_codes.mark_connected(code, Some(agent_name.clone()), Some(agent_id.clone())).await;
   108→        support_codes.link_session(code, session_id).await;
   109→    }
   110→
   111→    // Use Arc<Mutex> for sender so we can use it from multiple places
   112→    let ws_sender = std::sync::Arc::new(tokio::sync::Mutex::new(ws_sender));
   113→    let ws_sender_input = ws_sender.clone();
   114→    let ws_sender_cancel = ws_sender.clone();
   115→
   116→    // Task to forward input events from viewers to agent
   117→    let input_forward = tokio::spawn(async move {
   118→        while let Some(input_data) = input_rx.recv().await {
   119→            let mut sender = ws_sender_input.lock().await;
   120→            if sender.send(Message::Binary(input_data.into())).await.is_err() {
   121→                break;
   122→            }
   123→        }
   124→    });
   125→
   126→    let sessions_cleanup = sessions.clone();
   127→    let sessions_status = sessions.clone();
   128→    let support_codes_cleanup = support_codes.clone();
   129→    let support_code_cleanup = support_code.clone();
   130→    let support_code_check = support_code.clone();
   131→    let support_codes_check = support_codes.clone();
   132→
   133→    // Task to check for cancellation every 2 seconds
   134→    let cancel_check = tokio::spawn(async move {
   135→        let mut interval = tokio::time::interval(std::time::Duration::from_secs(2));
   136→        loop {
   137→            interval.tick().await;
   138→            if let Some(ref code) = support_code_check {
   139→                if support_codes_check.is_cancelled(code).await {
   140→                    info!("Support code {} was cancelled, disconnecting agent", code);
   141→                    // Send disconnect message
   142→                    let disconnect_msg = proto::Message {
   143→                        payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
   144→                            reason: "Support session was cancelled by technician".to_string(),
   145→                        })),
   146→                    };
   147→                    let mut buf = Vec::new();
   148→                    if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() {
   149→                        let mut sender = ws_sender_cancel.lock().await;
   150→                        let _ = sender.send(Message::Binary(buf.into())).await;
   151→                        let _ = sender.close().await;
   152→                    }
   153→                    break;
   154→                }
   155→            }
   156→        }
   157→    });
   158→
   159→    // Main loop: receive messages from agent
   160→    while let Some(msg) = ws_receiver.next().await {
   161→        match msg {
   162→            Ok(Message::Binary(data)) => {
   163→                // Try to decode as protobuf message
   164→                match proto::Message::decode(data.as_ref()) {
   165→                    Ok(proto_msg) => {
   166→                        match &proto_msg.payload {
   167→                            Some(proto::message::Payload::VideoFrame(_)) => {
   168→                                // Broadcast frame to all viewers (only sent when streaming)
   169→                                let _ = frame_tx.send(data.to_vec());
   170→                            }
   171→                            Some(proto::message::Payload::ChatMessage(chat)) => {
   172→                                // Broadcast chat message to all viewers
   173→                                info!("Chat from client: {}", chat.content);
   174→                                let _ = frame_tx.send(data.to_vec());
   175→                            }
   176→                            Some(proto::message::Payload::AgentStatus(status)) => {
   177→                                // Update session with agent status
   178→                                sessions_status.update_agent_status(
   179→                                    session_id,
   180→                                    Some(status.os_version.clone()),
   181→                                    status.is_elevated,
   182→                                    status.uptime_secs,
   183→                                    status.display_count,
   184→                                    status.is_streaming,
   185→                                ).await;
   186→                                info!("Agent status update: {} - streaming={}, uptime={}s",
   187→                                    status.hostname, status.is_streaming, status.uptime_secs);
   188→                            }
   189→                            Some(proto::message::Payload::Heartbeat(_)) => {
   190→                                // Update heartbeat timestamp
   191→                                sessions_status.update_heartbeat(session_id).await;
   192→                            }
   193→                            Some(proto::message::Payload::HeartbeatAck(_)) => {
   194→                                // Agent acknowledged our heartbeat
   195→                                sessions_status.update_heartbeat(session_id).await;
   196→                            }
   197→                            _ => {}
   198→                        }
   199→                    }
   200→                    Err(e) => {
   201→                        warn!("Failed to decode agent message: {}", e);
   202→                    }
   203→                }
   204→            }
   205→            Ok(Message::Close(_)) => {
   206→                info!("Agent disconnected: {}", agent_id);
   207→                break;
   208→            }
   209→            Ok(Message::Ping(data)) => {
   210→                // Pong is handled automatically by axum
   211→                let _ = data;
   212→            }
   213→            Ok(_) => {}
   214→            Err(e) => {
   215→                error!("WebSocket error from agent {}: {}", agent_id, e);
   216→                break;
   217→            }
   218→        }
   219→    }
   220→
   221→    // Cleanup
   222→    input_forward.abort();
   223→    cancel_check.abort();
   224→    sessions_cleanup.remove_session(session_id).await;
   225→
   226→    // Mark support code as completed if one was used (unless cancelled)
   227→    if let Some(ref code) = support_code_cleanup {
   228→        if !support_codes_cleanup.is_cancelled(code).await {
   229→            support_codes_cleanup.mark_completed(code).await;
   230→            info!("Support code {} marked as completed", code);
   231→        }
   232→    }
   233→
   234→    info!("Session {} ended", session_id);
   235→}
   236→
   237→/// Handle a viewer WebSocket connection
   238→async fn handle_viewer_connection(
   239→    socket: WebSocket,
   240→    sessions: SessionManager,
   241→    session_id_str: String,
   242→) {
   243→    // Parse session ID
   244→    let session_id = match uuid::Uuid::parse_str(&session_id_str) {
   245→        Ok(id) => id,
   246→        Err(_) => {
   247→            warn!("Invalid session ID: {}", session_id_str);
   248→            return;
   249→        }
   250→    };
   251→
   252→    // Generate unique viewer ID
   253→    let viewer_id = Uuid::new_v4().to_string();
   254→
   255→    // Join the session (this sends StartStream to agent if first viewer)
   256→    let (mut frame_rx, input_tx) = match sessions.join_session(session_id, viewer_id.clone()).await {
   257→        Some(channels) => channels,
   258→        None => {
   259→            warn!("Session not found: {}", session_id);
   260→            return;
   261→        }
   262→    };
   263→
   264→    info!("Viewer {} joined session: {}", viewer_id, session_id);
   265→
   266→    let (mut ws_sender, mut ws_receiver) = socket.split();
   267→
   268→    // Task to forward frames from agent to this viewer
   269→    let frame_forward = tokio::spawn(async move {
   270→        while let Ok(frame_data) = frame_rx.recv().await {
   271→            if ws_sender.send(Message::Binary(frame_data.into())).await.is_err() {
   272→                break;
   273→            }
   274→        }
   275→    });
   276→
   277→    let sessions_cleanup = sessions.clone();
   278→    let viewer_id_cleanup = viewer_id.clone();
   279→
   280→    // Main loop: receive input from viewer and forward to agent
   281→    while let Some(msg) = ws_receiver.next().await {
   282→        match msg {
   283→            Ok(Message::Binary(data)) => {
   284→                // Try to decode as protobuf message
   285→                match proto::Message::decode(data.as_ref()) {
   286→                    Ok(proto_msg) => {
   287→                        match &proto_msg.payload {
   288→                            Some(proto::message::Payload::MouseEvent(_)) |
   289→                            Some(proto::message::Payload::KeyEvent(_)) |
   290→                            Some(proto::message::Payload::SpecialKey(_)) => {
   291→                                // Forward input to agent
   292→                                let _ = input_tx.send(data.to_vec()).await;
   293→                            }
   294→                            Some(proto::message::Payload::ChatMessage(chat)) => {
   295→                                // Forward chat message to agent
   296→                                info!("Chat from technician: {}", chat.content);
   297→                                let _ = input_tx.send(data.to_vec()).await;
   298→                            }
   299→                            _ => {}
   300→                        }
   301→                    }
   302→                    Err(e) => {
   303→                        warn!("Failed to decode viewer message: {}", e);
   304→                    }
   305→                }
   306→            }
   307→            Ok(Message::Close(_)) => {
   308→                info!("Viewer {} disconnected from session: {}", viewer_id, session_id);
   309→                break;
   310→            }
   311→            Ok(_) => {}
   312→            Err(e) => {
   313→                error!("WebSocket error from viewer {}: {}", viewer_id, e);
   314→                break;
   315→            }
   316→        }
   317→    }
   318→
   319→    // Cleanup (this sends StopStream to agent if last viewer)
   320→    frame_forward.abort();
   321→    sessions_cleanup.leave_session(session_id, &viewer_id_cleanup).await;
   322→    info!("Viewer {} left session: {}", viewer_id_cleanup, session_id);
   323→}
   324→

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

//! WebSocket relay handlers
//!
//! Handles WebSocket connections from agents and viewers,
//! relaying video frames and input events between them.

use axum::{
    extract::{
        ws::{Message, WebSocket, WebSocketUpgrade},
        Query, State,
    },
    response::IntoResponse,
};
use futures_util::{SinkExt, StreamExt};
use prost::Message as ProstMessage;
use serde::Deserialize;
use tracing::{error, info, warn};
use uuid::Uuid;

use crate::proto;
use crate::session::SessionManager;
use crate::AppState;

#[derive(Debug, Deserialize)]
pub struct AgentParams {
    agent_id: String,
    #[serde(default)]
    agent_name: Option<String>,
    #[serde(default)]
    support_code: Option<String>,
    #[serde(default)]
    hostname: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct ViewerParams {
    session_id: String,
}

/// WebSocket handler for agent connections
pub async fn agent_ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<AppState>,
    Query(params): Query<AgentParams>,
) -> impl IntoResponse {
    let agent_id = params.agent_id;
    let agent_name = params.hostname.or(params.agent_name).unwrap_or_else(|| agent_id.clone());
    let support_code = params.support_code;
    let sessions = state.sessions.clone();
    let support_codes = state.support_codes.clone();

    ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, agent_id, agent_name, support_code))
}

/// WebSocket handler for viewer connections
pub async fn viewer_ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<AppState>,
    Query(params): Query<ViewerParams>,
) -> impl IntoResponse {
    let session_id = params.session_id;
    let sessions = state.sessions.clone();

    ws.on_upgrade(move |socket| handle_viewer_connection(socket, sessions, session_id))
}

/// Handle an agent WebSocket connection
async fn handle_agent_connection(
    socket: WebSocket,
    sessions: SessionManager,
    support_codes: crate::support_codes::SupportCodeManager,
    agent_id: String,
    agent_name: String,
    support_code: Option<String>,
) {
    info!("Agent connected: {} ({})", agent_name, agent_id);

    let (mut ws_sender, mut ws_receiver) = socket.split();

    // If a support code was provided, check if it's valid
    if let Some(ref code) = support_code {
        // Check if the code is cancelled or invalid
        if support_codes.is_cancelled(code).await {
            warn!("Agent tried to connect with cancelled code: {}", code);
            // Send disconnect message to agent
            let disconnect_msg = proto::Message {
                payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
                    reason: "Support session was cancelled by technician".to_string(),
                })),
            };
            let mut buf = Vec::new();
            if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() {
                let _ = ws_sender.send(Message::Binary(buf.into())).await;
            }
            let _ = ws_sender.close().await;
            return;
        }
    }

    // Register the agent and get channels
    let (session_id, frame_tx, mut input_rx) = sessions.register_agent(agent_id.clone(), agent_name.clone()).await;

    info!("Session created: {} (agent in idle mode)", session_id);

    // If a support code was provided, mark it as connected
    if let Some(ref code) = support_code {
        info!("Linking support code {} to session {}", code, session_id);
        support_codes.mark_connected(code, Some(agent_name.clone()), Some(agent_id.clone())).await;
        support_codes.link_session(code, session_id).await;
    }

    // Use Arc<Mutex> for sender so we can use it from multiple places
    let ws_sender = std::sync::Arc::new(tokio::sync::Mutex::new(ws_sender));
    let ws_sender_input = ws_sender.clone();
    let ws_sender_cancel = ws_sender.clone();

    // Task to forward input events from viewers to agent
    let input_forward = tokio::spawn(async move {
        while let Some(input_data) = input_rx.recv().await {
            let mut sender = ws_sender_input.lock().await;
            if sender.send(Message::Binary(input_data.into())).await.is_err() {
                break;
            }
        }
    });

    let sessions_cleanup = sessions.clone();
    let sessions_status = sessions.clone();
    let support_codes_cleanup = support_codes.clone();
    let support_code_cleanup = support_code.clone();
    let support_code_check = support_code.clone();
    let support_codes_check = support_codes.clone();

    // Task to check for cancellation every 2 seconds
    let cancel_check = tokio::spawn(async move {
        let mut interval = tokio::time::interval(std::time::Duration::from_secs(2));
        loop {
            interval.tick().await;
            if let Some(ref code) = support_code_check {
                if support_codes_check.is_cancelled(code).await {
                    info!("Support code {} was cancelled, disconnecting agent", code);
                    // Send disconnect message
                    let disconnect_msg = proto::Message {
                        payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
                            reason: "Support session was cancelled by technician".to_string(),
                        })),
                    };
                    let mut buf = Vec::new();
                    if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() {
                        let mut sender = ws_sender_cancel.lock().await;
                        let _ = sender.send(Message::Binary(buf.into())).await;
                        let _ = sender.close().await;
                    }
                    break;
                }
            }
        }
    });

    // Main loop: receive messages from agent
    while let Some(msg) = ws_receiver.next().await {
        match msg {
            Ok(Message::Binary(data)) => {
                // Try to decode as protobuf message
                match proto::Message::decode(data.as_ref()) {
                    Ok(proto_msg) => {
                        match &proto_msg.payload {
                            Some(proto::message::Payload::VideoFrame(_)) => {
                                // Broadcast frame to all viewers (only sent when streaming)
                                let _ = frame_tx.send(data.to_vec());
                            }
                            Some(proto::message::Payload::ChatMessage(chat)) => {
                                // Broadcast chat message to all viewers
                                info!("Chat from client: {}", chat.content);
                                let _ = frame_tx.send(data.to_vec());
                            }
                            Some(proto::message::Payload::AgentStatus(status)) => {
                                // Update session with agent status
                                sessions_status.update_agent_status(
                                    session_id,
                                    Some(status.os_version.clone()),
                                    status.is_elevated,
                                    status.uptime_secs,
                                    status.display_count,
                                    status.is_streaming,
                                ).await;
                                info!("Agent status update: {} - streaming={}, uptime={}s",
                                    status.hostname, status.is_streaming, status.uptime_secs);
                            }
                            Some(proto::message::Payload::Heartbeat(_)) => {
                                // Update heartbeat timestamp
                                sessions_status.update_heartbeat(session_id).await;
                            }
                            Some(proto::message::Payload::HeartbeatAck(_)) => {
                                // Agent acknowledged our heartbeat
                                sessions_status.update_heartbeat(session_id).await;
                            }
                            _ => {}
                        }
                    }
                    Err(e) => {
                        warn!("Failed to decode agent message: {}", e);
                    }
                }
            }
            Ok(Message::Close(_)) => {
                info!("Agent disconnected: {}", agent_id);
                break;
            }
            Ok(Message::Ping(data)) => {
                // Pong is handled automatically by axum
                let _ = data;
            }
            Ok(_) => {}
            Err(e) => {
                error!("WebSocket error from agent {}: {}", agent_id, e);
                break;
            }
        }
    }

    // Cleanup
    input_forward.abort();
    cancel_check.abort();
    sessions_cleanup.remove_session(session_id).await;

    // Mark support code as completed if one was used (unless cancelled)
    if let Some(ref code) = support_code_cleanup {
        if !support_codes_cleanup.is_cancelled(code).await {
            support_codes_cleanup.mark_completed(code).await;
            info!("Support code {} marked as completed", code);
        }
    }

    info!("Session {} ended", session_id);
}

/// Handle a viewer WebSocket connection
async fn handle_viewer_connection(
    socket: WebSocket,
    sessions: SessionManager,
    session_id_str: String,
) {
    // Parse session ID
    let session_id = match uuid::Uuid::parse_str(&session_id_str) {
        Ok(id) => id,
        Err(_) => {
            warn!("Invalid session ID: {}", session_id_str);
            return;
        }
    };

    // Generate unique viewer ID
    let viewer_id = Uuid::new_v4().to_string();

    // Join the session (this sends StartStream to agent if first viewer)
    let (mut frame_rx, input_tx) = match sessions.join_session(session_id, viewer_id.clone()).await {
        Some(channels) => channels,
        None => {
            warn!("Session not found: {}", session_id);
            return;
        }
    };

    info!("Viewer {} joined session: {}", viewer_id, session_id);

    let (mut ws_sender, mut ws_receiver) = socket.split();

    // Task to forward frames from agent to this viewer
    let frame_forward = tokio::spawn(async move {
        while let Ok(frame_data) = frame_rx.recv().await {
            if ws_sender.send(Message::Binary(frame_data.into())).await.is_err() {
                break;
            }
        }
    });

    let sessions_cleanup = sessions.clone();
    let viewer_id_cleanup = viewer_id.clone();

    // Main loop: receive input from viewer and forward to agent
    while let Some(msg) = ws_receiver.next().await {
        match msg {
            Ok(Message::Binary(data)) => {
                // Try to decode as protobuf message
                match proto::Message::decode(data.as_ref()) {
                    Ok(proto_msg) => {
                        match &proto_msg.payload {
                            Some(proto::message::Payload::MouseEvent(_)) |
                            Some(proto::message::Payload::KeyEvent(_)) |
                            Some(proto::message::Payload::SpecialKey(_)) => {
                                // Forward input to agent
                                let _ = input_tx.send(data.to_vec()).await;
                            }
                            Some(proto::message::Payload::ChatMessage(chat)) => {
                                // Forward chat message to agent
                                info!("Chat from technician: {}", chat.content);
                                let _ = input_tx.send(data.to_vec()).await;
                            }
                            _ => {}
                        }
                    }
                    Err(e) => {
                        warn!("Failed to decode viewer message: {}", e);
                    }
                }
            }
            Ok(Message::Close(_)) => {
                info!("Viewer {} disconnected from session: {}", viewer_id, session_id);
                break;
            }
            Ok(_) => {}
            Err(e) => {
                error!("WebSocket error from viewer {}: {}", viewer_id, e);
                break;
            }
        }
    }

    // Cleanup (this sends StopStream to agent if last viewer)
    frame_forward.abort();
    sessions_cleanup.leave_session(session_id, &viewer_id_cleanup).await;
    info!("Viewer {} left session: {}", viewer_id_cleanup, session_id);
}


Perfect! Now let me get a quick look at the dashboard structure to see how it all connects:


Bash

find "C:\Users\MikeSwanson\Claude\guru-connect\dashboard" -type f -name "*.tsx" -o -name "*.ts" -o -name "package.json" | head -15

Find dashboard project files


Perfect! Now I have a comprehensive view. Let me create a detailed summary document:

## GuruConnect Dashboard and Viewer Implementation - Exploration Report

Based on my analysis of the GuruConnect codebase, here's a comprehensive breakdown of what currently exists and what needs to be built for a full viewer implementation:

---

## 1. CURRENT DASHBOARD STRUCTURE (dashboard.html)

### What Exists:
- **Full HTML dashboard** with tabs for: Support, Access, Build, and Settings
- **Static portal page** served from `/dashboard` route with embedded JavaScript
- **Tab system** with Active Support Sessions, Access (machines list), Installer Builder, and Settings
- **Session management**:
  - Generate support codes with `/api/codes` endpoint
  - List active sessions with auto-refresh every 3 seconds
  - Show session status (pending/connected)
  - Display connected machines with viewer count and status

### Connect Button Implementation:
The current implementation has **placeholder alerts**:
```javascript
function joinSession(sessionId) {
    // TODO: Open viewer in new window
    alert("Viewer not yet implemented.\n\nSession ID: " + sessionId +
          "\n\nWebSocket: wss://connect.azcomputerguru.com/ws/viewer?session_id=" + sessionId);
}
```

Similar placeholder exists for `connectToMachine()` function.

### Existing Chat Implementation:
- **Functional chat modal** in the dashboard HTML
- **WebSocket connection** to `/ws/viewer?session_id={sessionId}`
- **Simplified protobuf parsing** for chat messages (manual binary parsing)
- **Message encoding/decoding** with basic structure:
  - Message type identifier (field numbers: 60 = ChatMessage)
  - Sender/content extraction from binary data
  - Simple varint encoding for protobuf

---

## 2. REACT DASHBOARD COMPONENTS (TypeScript/React)

### Already Built Components:

#### RemoteViewer.tsx
A **complete, production-ready canvas viewer component** that:
- Connects via WebSocket to receive video frames
- Renders BGRA frames to HTML5 canvas
- Handles frame decompression (placeholder for zstd)
- Supports display size changes
- Captures and sends mouse events (move, down, up, wheel)
- Captures and sends keyboard events
- Shows status bar with connection state, resolution, FPS, latency
- Props: `serverUrl`, `sessionId`, `className`, `onStatusChange`, `autoConnect`, `showStatusBar`

#### useRemoteSession Hook
A **complete React hook** that manages:
- WebSocket connection to `/ws/viewer?session_id={sessionId}`
- Connection state and automatic reconnection (2-second retry)
- Frame decoding and FPS calculation
- Mouse and keyboard event encoding
- Proper cleanup and disconnect handling
- Returns: `status`, `connect()`, `disconnect()`, `sendMouseEvent()`, `sendKeyEvent()`

#### SessionControls.tsx
A **toolbar component** for:
- Display selector (for multi-monitor systems)
- Quality preset dropdown (auto/low/balanced/high)
- Special key buttons (Ctrl+Alt+Del, Lock Screen, Print Screen)
- Disconnect button with red styling
- Fully styled with inline CSS for dark theme

#### Protocol Types (protocol.ts)
**Complete TypeScript definitions** for:
- VideoFrame (raw/VP9/H264/H265 variants)
- RawFrame with BGRA data, compression flag, dirty rects
- MouseEvent with button states and wheel deltas
- KeyEvent with modifier keys and Unicode support
- QualitySettings and ConnectionStatus
- All types matching protobuf definitions

#### Protobuf Library (protobuf.ts)
**Minimal protobuf encoder/decoder** with:
- `encodeMouseEvent()` - 11-byte binary format
- `encodeKeyEvent()` - 8-10 byte binary format
- `decodeVideoFrame()` - parses raw/VP9/H264/H265 frames
- Simple 32-byte buffer approach (not full protobuf.js)
- Placeholder for zstd decompression

---

## 3. PROTOBUF MESSAGE STRUCTURE (guruconnect.proto)

### VideoFrame Structure:
```protobuf
message VideoFrame {
    int64 timestamp = 1;
    int32 display_id = 2;
    int32 sequence = 3;

    oneof encoding {
        RawFrame raw = 10;           // Zstd compressed BGRA
        EncodedFrame vp9 = 11;       // VP9 codec
        EncodedFrame h264 = 12;      // H264 codec
        EncodedFrame h265 = 13;      // H265 codec
    }
}

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

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

### Top-Level Message Wrapper:
```protobuf
message Message {
    oneof payload {
        VideoFrame video_frame = 10;
        MouseEvent mouse_event = 20;
        KeyEvent key_event = 21;
        ChatMessage chat_message = 60;
        // ... 20+ other message types
    }
}
```

---

## 4. SERVER-SIDE IMPLEMENTATION

### WebSocket Relay (/ws/viewer endpoint)
**File**: `server/src/relay/mod.rs`

#### Current Implementation:
- Accepts viewer connections with `session_id` parameter
- Uses `sessions.join_session()` to subscribe to frame broadcasts
- **Forwards binary frames** from agent to viewer in real-time
- **Forwards input events** (mouse, keyboard, chat) from viewer to agent
- Automatic cleanup when viewer disconnects
- Sends `StartStream` to agent when first viewer joins
- Sends `StopStream` when last viewer leaves

#### Frame Flow:
1. Agent captures screen → encodes to protobuf → sends binary to server
2. Server broadcasts to all connected viewers
3. Viewer receives, decodes protobuf, renders to canvas

#### Input Flow:
1. Viewer captures mouse/keyboard → encodes to protobuf → sends binary
2. Server forwards to agent
3. Agent injects input to Windows

---

## 5. WHAT'S MISSING FOR FULL VIEWER

### Critical Missing Pieces:

#### 1. **Integration with Dashboard HTML**
- The React components exist but aren't integrated into the static `dashboard.html`
- Need to either:
  - Replace HTML viewer with React Modal component, OR
  - Embed React viewer in the existing dashboard page, OR
  - Create a separate viewer.html that uses React

#### 2. **Proper Protobuf Library**
Currently using simplified binary format. Needs:
- `protobufjs` library to handle proper protobuf encoding/decoding
- Full support for all 60+ message types
- Proper varint encoding for field numbers > 127
- Nested message support

#### 3. **Video Codec Support**
Current code only handles raw BGRA. Missing:
- **VP9 codec decoding** (software)
- **H264 codec decoding** (hardware acceleration)
- **H265 codec decoding** (hardware acceleration)
- Requires: `libav.js` or `ffmpeg.js` for client-side decoding

#### 4. **Zstd Decompression**
Placeholder exists but not implemented:
- Need `fzstd` library (npm package)
- Decompress raw BGRA frames before canvas rendering

#### 5. **Dirty Rectangle Optimization**
Current code ignores dirty rects:
- Need to parse `DirtyRect` array from RawFrame
- Only update changed regions on canvas for bandwidth savings

#### 6. **Multi-Display Support**
- UI exists in SessionControls but implementation missing
- Need to handle `switch_display` message to agent
- Track multiple display info from agent

#### 7. **Clipboard Support**
Not implemented:
- Send/receive clipboard data
- Handle text, HTML, RTF, images
- Requires clipboard API (limited browser support)

#### 8. **Quality/Bitrate Control**
UI exists but not wired:
- Send `QualitySettings` to agent
- Agent adjusts frame rate and codec based on settings
- Need latency-based auto-adjustment

#### 9. **Cursor Shape/Position**
Not implemented:
- Render custom cursor shapes from agent
- Track cursor position for smooth remote cursor display
- Handle cursor hotspot positioning

#### 10. **Special Key Support**
UI buttons exist but not implemented:
- Send `SpecialKeyEvent` for Ctrl+Alt+Del, Lock Screen, Print Screen
- Requires special protobuf message encoding

#### 11. **Session Recovery**
Not implemented:
- Auto-reconnect on network drop
- Session state restoration
- Frame buffering/catchup

#### 12. **Performance Monitoring**
Partial (FPS tracking exists):
- Need `LatencyReport` message from agent
- RTT measurement
- Bitrate monitoring
- Network statistics in UI

---

## 6. ARCHITECTURE OPTIONS

### Option A: Standalone React Viewer App
```
/viewer - New route serving React app
├── Embed RemoteViewer component
├── Add SessionControls toolbar
├── Connect to /ws/viewer endpoint
└── Full React ecosystem
```

### Option B: Integrate React into Dashboard
```
dashboard.html → Load React from CDN
├── Add modal with RemoteViewer component
├── Embed SessionControls
├── Preserve existing HTML structure
└── Minimal changes to current dashboard
```

### Option C: Hybrid Approach
```
/dashboard - Keep static HTML + chat
/viewer/{sessionId} - React viewer page
├── Purpose-built for viewing
├── Full feature set
├── Separate from session management
└── Clean separation of concerns
```

---

## 7. DEPLOYMENT CHECKLIST

To make the viewer fully functional, you need:

- [ ] Choose integration approach (A/B/C above)
- [ ] Implement proper protobuf decoding (`protobufjs`)
- [ ] Add video codec library (`libav.js` or alternatives)
- [ ] Add zstd decompression (`fzstd`)
- [ ] Wire up UI controls to proto messages
- [ ] Implement special key handling
- [ ] Add clipboard support
- [ ] Implement multi-display switching
- [ ] Test with real agent video frames
- [ ] Add performance monitoring
- [ ] Handle network disconnection/reconnection
- [ ] Security: implement session auth tokens
- [ ] Build & bundle React viewer

---

## Summary

**What Already Works:**
- Server-side WebSocket relay and frame broadcasting
- React viewer components (RemoteViewer, useRemoteSession hook)
- TypeScript protocol types
- Basic chat functionality
- Session management API
- Dashboard UI for initiating sessions

**What Needs To Be Built:**
- Proper protobuf encoding/decoding library
- Video codec support (VP9, H264, H265)
- Frame decompression (zstd)
- UI/React integration
- Clipboard and special keys
- Performance monitoring
- Session authentication

The foundation is **solid and well-architected**. The missing pieces are mostly client-side polish and feature completeness.