     1→//! Installation and protocol handler registration
     2→//!
     3→//! Handles:
     4→//! - Self-installation to Program Files (with UAC) or LocalAppData (fallback)
     5→//! - Protocol handler registration (guruconnect://)
     6→//! - UAC elevation with graceful fallback
     7→
     8→use anyhow::{anyhow, Result};
     9→use tracing::{info, warn, error};
    10→
    11→#[cfg(windows)]
    12→use windows::{
    13→    core::PCWSTR,
    14→    Win32::Foundation::HANDLE,
    15→    Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
    16→    Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
    17→    Win32::System::Registry::{
    18→        RegCreateKeyExW, RegSetValueExW, RegCloseKey, HKEY, HKEY_CLASSES_ROOT,
    19→        HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, REG_OPTION_NON_VOLATILE,
    20→    },
    21→    Win32::UI::Shell::ShellExecuteW,
    22→    Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
    23→};
    24→
    25→#[cfg(windows)]
    26→use std::ffi::OsStr;
    27→#[cfg(windows)]
    28→use std::os::windows::ffi::OsStrExt;
    29→
    30→/// Install locations
    31→pub const SYSTEM_INSTALL_PATH: &str = r"C:\Program Files\GuruConnect";
    32→pub const USER_INSTALL_PATH: &str = r"GuruConnect"; // Relative to %LOCALAPPDATA%
    33→
    34→/// Check if running with elevated privileges
    35→#[cfg(windows)]
    36→pub fn is_elevated() -> bool {
    37→    unsafe {
    38→        let mut token_handle = HANDLE::default();
    39→        if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
    40→            return false;
    41→        }
    42→
    43→        let mut elevation = TOKEN_ELEVATION::default();
    44→        let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
    45→
    46→        let result = GetTokenInformation(
    47→            token_handle,
    48→            TokenElevation,
    49→            Some(&mut elevation as *mut _ as *mut _),
    50→            size,
    51→            &mut size,
    52→        );
    53→
    54→        let _ = windows::Win32::Foundation::CloseHandle(token_handle);
    55→
    56→        result.is_ok() && elevation.TokenIsElevated != 0
    57→    }
    58→}
    59→
    60→#[cfg(not(windows))]
    61→pub fn is_elevated() -> bool {
    62→    unsafe { libc::geteuid() == 0 }
    63→}
    64→
    65→/// Get the install path based on elevation status
    66→pub fn get_install_path(elevated: bool) -> std::path::PathBuf {
    67→    if elevated {
    68→        std::path::PathBuf::from(SYSTEM_INSTALL_PATH)
    69→    } else {
    70→        let local_app_data = std::env::var("LOCALAPPDATA")
    71→            .unwrap_or_else(|_| {
    72→                let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
    73→                format!(r"{}\AppData\Local", home)
    74→            });
    75→        std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
    76→    }
    77→}
    78→
    79→/// Get the executable path
    80→pub fn get_exe_path(install_path: &std::path::Path) -> std::path::PathBuf {
    81→    install_path.join("guruconnect.exe")
    82→}
    83→
    84→/// Attempt to elevate and re-run with install command
    85→#[cfg(windows)]
    86→pub fn try_elevate_and_install() -> Result<bool> {
    87→    let exe_path = std::env::current_exe()?;
    88→    let exe_path_wide: Vec<u16> = OsStr::new(exe_path.as_os_str())
    89→        .encode_wide()
    90→        .chain(std::iter::once(0))
    91→        .collect();
    92→
    93→    let verb: Vec<u16> = OsStr::new("runas")
    94→        .encode_wide()
    95→        .chain(std::iter::once(0))
    96→        .collect();
    97→
    98→    let params: Vec<u16> = OsStr::new("install --elevated")
    99→        .encode_wide()
   100→        .chain(std::iter::once(0))
   101→        .collect();
   102→
   103→    unsafe {
   104→        let result = ShellExecuteW(
   105→            None,
   106→            PCWSTR(verb.as_ptr()),
   107→            PCWSTR(exe_path_wide.as_ptr()),
   108→            PCWSTR(params.as_ptr()),
   109→            PCWSTR::null(),
   110→            SW_SHOWNORMAL,
   111→        );
   112→
   113→        // ShellExecuteW returns > 32 on success
   114→        if result.0 as usize > 32 {
   115→            info!("UAC elevation requested");
   116→            Ok(true)
   117→        } else {
   118→            warn!("UAC elevation denied or failed");
   119→            Ok(false)
   120→        }
   121→    }
   122→}
   123→
   124→#[cfg(not(windows))]
   125→pub fn try_elevate_and_install() -> Result<bool> {
   126→    Ok(false)
   127→}
   128→
   129→/// Register the guruconnect:// protocol handler
   130→#[cfg(windows)]
   131→pub fn register_protocol_handler(elevated: bool) -> Result<()> {
   132→    let install_path = get_install_path(elevated);
   133→    let exe_path = get_exe_path(&install_path);
   134→    let exe_path_str = exe_path.to_string_lossy();
   135→
   136→    // Command to execute: "C:\...\guruconnect.exe" "launch" "%1"
   137→    let command = format!("\"{}\" launch \"%1\"", exe_path_str);
   138→
   139→    // Choose registry root based on elevation
   140→    let root_key = if elevated {
   141→        HKEY_CLASSES_ROOT
   142→    } else {
   143→        // User-level registration under Software\Classes
   144→        HKEY_CURRENT_USER
   145→    };
   146→
   147→    let base_path = if elevated {
   148→        "guruconnect"
   149→    } else {
   150→        r"Software\Classes\guruconnect"
   151→    };
   152→
   153→    unsafe {
   154→        // Create guruconnect key
   155→        let mut protocol_key = HKEY::default();
   156→        let key_path = to_wide(base_path);
   157→        let result = RegCreateKeyExW(
   158→            root_key,
   159→            PCWSTR(key_path.as_ptr()),
   160→            0,
   161→            PCWSTR::null(),
   162→            REG_OPTION_NON_VOLATILE,
   163→            KEY_WRITE,
   164→            None,
   165→            &mut protocol_key,
   166→            None,
   167→        );
   168→        if result.is_err() {
   169→            return Err(anyhow!("Failed to create protocol key: {:?}", result));
   170→        }
   171→
   172→        // Set default value (protocol description)
   173→        let description = to_wide("GuruConnect Protocol");
   174→        let result = RegSetValueExW(
   175→            protocol_key,
   176→            PCWSTR::null(),
   177→            0,
   178→            REG_SZ,
   179→            Some(&description_to_bytes(&description)),
   180→        );
   181→        if result.is_err() {
   182→            let _ = RegCloseKey(protocol_key);
   183→            return Err(anyhow!("Failed to set protocol description: {:?}", result));
   184→        }
   185→
   186→        // Set URL Protocol (empty string indicates this is a protocol handler)
   187→        let url_protocol = to_wide("URL Protocol");
   188→        let empty = to_wide("");
   189→        let result = RegSetValueExW(
   190→            protocol_key,
   191→            PCWSTR(url_protocol.as_ptr()),
   192→            0,
   193→            REG_SZ,
   194→            Some(&description_to_bytes(&empty)),
   195→        );
   196→        if result.is_err() {
   197→            let _ = RegCloseKey(protocol_key);
   198→            return Err(anyhow!("Failed to set URL Protocol: {:?}", result));
   199→        }
   200→
   201→        let _ = RegCloseKey(protocol_key);
   202→
   203→        // Create shell\open\command key
   204→        let command_path = if elevated {
   205→            r"guruconnect\shell\open\command"
   206→        } else {
   207→            r"Software\Classes\guruconnect\shell\open\command"
   208→        };
   209→        let command_key_path = to_wide(command_path);
   210→        let mut command_key = HKEY::default();
   211→        let result = RegCreateKeyExW(
   212→            root_key,
   213→            PCWSTR(command_key_path.as_ptr()),
   214→            0,
   215→            PCWSTR::null(),
   216→            REG_OPTION_NON_VOLATILE,
   217→            KEY_WRITE,
   218→            None,
   219→            &mut command_key,
   220→            None,
   221→        );
   222→        if result.is_err() {
   223→            return Err(anyhow!("Failed to create command key: {:?}", result));
   224→        }
   225→
   226→        // Set the command
   227→        let command_wide = to_wide(&command);
   228→        let result = RegSetValueExW(
   229→            command_key,
   230→            PCWSTR::null(),
   231→            0,
   232→            REG_SZ,
   233→            Some(&description_to_bytes(&command_wide)),
   234→        );
   235→        if result.is_err() {
   236→            let _ = RegCloseKey(command_key);
   237→            return Err(anyhow!("Failed to set command: {:?}", result));
   238→        }
   239→
   240→        let _ = RegCloseKey(command_key);
   241→    }
   242→
   243→    info!("Protocol handler registered: guruconnect://");
   244→    Ok(())
   245→}
   246→
   247→#[cfg(not(windows))]
   248→pub fn register_protocol_handler(_elevated: bool) -> Result<()> {
   249→    warn!("Protocol handler registration not supported on this platform");
   250→    Ok(())
   251→}
   252→
   253→/// Install the application
   254→pub fn install(force_user_install: bool) -> Result<()> {
   255→    let elevated = is_elevated();
   256→
   257→    // If not elevated and not forcing user install, try to elevate
   258→    if !elevated && !force_user_install {
   259→        info!("Attempting UAC elevation for system-wide install...");
   260→        match try_elevate_and_install() {
   261→            Ok(true) => {
   262→                // Elevation was requested, exit this instance
   263→                // The elevated instance will continue the install
   264→                info!("Elevated process started, exiting current instance");
   265→                std::process::exit(0);
   266→            }
   267→            Ok(false) => {
   268→                info!("UAC denied, falling back to user install");
   269→            }
   270→            Err(e) => {
   271→                warn!("Elevation failed: {}, falling back to user install", e);
   272→            }
   273→        }
   274→    }
   275→
   276→    let install_path = get_install_path(elevated);
   277→    let exe_path = get_exe_path(&install_path);
   278→
   279→    info!("Installing to: {}", install_path.display());
   280→
   281→    // Create install directory
   282→    std::fs::create_dir_all(&install_path)?;
   283→
   284→    // Copy ourselves to install location
   285→    let current_exe = std::env::current_exe()?;
   286→    if current_exe != exe_path {
   287→        std::fs::copy(&current_exe, &exe_path)?;
   288→        info!("Copied executable to: {}", exe_path.display());
   289→    }
   290→
   291→    // Register protocol handler
   292→    register_protocol_handler(elevated)?;
   293→
   294→    info!("Installation complete!");
   295→    if elevated {
   296→        info!("Installed system-wide to: {}", install_path.display());
   297→    } else {
   298→        info!("Installed for current user to: {}", install_path.display());
   299→    }
   300→
   301→    Ok(())
   302→}
   303→
   304→/// Parse a guruconnect:// URL and extract session parameters
   305→pub fn parse_protocol_url(url: &str) -> Result<(String, String, Option<String>)> {
   306→    // Expected formats:
   307→    // guruconnect://view/SESSION_ID
   308→    // guruconnect://view/SESSION_ID?token=API_KEY
   309→    // guruconnect://connect/SESSION_ID?server=wss://...&token=API_KEY
   310→
   311→    let url = url::Url::parse(url)
   312→        .map_err(|e| anyhow!("Invalid URL: {}", e))?;
   313→
   314→    if url.scheme() != "guruconnect" {
   315→        return Err(anyhow!("Invalid scheme: expected guruconnect://"));
   316→    }
   317→
   318→    let path = url.path().trim_start_matches('/');
   319→    let parts: Vec<&str> = path.split('/').collect();
   320→
   321→    if parts.is_empty() {
   322→        return Err(anyhow!("Missing action in URL"));
   323→    }
   324→
   325→    let action = parts[0];
   326→    let session_id = parts.get(1).map(|s| s.to_string())
   327→        .ok_or_else(|| anyhow!("Missing session ID"))?;
   328→
   329→    // Extract query parameters
   330→    let mut server = None;
   331→    let mut token = None;
   332→
   333→    for (key, value) in url.query_pairs() {
   334→        match key.as_ref() {
   335→            "server" => server = Some(value.to_string()),
   336→            "token" | "api_key" => token = Some(value.to_string()),
   337→            _ => {}
   338→        }
   339→    }
   340→
   341→    // Default server if not specified
   342→    let server = server.unwrap_or_else(|| "wss://connect.azcomputerguru.com/ws/viewer".to_string());
   343→
   344→    match action {
   345→        "view" | "connect" => Ok((server, session_id, token)),
   346→        _ => Err(anyhow!("Unknown action: {}", action)),
   347→    }
   348→}
   349→
   350→// Helper functions for Windows registry operations
   351→#[cfg(windows)]
   352→fn to_wide(s: &str) -> Vec<u16> {
   353→    OsStr::new(s)
   354→        .encode_wide()
   355→        .chain(std::iter::once(0))
   356→        .collect()
   357→}
   358→
   359→#[cfg(windows)]
   360→fn description_to_bytes(wide: &[u16]) -> Vec<u8> {
   361→    wide.iter()
   362→        .flat_map(|w| w.to_le_bytes())
   363→        .collect()
   364→}
   365→

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

//! Installation and protocol handler registration
//!
//! Handles:
//! - Self-installation to Program Files (with UAC) or LocalAppData (fallback)
//! - Protocol handler registration (guruconnect://)
//! - UAC elevation with graceful fallback

use anyhow::{anyhow, Result};
use tracing::{info, warn, error};

#[cfg(windows)]
use windows::{
    core::PCWSTR,
    Win32::Foundation::HANDLE,
    Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
    Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
    Win32::System::Registry::{
        RegCreateKeyExW, RegSetValueExW, RegCloseKey, HKEY, HKEY_CLASSES_ROOT,
        HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, REG_OPTION_NON_VOLATILE,
    },
    Win32::UI::Shell::ShellExecuteW,
    Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
};

#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;

/// Install locations
pub const SYSTEM_INSTALL_PATH: &str = r"C:\Program Files\GuruConnect";
pub const USER_INSTALL_PATH: &str = r"GuruConnect"; // Relative to %LOCALAPPDATA%

/// Check if running with elevated privileges
#[cfg(windows)]
pub fn is_elevated() -> bool {
    unsafe {
        let mut token_handle = HANDLE::default();
        if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
            return false;
        }

        let mut elevation = TOKEN_ELEVATION::default();
        let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;

        let result = GetTokenInformation(
            token_handle,
            TokenElevation,
            Some(&mut elevation as *mut _ as *mut _),
            size,
            &mut size,
        );

        let _ = windows::Win32::Foundation::CloseHandle(token_handle);

        result.is_ok() && elevation.TokenIsElevated != 0
    }
}

#[cfg(not(windows))]
pub fn is_elevated() -> bool {
    unsafe { libc::geteuid() == 0 }
}

/// Get the install path based on elevation status
pub fn get_install_path(elevated: bool) -> std::path::PathBuf {
    if elevated {
        std::path::PathBuf::from(SYSTEM_INSTALL_PATH)
    } else {
        let local_app_data = std::env::var("LOCALAPPDATA")
            .unwrap_or_else(|_| {
                let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
                format!(r"{}\AppData\Local", home)
            });
        std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
    }
}

/// Get the executable path
pub fn get_exe_path(install_path: &std::path::Path) -> std::path::PathBuf {
    install_path.join("guruconnect.exe")
}

/// Attempt to elevate and re-run with install command
#[cfg(windows)]
pub fn try_elevate_and_install() -> Result<bool> {
    let exe_path = std::env::current_exe()?;
    let exe_path_wide: Vec<u16> = OsStr::new(exe_path.as_os_str())
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    let verb: Vec<u16> = OsStr::new("runas")
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    let params: Vec<u16> = OsStr::new("install --elevated")
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        let result = ShellExecuteW(
            None,
            PCWSTR(verb.as_ptr()),
            PCWSTR(exe_path_wide.as_ptr()),
            PCWSTR(params.as_ptr()),
            PCWSTR::null(),
            SW_SHOWNORMAL,
        );

        // ShellExecuteW returns > 32 on success
        if result.0 as usize > 32 {
            info!("UAC elevation requested");
            Ok(true)
        } else {
            warn!("UAC elevation denied or failed");
            Ok(false)
        }
    }
}

#[cfg(not(windows))]
pub fn try_elevate_and_install() -> Result<bool> {
    Ok(false)
}

/// Register the guruconnect:// protocol handler
#[cfg(windows)]
pub fn register_protocol_handler(elevated: bool) -> Result<()> {
    let install_path = get_install_path(elevated);
    let exe_path = get_exe_path(&install_path);
    let exe_path_str = exe_path.to_string_lossy();

    // Command to execute: "C:\...\guruconnect.exe" "launch" "%1"
    let command = format!("\"{}\" launch \"%1\"", exe_path_str);

    // Choose registry root based on elevation
    let root_key = if elevated {
        HKEY_CLASSES_ROOT
    } else {
        // User-level registration under Software\Classes
        HKEY_CURRENT_USER
    };

    let base_path = if elevated {
        "guruconnect"
    } else {
        r"Software\Classes\guruconnect"
    };

    unsafe {
        // Create guruconnect key
        let mut protocol_key = HKEY::default();
        let key_path = to_wide(base_path);
        let result = RegCreateKeyExW(
            root_key,
            PCWSTR(key_path.as_ptr()),
            0,
            PCWSTR::null(),
            REG_OPTION_NON_VOLATILE,
            KEY_WRITE,
            None,
            &mut protocol_key,
            None,
        );
        if result.is_err() {
            return Err(anyhow!("Failed to create protocol key: {:?}", result));
        }

        // Set default value (protocol description)
        let description = to_wide("GuruConnect Protocol");
        let result = RegSetValueExW(
            protocol_key,
            PCWSTR::null(),
            0,
            REG_SZ,
            Some(&description_to_bytes(&description)),
        );
        if result.is_err() {
            let _ = RegCloseKey(protocol_key);
            return Err(anyhow!("Failed to set protocol description: {:?}", result));
        }

        // Set URL Protocol (empty string indicates this is a protocol handler)
        let url_protocol = to_wide("URL Protocol");
        let empty = to_wide("");
        let result = RegSetValueExW(
            protocol_key,
            PCWSTR(url_protocol.as_ptr()),
            0,
            REG_SZ,
            Some(&description_to_bytes(&empty)),
        );
        if result.is_err() {
            let _ = RegCloseKey(protocol_key);
            return Err(anyhow!("Failed to set URL Protocol: {:?}", result));
        }

        let _ = RegCloseKey(protocol_key);

        // Create shell\open\command key
        let command_path = if elevated {
            r"guruconnect\shell\open\command"
        } else {
            r"Software\Classes\guruconnect\shell\open\command"
        };
        let command_key_path = to_wide(command_path);
        let mut command_key = HKEY::default();
        let result = RegCreateKeyExW(
            root_key,
            PCWSTR(command_key_path.as_ptr()),
            0,
            PCWSTR::null(),
            REG_OPTION_NON_VOLATILE,
            KEY_WRITE,
            None,
            &mut command_key,
            None,
        );
        if result.is_err() {
            return Err(anyhow!("Failed to create command key: {:?}", result));
        }

        // Set the command
        let command_wide = to_wide(&command);
        let result = RegSetValueExW(
            command_key,
            PCWSTR::null(),
            0,
            REG_SZ,
            Some(&description_to_bytes(&command_wide)),
        );
        if result.is_err() {
            let _ = RegCloseKey(command_key);
            return Err(anyhow!("Failed to set command: {:?}", result));
        }

        let _ = RegCloseKey(command_key);
    }

    info!("Protocol handler registered: guruconnect://");
    Ok(())
}

#[cfg(not(windows))]
pub fn register_protocol_handler(_elevated: bool) -> Result<()> {
    warn!("Protocol handler registration not supported on this platform");
    Ok(())
}

/// Install the application
pub fn install(force_user_install: bool) -> Result<()> {
    let elevated = is_elevated();

    // If not elevated and not forcing user install, try to elevate
    if !elevated && !force_user_install {
        info!("Attempting UAC elevation for system-wide install...");
        match try_elevate_and_install() {
            Ok(true) => {
                // Elevation was requested, exit this instance
                // The elevated instance will continue the install
                info!("Elevated process started, exiting current instance");
                std::process::exit(0);
            }
            Ok(false) => {
                info!("UAC denied, falling back to user install");
            }
            Err(e) => {
                warn!("Elevation failed: {}, falling back to user install", e);
            }
        }
    }

    let install_path = get_install_path(elevated);
    let exe_path = get_exe_path(&install_path);

    info!("Installing to: {}", install_path.display());

    // Create install directory
    std::fs::create_dir_all(&install_path)?;

    // Copy ourselves to install location
    let current_exe = std::env::current_exe()?;
    if current_exe != exe_path {
        std::fs::copy(&current_exe, &exe_path)?;
        info!("Copied executable to: {}", exe_path.display());
    }

    // Register protocol handler
    register_protocol_handler(elevated)?;

    info!("Installation complete!");
    if elevated {
        info!("Installed system-wide to: {}", install_path.display());
    } else {
        info!("Installed for current user to: {}", install_path.display());
    }

    Ok(())
}

/// Parse a guruconnect:// URL and extract session parameters
pub fn parse_protocol_url(url: &str) -> Result<(String, String, Option<String>)> {
    // Expected formats:
    // guruconnect://view/SESSION_ID
    // guruconnect://view/SESSION_ID?token=API_KEY
    // guruconnect://connect/SESSION_ID?server=wss://...&token=API_KEY

    let url = url::Url::parse(url)
        .map_err(|e| anyhow!("Invalid URL: {}", e))?;

    if url.scheme() != "guruconnect" {
        return Err(anyhow!("Invalid scheme: expected guruconnect://"));
    }

    let path = url.path().trim_start_matches('/');
    let parts: Vec<&str> = path.split('/').collect();

    if parts.is_empty() {
        return Err(anyhow!("Missing action in URL"));
    }

    let action = parts[0];
    let session_id = parts.get(1).map(|s| s.to_string())
        .ok_or_else(|| anyhow!("Missing session ID"))?;

    // Extract query parameters
    let mut server = None;
    let mut token = None;

    for (key, value) in url.query_pairs() {
        match key.as_ref() {
            "server" => server = Some(value.to_string()),
            "token" | "api_key" => token = Some(value.to_string()),
            _ => {}
        }
    }

    // Default server if not specified
    let server = server.unwrap_or_else(|| "wss://connect.azcomputerguru.com/ws/viewer".to_string());

    match action {
        "view" | "connect" => Ok((server, session_id, token)),
        _ => Err(anyhow!("Unknown action: {}", action)),
    }
}

// Helper functions for Windows registry operations
#[cfg(windows)]
fn to_wide(s: &str) -> Vec<u16> {
    OsStr::new(s)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect()
}

#[cfg(windows)]
fn description_to_bytes(wide: &[u16]) -> Vec<u8> {
    wide.iter()
        .flat_map(|w| w.to_le_bytes())
        .collect()
}


The protocol handler is already implemented in install.rs. It:
1. Registers `guruconnect://` protocol in the registry
2. Parses URLs like `guruconnect://view/SESSION_ID?server=...&token=...`
3. The command registered points to `guruconnect.exe launch "%1"`

Now I need to update the dashboard to:
1. Use the `guruconnect://` protocol link
2. Detect if the protocol handler isn't installed and offer to download/run the viewer
3. Retry after installation

The URL format should be:
`guruconnect://view/SESSION_ID?server=wss://connect.azcomputerguru.com/ws/viewer`

Let me update the dashboard.


The protocol handler is already implemented. Now I need to update the dashboard to use it properly with fallback. Let me update the dashboard:


Edit

                        <button class="copy-btn" onclick="copyNativeCommand()">Copy</button>

                        Run this command in PowerShell or CMD where guruconnect-viewer.exe is located.

                        Native viewer not installed

                        Download and run GuruConnect to register the protocol handler, then click "Try Again".

                        Download GuruConnect
                        <button class="btn btn-outline" onclick="retryNativeViewer()">Try Again</button>


The file C:\Users\MikeSwanson\Claude\guru-connect\server\static\dashboard.html has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   595→
Native Viewer

   596→
Full keyboard capture including Win key, Alt+Tab, and Ctrl+Alt+Del.

   597→

   598→

   599→

   600→

   601→                        Native viewer not installed
   602→

   603→

   604→                        Download and run GuruConnect to register the protocol handler, then click "Try Again".
   605→

   606→

   607→                        Download GuruConnect
   608→                        <button class="btn btn-outline" onclick="retryNativeViewer()">Try Again</button>
   609→

   610→

   611→

   612→

   613→

   614→

                        Native viewer not installed

                        Download and run GuruConnect to register the protocol handler, then click "Try Again".

                        Download GuruConnect
                        <button class="btn btn-outline" onclick="retryNativeViewer()">Try Again</button>

<!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>
        <button class="tab admin-only" data-tab="users" style="display: none;">Users</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.

User Management

Manage user accounts and permissions

                    Open User Management

Connect to Machine

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

🌐

Web Viewer

Opens in browser. Most keys work, but Win key and Alt+Tab are captured by your OS.

🖥

Native Viewer

Full keyboard capture including Win key, Alt+Tab, and Ctrl+Alt+Del.

                        <button class="copy-btn" onclick="copyNativeCommand()">Copy</button>

                        Run this command in PowerShell or CMD where guruconnect-viewer.exe is located.

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>

-                <div id="nativeCommandContainer" style="display: none;">

-                    <div class="native-command">

-                        <button class="copy-btn" onclick="copyNativeCommand()">Copy</button>

-                    <p style="margin-top: 12px; font-size: 12px; color: hsl(var(--muted-foreground));">

-                        Run this command in PowerShell or CMD where guruconnect-viewer.exe is located.

+                <div id="nativeNotInstalled" style="display: none; margin-top: 16px; padding: 16px; background: hsla(45, 93%, 47%, 0.1); border: 1px solid hsla(45, 93%, 47%, 0.3); border-radius: 8px;">

+                    <p style="margin-bottom: 12px; color: hsl(45, 93%, 55%);">

+                        Native viewer not installed

+                    <p style="font-size: 13px; margin-bottom: 12px; color: hsl(var(--muted-foreground));">

+                        Download and run GuruConnect to register the protocol handler, then click "Try Again".

+                    <div style="display: flex; gap: 8px;">

+                        Download GuruConnect

+                        <button class="btn btn-outline" onclick="retryNativeViewer()">Try Again</button>


Now I need to update the JavaScript to use the protocol URL and handle the "not installed" case. Let me update the JavaScript for the connect modal.


Now update the JavaScript to use the protocol handler:


        // Connect modal state
        let connectSessionId = null;
        let connectMachineName = null;

        const connectModal = document.getElementById("connectModal");
        const connectClose = document.getElementById("connectClose");
        const connectWeb = document.getElementById("connectWeb");
        const connectNative = document.getElementById("connectNative");
        const nativeCommandContainer = document.getElementById("nativeCommandContainer");
        const nativeCommandText = document.getElementById("nativeCommandText");
        const connectMachineNameEl = document.getElementById("connectMachineName");

        connectClose.addEventListener("click", () => {
            connectModal.classList.remove("active");
            nativeCommandContainer.style.display = "none";
        });

        connectModal.addEventListener("click", (e) => {
            if (e.target === connectModal) {
                connectModal.classList.remove("active");
                nativeCommandContainer.style.display = "none";
            }
        });

        connectWeb.addEventListener("click", () => {
            if (connectSessionId) {
                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;
                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
                connectModal.classList.remove("active");
                nativeCommandContainer.style.display = "none";
            }
        });

        connectNative.addEventListener("click", () => {
            if (connectSessionId) {
                const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
                const serverUrl = protocol + "//" + window.location.host + "/ws/viewer";
                const cmd = `guruconnect-viewer.exe -i ${connectSessionId} -s ${serverUrl}`;
                nativeCommandText.textContent = cmd;
                nativeCommandContainer.style.display = "block";
            }
        });

        function copyNativeCommand() {
            const cmd = nativeCommandText.textContent;
            navigator.clipboard.writeText(cmd).then(() => {
                const btn = document.querySelector(".copy-btn");
                btn.textContent = "Copied!";
                setTimeout(() => btn.textContent = "Copy", 2000);
            });
        }

        function connectToMachine(sessionId) {
            const machine = machines.find(m => m.id === sessionId);
            connectSessionId = sessionId;
            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);
            connectMachineNameEl.textContent = connectMachineName;
            nativeCommandContainer.style.display = "none";
            connectModal.classList.add("active");
        }

        // Connect modal state
        let connectSessionId = null;
        let connectMachineName = null;
        let protocolCheckTimeout = null;

        const connectModal = document.getElementById("connectModal");
        const connectClose = document.getElementById("connectClose");
        const connectWeb = document.getElementById("connectWeb");
        const connectNative = document.getElementById("connectNative");
        const nativeNotInstalled = document.getElementById("nativeNotInstalled");
        const connectMachineNameEl = document.getElementById("connectMachineName");

        connectClose.addEventListener("click", () => {
            connectModal.classList.remove("active");
            nativeNotInstalled.style.display = "none";
            if (protocolCheckTimeout) clearTimeout(protocolCheckTimeout);
        });

        connectModal.addEventListener("click", (e) => {
            if (e.target === connectModal) {
                connectModal.classList.remove("active");
                nativeNotInstalled.style.display = "none";
                if (protocolCheckTimeout) clearTimeout(protocolCheckTimeout);
            }
        });

        connectWeb.addEventListener("click", () => {
            if (connectSessionId) {
                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;
                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
                connectModal.classList.remove("active");
                nativeNotInstalled.style.display = "none";
            }
        });

        connectNative.addEventListener("click", () => {
            launchNativeViewer();
        });

        function launchNativeViewer() {
            if (!connectSessionId) return;

            const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
            const serverUrl = encodeURIComponent(protocol + "//" + window.location.host + "/ws/viewer");
            const protocolUrl = `guruconnect://view/${connectSessionId}?server=${serverUrl}`;

            // Try to launch the protocol handler
            // We use a hidden iframe to avoid navigation issues
            const iframe = document.createElement("iframe");
            iframe.style.display = "none";
            document.body.appendChild(iframe);

            // Set up timeout to detect if protocol handler didn't work
            let launched = false;
            const startTime = Date.now();

            // If window loses focus quickly, protocol likely worked
            const blurHandler = () => {
                if (Date.now() - startTime < 2000) {
                    launched = true;
                    connectModal.classList.remove("active");
                    nativeNotInstalled.style.display = "none";
                }
            };
            window.addEventListener("blur", blurHandler, { once: true });

            // Try to launch
            try {
                iframe.contentWindow.location.href = protocolUrl;
            } catch (e) {
                // Some browsers throw on custom protocols
            }

            // Fallback: also try window.location for browsers that block iframe protocols
            protocolCheckTimeout = setTimeout(() => {
                window.removeEventListener("blur", blurHandler);
                document.body.removeChild(iframe);

                if (!launched) {
                    // Protocol handler probably not installed, show download option
                    nativeNotInstalled.style.display = "block";
                }
            }, 1500);
        }

        function retryNativeViewer() {
            nativeNotInstalled.style.display = "none";
            launchNativeViewer();
        }

        function connectToMachine(sessionId) {
            const machine = machines.find(m => m.id === sessionId);
            connectSessionId = sessionId;
            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);
            connectMachineNameEl.textContent = connectMachineName;
            nativeNotInstalled.style.display = "none";
            connectModal.classList.add("active");
        }


The file C:\Users\MikeSwanson\Claude\guru-connect\server\static\dashboard.html has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   926→                    '<button class="btn btn-outline" style="width: 100%; color: hsl(0, 62.8%, 50%);" onclick="disconnectMachine(\'' + m.id + '\', \'' + (m.agent_name || m.agent_id).replace(/'/g, "\\'") + '\')">Disconnect</button>' +
   927→                '</div>';
   928→        }
   929→
   930→        // Connect modal state
   931→        let connectSessionId = null;
   932→        let connectMachineName = null;
   933→        let protocolCheckTimeout = null;
   934→
   935→        const connectModal = document.getElementById("connectModal");
   936→        const connectClose = document.getElementById("connectClose");
   937→        const connectWeb = document.getElementById("connectWeb");
   938→        const connectNative = document.getElementById("connectNative");
   939→        const nativeNotInstalled = document.getElementById("nativeNotInstalled");
   940→        const connectMachineNameEl = document.getElementById("connectMachineName");
   941→
   942→        connectClose.addEventListener("click", () => {
   943→            connectModal.classList.remove("active");
   944→            nativeNotInstalled.style.display = "none";
   945→            if (protocolCheckTimeout) clearTimeout(protocolCheckTimeout);
   946→        });
   947→
   948→        connectModal.addEventListener("click", (e) => {
   949→            if (e.target === connectModal) {
   950→                connectModal.classList.remove("active");
   951→                nativeNotInstalled.style.display = "none";
   952→                if (protocolCheckTimeout) clearTimeout(protocolCheckTimeout);
   953→            }
   954→        });
   955→
   956→        connectWeb.addEventListener("click", () => {
   957→            if (connectSessionId) {
   958→                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;
   959→                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
   960→                connectModal.classList.remove("active");
   961→                nativeNotInstalled.style.display = "none";
   962→            }
   963→        });
   964→
   965→        connectNative.addEventListener("click", () => {
   966→            launchNativeViewer();
   967→        });
   968→
   969→        function launchNativeViewer() {
   970→            if (!connectSessionId) return;
   971→
   972→            const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
   973→            const serverUrl = encodeURIComponent(protocol + "//" + window.location.host + "/ws/viewer");
   974→            const protocolUrl = `guruconnect://view/${connectSessionId}?server=${serverUrl}`;
   975→
   976→            // Try to launch the protocol handler
   977→            // We use a hidden iframe to avoid navigation issues
   978→            const iframe = document.createElement("iframe");
   979→            iframe.style.display = "none";
   980→            document.body.appendChild(iframe);
   981→
   982→            // Set up timeout to detect if protocol handler didn't work
   983→            let launched = false;
   984→            const startTime = Date.now();
   985→
   986→            // If window loses focus quickly, protocol likely worked
   987→            const blurHandler = () => {
   988→                if (Date.now() - startTime < 2000) {
   989→                    launched = true;
   990→                    connectModal.classList.remove("active");
   991→                    nativeNotInstalled.style.display = "none";
   992→                }
   993→            };
   994→            window.addEventListener("blur", blurHandler, { once: true });
   995→
   996→            // Try to launch
   997→            try {
   998→                iframe.contentWindow.location.href = protocolUrl;
   999→            } catch (e) {
  1000→                // Some browsers throw on custom protocols
  1001→            }
  1002→
  1003→            // Fallback: also try window.location for browsers that block iframe protocols
  1004→            protocolCheckTimeout = setTimeout(() => {
  1005→                window.removeEventListener("blur", blurHandler);
  1006→                document.body.removeChild(iframe);
  1007→
  1008→                if (!launched) {
  1009→                    // Protocol handler probably not installed, show download option
  1010→                    nativeNotInstalled.style.display = "block";
  1011→                }
  1012→            }, 1500);
  1013→        }
  1014→
  1015→        function retryNativeViewer() {
  1016→            nativeNotInstalled.style.display = "none";
  1017→            launchNativeViewer();
  1018→        }
  1019→
  1020→        function connectToMachine(sessionId) {
  1021→            const machine = machines.find(m => m.id === sessionId);
  1022→            connectSessionId = sessionId;
  1023→            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);
  1024→            connectMachineNameEl.textContent = connectMachineName;
  1025→            nativeNotInstalled.style.display = "none";
  1026→            connectModal.classList.add("active");
  1027→        }
  1028→
  1029→        async function disconnectMachine(sessionId, machineName) {
  1030→            if (!confirm("Disconnect " + machineName + "?\n\nThis will end the remote session.")) return;
  1031→            try {

        // Connect modal state
        let connectSessionId = null;
        let connectMachineName = null;

        const connectModal = document.getElementById("connectModal");
        const connectClose = document.getElementById("connectClose");
        const connectWeb = document.getElementById("connectWeb");
        const connectNative = document.getElementById("connectNative");
        const nativeCommandContainer = document.getElementById("nativeCommandContainer");
        const nativeCommandText = document.getElementById("nativeCommandText");
        const connectMachineNameEl = document.getElementById("connectMachineName");

        connectClose.addEventListener("click", () => {
            connectModal.classList.remove("active");
            nativeCommandContainer.style.display = "none";
        });

        connectModal.addEventListener("click", (e) => {
            if (e.target === connectModal) {
                connectModal.classList.remove("active");
                nativeCommandContainer.style.display = "none";
            }
        });

        connectWeb.addEventListener("click", () => {
            if (connectSessionId) {
                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;
                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
                connectModal.classList.remove("active");
                nativeCommandContainer.style.display = "none";
            }
        });

        connectNative.addEventListener("click", () => {
            if (connectSessionId) {
                const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
                const serverUrl = protocol + "//" + window.location.host + "/ws/viewer";
                const cmd = `guruconnect-viewer.exe -i ${connectSessionId} -s ${serverUrl}`;
                nativeCommandText.textContent = cmd;
                nativeCommandContainer.style.display = "block";
            }
        });

        function copyNativeCommand() {
            const cmd = nativeCommandText.textContent;
            navigator.clipboard.writeText(cmd).then(() => {
                const btn = document.querySelector(".copy-btn");
                btn.textContent = "Copied!";
                setTimeout(() => btn.textContent = "Copy", 2000);
            });
        }

        function connectToMachine(sessionId) {
            const machine = machines.find(m => m.id === sessionId);
            connectSessionId = sessionId;
            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);
            connectMachineNameEl.textContent = connectMachineName;
            nativeCommandContainer.style.display = "none";
            connectModal.classList.add("active");
        }

        // Connect modal state
        let connectSessionId = null;
        let connectMachineName = null;
        let protocolCheckTimeout = null;

        const connectModal = document.getElementById("connectModal");
        const connectClose = document.getElementById("connectClose");
        const connectWeb = document.getElementById("connectWeb");
        const connectNative = document.getElementById("connectNative");
        const nativeNotInstalled = document.getElementById("nativeNotInstalled");
        const connectMachineNameEl = document.getElementById("connectMachineName");

        connectClose.addEventListener("click", () => {
            connectModal.classList.remove("active");
            nativeNotInstalled.style.display = "none";
            if (protocolCheckTimeout) clearTimeout(protocolCheckTimeout);
        });

        connectModal.addEventListener("click", (e) => {
            if (e.target === connectModal) {
                connectModal.classList.remove("active");
                nativeNotInstalled.style.display = "none";
                if (protocolCheckTimeout) clearTimeout(protocolCheckTimeout);
            }
        });

        connectWeb.addEventListener("click", () => {
            if (connectSessionId) {
                const viewerUrl = "/viewer.html?session_id=" + connectSessionId;
                window.open(viewerUrl, "viewer_" + connectSessionId, "width=1280,height=800,menubar=no,toolbar=no,location=no,status=no");
                connectModal.classList.remove("active");
                nativeNotInstalled.style.display = "none";
            }
        });

        connectNative.addEventListener("click", () => {
            launchNativeViewer();
        });

        function launchNativeViewer() {
            if (!connectSessionId) return;

            const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
            const serverUrl = encodeURIComponent(protocol + "//" + window.location.host + "/ws/viewer");
            const protocolUrl = `guruconnect://view/${connectSessionId}?server=${serverUrl}`;

            // Try to launch the protocol handler
            // We use a hidden iframe to avoid navigation issues
            const iframe = document.createElement("iframe");
            iframe.style.display = "none";
            document.body.appendChild(iframe);

            // Set up timeout to detect if protocol handler didn't work
            let launched = false;
            const startTime = Date.now();

            // If window loses focus quickly, protocol likely worked
            const blurHandler = () => {
                if (Date.now() - startTime < 2000) {
                    launched = true;
                    connectModal.classList.remove("active");
                    nativeNotInstalled.style.display = "none";
                }
            };
            window.addEventListener("blur", blurHandler, { once: true });

            // Try to launch
            try {
                iframe.contentWindow.location.href = protocolUrl;
            } catch (e) {
                // Some browsers throw on custom protocols
            }

            // Fallback: also try window.location for browsers that block iframe protocols
            protocolCheckTimeout = setTimeout(() => {
                window.removeEventListener("blur", blurHandler);
                document.body.removeChild(iframe);

                if (!launched) {
                    // Protocol handler probably not installed, show download option
                    nativeNotInstalled.style.display = "block";
                }
            }, 1500);
        }

        function retryNativeViewer() {
            nativeNotInstalled.style.display = "none";
            launchNativeViewer();
        }

        function connectToMachine(sessionId) {
            const machine = machines.find(m => m.id === sessionId);
            connectSessionId = sessionId;
            connectMachineName = machine?.agent_name || sessionId.slice(0, 8);
            connectMachineNameEl.textContent = connectMachineName;
            nativeNotInstalled.style.display = "none";
            connectModal.classList.add("active");
        }

<!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>
        <button class="tab admin-only" data-tab="users" style="display: none;">Users</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.

User Management

Manage user accounts and permissions

                    Open User Management

Connect to Machine

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

🌐

Web Viewer

Opens in browser. Most keys work, but Win key and Alt+Tab are captured by your OS.

🖥

Native Viewer

Full keyboard capture including Win key, Alt+Tab, and Ctrl+Alt+Del.

                        Native viewer not installed

                        Download and run GuruConnect to register the protocol handler, then click "Try Again".

                        Download GuruConnect
                        <button class="btn btn-outline" onclick="retryNativeViewer()">Try Again</button>

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>

+        let protocolCheckTimeout = null;

-        const nativeCommandContainer = document.getElementById("nativeCommandContainer");

-        const nativeCommandText = document.getElementById("nativeCommandText");

+        const nativeNotInstalled = document.getElementById("nativeNotInstalled");

-            nativeCommandContainer.style.display = "none";

+            nativeNotInstalled.style.display = "none";

+            if (protocolCheckTimeout) clearTimeout(protocolCheckTimeout);

-                const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";

-                const serverUrl = protocol + "//" + window.location.host + "/ws/viewer";

-                const cmd = `guruconnect-viewer.exe -i ${connectSessionId} -s ${serverUrl}`;

-                nativeCommandText.textContent = cmd;

-                nativeCommandContainer.style.display = "block";

-            const cmd = nativeCommandText.textContent;

-            navigator.clipboard.writeText(cmd).then(() => {

-                const btn = document.querySelector(".copy-btn");

-                btn.textContent = "Copied!";

-                setTimeout(() => btn.textContent = "Copy", 2000);

+        function launchNativeViewer() {

+            if (!connectSessionId) return;

+            const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";

+            const serverUrl = encodeURIComponent(protocol + "//" + window.location.host + "/ws/viewer");

+            const protocolUrl = `guruconnect://view/${connectSessionId}?server=${serverUrl}`;

+            // Try to launch the protocol handler

+            // We use a hidden iframe to avoid navigation issues

+            const iframe = document.createElement("iframe");

+            iframe.style.display = "none";

+            document.body.appendChild(iframe);

+            // Set up timeout to detect if protocol handler didn't work

+            const startTime = Date.now();

+            // If window loses focus quickly, protocol likely worked

+            const blurHandler = () => {

+                if (Date.now() - startTime < 2000) {

+                    connectModal.classList.remove("active");

+            window.addEventListener("blur", blurHandler, { once: true });

+                iframe.contentWindow.location.href = protocolUrl;

+                // Some browsers throw on custom protocols

+            // Fallback: also try window.location for browsers that block iframe protocols

+            protocolCheckTimeout = setTimeout(() => {

+                window.removeEventListener("blur", blurHandler);

+                document.body.removeChild(iframe);

+                    // Protocol handler probably not installed, show download option

+                    nativeNotInstalled.style.display = "block";


Good. Now I need to push this to the server. Let me commit and push.


Now push to the server:


Bash

cd /c/Users/MikeSwanson/Claude/guru-connect && git add -A && git commit -m "Native viewer: use guruconnect:// protocol with fallback to download"

Commit protocol handler changes


[main 1b810a5] Native viewer: use guruconnect:// protocol with fallback to download
 1 file changed, 68 insertions(+), 27 deletions(-)


I also notice there was a viewer directory I tried to create that I should make sure is cleaned up. Let me check if it was removed and then push.


cd /c/Users/MikeSwanson/Claude/guru-connect && git push origin main && ssh guru@172.16.3.30 "cd ~/guru-connect && git pull origin main"

Push and pull to server


Updating 0c3435f..1b810a5
Fast-forward
 server/static/dashboard.html | 95 +++++++++++++++++++++++++++++++-------------
 1 file changed, 68 insertions(+), 27 deletions(-)
remote: . Processing 1 references
remote: Processed 1 references in total
To https://git.azcomputerguru.com/azcomputerguru/guru-connect.git
   0c3435f..1b810a5  main -> main
From https://git.azcomputerguru.com/azcomputerguru/guru-connect
 * branch            main       -> FETCH_HEAD
   0c3435f..1b810a5  main       -> origin/main


The code is pushed and pulled. Now when you click "Native Viewer":1. It tries to launch `guruconnect://view/SESSION_ID?server=wss://...`
2. If the protocol is registered, the native viewer opens3. If not, after 1.5 seconds it shows the "not installed" message with a download link and "Try Again" buttonHowever, I should note that the download link points to `/downloads/guruconnect.exe` which may not exist on the server yet. The user will need to build the agent/viewer and put it there.

Let me inform the user of what was done.


Done. The Native Viewer option now:

1. **Clicks "Native Viewer"** → Tries to launch `guruconnect://view/SESSION_ID?server=wss://connect.azcomputerguru.com/ws/viewer`

2. **If protocol registered** → Native viewer opens immediately, modal closes

3. **If not installed** (after 1.5s timeout) → Shows warning with:
   - "Download GuruConnect" button (links to `/downloads/guruconnect.exe`)
   - "Try Again" button to retry after installing

**To test:**
1. Build the agent: `cargo build -p guruconnect-agent --release`
2. Run `guruconnect.exe install` to register the protocol handler
3. Then the "Native Viewer" button will launch it directly

**Note:** You'll need to put the built `guruconnect.exe` at `/downloads/guruconnect.exe` on the server for the download link to work.


build the agent for windows


The user wants me to build the agent for Windows. I need to build it on a Windows machine or cross-compile. Let me check if I can build it on the current Windows machine or if I need to use the build server.

The current machine is Windows (C:\Users\MikeSwanson\Claude), so I should be able to build directly here. Let me check if Rust is installed and then build.


cd /c/Users/MikeSwanson/Claude/guru-connect && cargo build -p guruconnect-agent --release 2>&1

Build Windows agent


Error: Exit code 101
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package:   C:\Users\MikeSwanson\Claude\guru-connect\server\Cargo.toml
workspace: C:\Users\MikeSwanson\Claude\guru-connect\Cargo.toml
    Updating crates.io index
 Downloading crates ...
  Downloaded bytemuck_derive v1.10.2
  Downloaded softbuffer v0.4.8
error: package ID specification `guruconnect-agent` did not match any packages


The package name seems wrong. Let me check the Cargo.toml to see the correct package name.


     1→[package]
     2→name = "guruconnect"
     3→version = "0.1.0"
     4→edition = "2021"
     5→authors = ["AZ Computer Guru"]
     6→description = "GuruConnect Remote Desktop - Agent and Viewer"
     7→
     8→[dependencies]
     9→# CLI
    10→clap = { version = "4", features = ["derive"] }
    11→
    12→# Async runtime
    13→tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", "macros"] }
    14→
    15→# WebSocket
    16→tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
    17→futures-util = "0.3"
    18→
    19→# Windowing (for viewer)
    20→winit = { version = "0.30", features = ["rwh_06"] }
    21→softbuffer = "0.4"
    22→raw-window-handle = "0.6"
    23→
    24→# Compression
    25→zstd = "0.13"
    26→
    27→# Protocol (protobuf)
    28→prost = "0.13"
    29→prost-types = "0.13"
    30→bytes = "1"
    31→
    32→# Serialization
    33→serde = { version = "1", features = ["derive"] }
    34→serde_json = "1"
    35→
    36→# Logging
    37→tracing = "0.1"
    38→tracing-subscriber = { version = "0.3", features = ["env-filter"] }
    39→
    40→# Error handling
    41→anyhow = "1"
    42→thiserror = "1"
    43→
    44→# Configuration
    45→toml = "0.8"
    46→
    47→# Crypto
    48→ring = "0.17"
    49→
    50→# UUID
    51→uuid = { version = "1", features = ["v4", "serde"] }
    52→
    53→# Time
    54→chrono = { version = "0.4", features = ["serde"] }
    55→
    56→# Hostname
    57→hostname = "0.4"
    58→
    59→# URL encoding
    60→urlencoding = "2"
    61→
    62→# System tray (Windows)
    63→tray-icon = "0.19"
    64→muda = "0.15"  # Menu for tray icon
    65→
    66→# Image handling for tray icon
    67→image = { version = "0.25", default-features = false, features = ["png"] }
    68→
    69→# URL parsing
    70→url = "2"
    71→
    72→[target.'cfg(windows)'.dependencies]
    73→# Windows APIs for screen capture, input, and shell operations
    74→windows = { version = "0.58", features = [
    75→    "Win32_Foundation",
    76→    "Win32_Graphics_Gdi",
    77→    "Win32_Graphics_Dxgi",
    78→    "Win32_Graphics_Dxgi_Common",
    79→    "Win32_Graphics_Direct3D",
    80→    "Win32_Graphics_Direct3D11",
    81→    "Win32_UI_Input_KeyboardAndMouse",
    82→    "Win32_UI_WindowsAndMessaging",
    83→    "Win32_UI_Shell",
    84→    "Win32_System_LibraryLoader",
    85→    "Win32_System_Threading",
    86→    "Win32_System_Registry",
    87→    "Win32_System_Console",
    88→    "Win32_System_Environment",
    89→    "Win32_Security",
    90→    "Win32_Storage_FileSystem",
    91→    "Win32_System_Pipes",
    92→    "Win32_System_SystemServices",
    93→    "Win32_System_IO",
    94→]}
    95→
    96→# Windows service support
    97→windows-service = "0.7"
    98→
    99→[build-dependencies]
   100→prost-build = "0.13"
   101→winres = "0.1"
   102→
   103→[[bin]]
   104→name = "guruconnect"
   105→path = "src/main.rs"
   106→
   107→[[bin]]
   108→name = "guruconnect-sas-service"
   109→path = "src/bin/sas_service.rs"
   110→

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

[package]
name = "guruconnect"
version = "0.1.0"
edition = "2021"
authors = ["AZ Computer Guru"]
description = "GuruConnect Remote Desktop - Agent and Viewer"

[dependencies]
# CLI
clap = { version = "4", features = ["derive"] }

# Async runtime
tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", "macros"] }

# WebSocket
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
futures-util = "0.3"

# Windowing (for viewer)
winit = { version = "0.30", features = ["rwh_06"] }
softbuffer = "0.4"
raw-window-handle = "0.6"

# Compression
zstd = "0.13"

# Protocol (protobuf)
prost = "0.13"
prost-types = "0.13"
bytes = "1"

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Error handling
anyhow = "1"
thiserror = "1"

# Configuration
toml = "0.8"

# Crypto
ring = "0.17"

# UUID
uuid = { version = "1", features = ["v4", "serde"] }

# Time
chrono = { version = "0.4", features = ["serde"] }

# Hostname
hostname = "0.4"

# URL encoding
urlencoding = "2"

# System tray (Windows)
tray-icon = "0.19"
muda = "0.15"  # Menu for tray icon

# Image handling for tray icon
image = { version = "0.25", default-features = false, features = ["png"] }

# URL parsing
url = "2"

[target.'cfg(windows)'.dependencies]
# Windows APIs for screen capture, input, and shell operations
windows = { version = "0.58", features = [
    "Win32_Foundation",
    "Win32_Graphics_Gdi",
    "Win32_Graphics_Dxgi",
    "Win32_Graphics_Dxgi_Common",
    "Win32_Graphics_Direct3D",
    "Win32_Graphics_Direct3D11",
    "Win32_UI_Input_KeyboardAndMouse",
    "Win32_UI_WindowsAndMessaging",
    "Win32_UI_Shell",
    "Win32_System_LibraryLoader",
    "Win32_System_Threading",
    "Win32_System_Registry",
    "Win32_System_Console",
    "Win32_System_Environment",
    "Win32_Security",
    "Win32_Storage_FileSystem",
    "Win32_System_Pipes",
    "Win32_System_SystemServices",
    "Win32_System_IO",
]}

# Windows service support
windows-service = "0.7"

[build-dependencies]
prost-build = "0.13"
winres = "0.1"

[[bin]]
name = "guruconnect"
path = "src/main.rs"

[[bin]]
name = "guruconnect-sas-service"
path = "src/bin/sas_service.rs"


The package name is just `guruconnect`, not `guruconnect-agent`. Let me build it with the correct name.


cd /c/Users/MikeSwanson/Claude/guru-connect && cargo build -p guruconnect --release 2>&1

Build Windows agent with correct package name


warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package:   C:\Users\MikeSwanson\Claude\guru-connect\server\Cargo.toml
workspace: C:\Users\MikeSwanson\Claude\guru-connect\Cargo.toml
   Compiling smallvec v1.15.1
   Compiling windows-sys v0.61.2
   Compiling prost-derive v0.13.5
   Compiling utf8parse v0.2.2
   Compiling serde v1.0.228
   Compiling once_cell_polyfill v1.70.2
   Compiling anstyle v1.0.13
   Compiling tracing v0.1.44
   Compiling anstyle-parse v0.2.7
   Compiling colorchoice v1.0.4
   Compiling is_terminal_polyfill v1.70.2
   Compiling cfg_aliases v0.2.1
   Compiling parking_lot_core v0.9.12
   Compiling windows-implement v0.58.0
   Compiling windows-sys v0.52.0
   Compiling windows-interface v0.58.0
   Compiling raw-window-handle v0.6.2
   Compiling strsim v0.11.1
   Compiling percent-encoding v2.3.2
   Compiling clap_lex v0.7.6
   Compiling icu_normalizer v2.1.1
   Compiling futures-util v0.3.31
   Compiling clap_derive v4.5.49
   Compiling smol_str v0.2.2
   Compiling cursor-icon v1.2.0
   Compiling winit v0.30.12
   Compiling toml_datetime v0.6.3
   Compiling keyboard-types v0.7.0
   Compiling serde_spanned v0.6.9
   Compiling chrono v0.4.42
   Compiling parking_lot v0.12.5
   Compiling form_urlencoded v1.2.2
   Compiling toml_edit v0.20.2
   Compiling prost v0.13.5
   Compiling idna_adapter v1.2.1
   Compiling windows-core v0.58.0
   Compiling idna v1.1.0
   Compiling muda v0.15.3
   Compiling prost-types v0.13.5
   Compiling url v2.5.7
   Compiling toml v0.8.2
   Compiling windows v0.58.0
   Compiling prost-build v0.13.5
   Compiling windows-service v0.7.0
   Compiling tray-icon v0.19.3
   Compiling schannel v0.1.28
   Compiling mio v1.1.1
   Compiling anstyle-query v1.1.5
   Compiling anstyle-wincon v3.0.11
   Compiling nu-ansi-term v0.50.3
   Compiling softbuffer v0.4.8
   Compiling anstream v0.6.21
   Compiling guruconnect v0.1.0 (C:\Users\MikeSwanson\Claude\guru-connect\agent)
   Compiling native-tls v0.2.14
   Compiling tracing-subscriber v0.3.22
   Compiling tungstenite v0.24.0
   Compiling clap_builder v4.5.53
   Compiling tokio v1.48.0
   Compiling clap v4.5.53
   Compiling tokio-native-tls v0.3.1
   Compiling tokio-tungstenite v0.24.0
warning: unused import: `Write as IoWrite`
 --> agent\src\bin\sas_service.rs:7:21
  |
7 | use std::io::{Read, Write as IoWrite};
  |                     ^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default

warning: unused import: `Read`
 --> agent\src\bin\sas_service.rs:7:15
  |
7 | use std::io::{Read, Write as IoWrite};
  |               ^^^^

warning: structure field `nLength` should have a snake case name
  --> agent\src\bin\sas_service.rs:88:5
   |
88 |     nLength: u32,
   |     ^^^^^^^ help: convert the identifier to snake case: `n_length`
   |
   = note: `#[warn(non_snake_case)]` (part of `#[warn(nonstandard_style)]`) on by default

warning: structure field `lpSecurityDescriptor` should have a snake case name
  --> agent\src\bin\sas_service.rs:89:5
   |
89 |     lpSecurityDescriptor: *mut u8,
   |     ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `lp_security_descriptor`

warning: structure field `bInheritHandle` should have a snake case name
  --> agent\src\bin\sas_service.rs:90:5
   |
90 |     bInheritHandle: i32,
   |     ^^^^^^^^^^^^^^ help: convert the identifier to snake case: `b_inherit_handle`

warning: unused import: `DisplayInfo`
  --> agent\src\capture\mod.rs:12:28
   |
12 | pub use display::{Display, DisplayInfo};
   |                            ^^^^^^^^^^^
   |
   = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default

warning: unused imports: `error` and `warn`
 --> agent\src\chat\mod.rs:9:21
  |
9 | use tracing::{info, warn, error};
  |                     ^^^^  ^^^^^

warning: unused import: `windows::Win32::Foundation::*`
  --> agent\src\chat\mod.rs:14:5
   |
14 | use windows::Win32::Foundation::*;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: unused import: `windows::Win32::Graphics::Gdi::*`
  --> agent\src\chat\mod.rs:16:5
   |
16 | use windows::Win32::Graphics::Gdi::*;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: unused import: `windows::Win32::System::LibraryLoader::GetModuleHandleW`
  --> agent\src\chat\mod.rs:18:5
   |
18 | use windows::Win32::System::LibraryLoader::GetModuleHandleW;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: unused import: `std::ffi::OsStr`
   --> agent\src\chat\mod.rs:103:9
    |
103 |     use std::ffi::OsStr;
    |         ^^^^^^^^^^^^^^^

warning: unused import: `std::os::windows::ffi::OsStrExt`
   --> agent\src\chat\mod.rs:104:9
    |
104 |     use std::os::windows::ffi::OsStrExt;
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: unused imports: `DirtyRect as ProtoDirtyRect` and `RawFrame`
  --> agent\src\encoder\mod.rs:13:32
   |
13 | use crate::proto::{VideoFrame, RawFrame, DirtyRect as ProtoDirtyRect};
   |                                ^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: unused import: `KEYEVENTF_SCANCODE`
 --> agent\src\input\keyboard.rs:8:22
  |
8 |     KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, KEYEVENTF_UNICODE, KEYBDINPUT,
  |                      ^^^^^^^^^^^^^^^^^^

warning: unused import: `error`
 --> agent\src\install.rs:9:27
  |
9 | use tracing::{info, warn, error};
  |                           ^^^^^

warning: unused import: `std::time::Duration`
 --> agent\src\sas_client.rs:8:5
  |
8 | use std::time::Duration;
  |     ^^^^^^^^^^^^^^^^^^^

warning: unused import: `Display`
  --> agent\src\session\mod.rs:14:38
   |
14 | use crate::capture::{self, Capturer, Display};
   |                                      ^^^^^^^

warning: unused import: `error`
 --> agent\src\startup.rs:6:27
  |
6 | use tracing::{info, warn, error};
  |                           ^^^^^

warning: unused import: `Submenu`
 --> agent\src\tray\mod.rs:9:59
  |
9 | use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
  |                                                           ^^^^^^^

warning: unused import: `warn`
  --> agent\src\tray\mod.rs:13:21
   |
13 | use tracing::{info, warn};
   |                     ^^^^

warning: unused import: `GetMessageW`
  --> agent\src\viewer\input.rs:15:43
   |
15 |         CallNextHookEx, DispatchMessageW, GetMessageW, PeekMessageW, SetWindowsHookExW,
   |                                           ^^^^^^^^^^^

warning: `guruconnect` (bin "guruconnect-sas-service") generated 5 warnings (run `cargo fix --bin "guruconnect-sas-service" -p guruconnect` to apply 1 suggestion)
warning: variable does not need to be mutable
  --> agent\src\capture\display.rs:63:9
   |
63 |     let mut display_id = 0u32;
   |         ----^^^^^^^^^^
   |         |
   |         help: remove this `mut`
   |
   = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default

warning: unused variable: `display_id`
  --> agent\src\capture\display.rs:63:9
   |
63 |     let mut display_id = 0u32;
   |         ^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_display_id`
   |
   = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: unused variable: `message_tx`
   --> agent\src\chat\mod.rs:102:55
    |
102 | fn run_chat_window(command_rx: Receiver<ChatCommand>, message_tx: Sender<ChatMessage>) {
    |                                                       ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_message_tx`

warning: unused variable: `data`
   --> agent\src\transport\websocket.rs:171:29
    |
171 |             WsMessage::Ping(data) => {
    |                             ^^^^ help: if this is intentional, prefix it with an underscore: `_data`

warning: field `dirty_rects` is never read
  --> agent\src\capture\mod.rs:36:9
   |
19 | pub struct CapturedFrame {
   |            ------------- field in this struct
...
36 |     pub dirty_rects: Option<Vec<DirtyRect>>,
   |         ^^^^^^^^^^^
   |
   = note: `CapturedFrame` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis
   = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

warning: methods `display` and `is_valid` are never used
  --> agent\src\capture\mod.rs:56:8
   |
49 | pub trait Capturer: Send {
   |           -------- methods in this trait
...
56 |     fn display(&self) -> &Display;
   |        ^^^^^^^
...
59 |     fn is_valid(&self) -> bool;
   |        ^^^^^^^^

warning: field `last_frame` is never read
  --> agent\src\capture\dxgi.rs:36:5
   |
28 | pub struct DxgiCapturer {
   |            ------------ field in this struct
...
36 |     last_frame: Option<Vec<u8>>,
   |     ^^^^^^^^^^

warning: struct `DisplayInfo` is never constructed
  --> agent\src\capture\display.rs:36:12
   |
36 | pub struct DisplayInfo {
   |            ^^^^^^^^^^^

warning: methods `pixel_count` and `buffer_size` are never used
  --> agent\src\capture\display.rs:43:12
   |
41 | impl Display {
   | ------------ methods in this implementation
42 |     /// Total pixels in the display
43 |     pub fn pixel_count(&self) -> u32 {
   |            ^^^^^^^^^^^
...
48 |     pub fn buffer_size(&self) -> usize {
   |            ^^^^^^^^^^^

warning: function `get_display_info` is never used
   --> agent\src\capture\display.rs:147:8
    |
147 | pub fn get_display_info() -> Result<DisplayInfo> {
    |        ^^^^^^^^^^^^^^^^

warning: variants `Show`, `Hide`, and `Close` are never constructed
  --> agent\src\chat\mod.rs:34:5
   |
33 | pub enum ChatCommand {
   |          ----------- variants in this enum
34 |     Show,
   |     ^^^^
35 |     Hide,
   |     ^^^^
36 |     AddMessage(ChatMessage),
37 |     Close,
   |     ^^^^^
   |
   = note: `ChatCommand` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis

warning: methods `show`, `hide`, and `close` are never used
  --> agent\src\chat\mod.rs:72:12
   |
47 | impl ChatController {
   | ------------------- methods in this implementation
...
72 |     pub fn show(&self) {
   |            ^^^^
...
77 |     pub fn hide(&self) {
   |            ^^^^
...
96 |     pub fn close(&self) {
   |            ^^^^^

warning: function `example_config` is never used
   --> agent\src\config.rs:211:8
    |
211 | pub fn example_config() -> &'static str {
    |        ^^^^^^^^^^^^^^

warning: field `is_keyframe` is never read
  --> agent\src\encoder\mod.rs:26:9
   |
18 | pub struct EncodedFrame {
   |            ------------ field in this struct
...
26 |     pub is_keyframe: bool,
   |         ^^^^^^^^^^^
   |
   = note: `EncodedFrame` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis

warning: methods `request_keyframe` and `name` are never used
  --> agent\src\encoder\mod.rs:35:8
   |
30 | pub trait Encoder: Send {
   |           ------- methods in this trait
...
35 |     fn request_keyframe(&mut self);
   |        ^^^^^^^^^^^^^^^^
...
38 |     fn name(&self) -> &str;
   |        ^^^^

warning: methods `mouse`, `keyboard`, and `type_unicode` are never used
  --> agent\src\input\mod.rs:29:12
   |
19 | impl InputController {
   | -------------------- methods in this implementation
...
29 |     pub fn mouse(&mut self) -> &mut MouseController {
   |            ^^^^^
...
34 |     pub fn keyboard(&mut self) -> &mut KeyboardController {
   |            ^^^^^^^^
...
67 |     pub fn type_unicode(&mut self, ch: char) -> Result<()> {
   |            ^^^^^^^^^^^^

warning: variants `X1` and `X2` are never constructed
  --> agent\src\input\mod.rs:83:5
   |
79 | pub enum MouseButton {
   |          ----------- variants in this enum
...
83 |     X1,
   |     ^^
84 |     X2,
   |     ^^
   |
   = note: `MouseButton` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis

warning: fields `ctrl`, `alt`, `shift`, and `meta` are never read
  --> agent\src\input\keyboard.rs:21:5
   |
20 | struct ModifierState {
   |        ------------- fields in this struct
21 |     ctrl: bool,
   |     ^^^^
22 |     alt: bool,
   |     ^^^
23 |     shift: bool,
   |     ^^^^^
24 |     meta: bool,
   |     ^^^^

warning: methods `type_char` and `type_string` are never used
   --> agent\src\input\keyboard.rs:82:12
    |
 27 | impl KeyboardController {
    | ----------------------- methods in this implementation
...
 82 |     pub fn type_char(&mut self, ch: char) -> Result<()> {
    |            ^^^^^^^^^
...
123 |     pub fn type_string(&mut self, text: &str) -> Result<()> {
    |            ^^^^^^^^^^^

warning: constant `TIMEOUT_MS` is never used
  --> agent\src\sas_client.rs:14:7
   |
14 | const TIMEOUT_MS: u64 = 5000;
   |       ^^^^^^^^^^

warning: function `is_service_available` is never used
  --> agent\src\sas_client.rs:68:8
   |
68 | pub fn is_service_available() -> bool {
   |        ^^^^^^^^^^^^^^^^^^^^

warning: function `get_service_status` is never used
  --> agent\src\sas_client.rs:88:8
   |
88 | pub fn get_service_status() -> String {
   |        ^^^^^^^^^^^^^^^^^^

warning: function `install_sas_service` is never used
   --> agent\src\startup.rs:183:8
    |
183 | pub fn install_sas_service() -> Result<()> {
    |        ^^^^^^^^^^^^^^^^^^^

warning: function `uninstall_sas_service` is never used
   --> agent\src\startup.rs:230:8
    |
230 | pub fn uninstall_sas_service() -> Result<()> {
    |        ^^^^^^^^^^^^^^^^^^^^^

warning: function `check_sas_service` is never used
   --> agent\src\startup.rs:262:8
    |
262 | pub fn check_sas_service() -> bool {
    |        ^^^^^^^^^^^^^^^^^

warning: methods `recv` and `close` are never used
   --> agent\src\transport\websocket.rs:139:18
    |
 30 | impl WebSocketTransport {
    | ----------------------- methods in this implementation
...
139 |     pub async fn recv(&mut self) -> Result<Option<Message>> {
    |                  ^^^^
...
200 |     pub async fn close(&mut self) -> Result<()> {
    |                  ^^^^^

warning: field `menu` is never read
  --> agent\src\tray\mod.rs:31:5
   |
29 | pub struct TrayController {
   |            -------------- field in this struct
30 |     _tray_icon: TrayIcon,
31 |     menu: Menu,
   |     ^^^^

warning: variant `SpecialKey` is never constructed
  --> agent\src\viewer\mod.rs:29:5
   |
26 | pub enum InputEvent {
   |          ---------- variant in this enum
...
29 |     SpecialKey(proto::SpecialKeyEvent),
   |     ^^^^^^^^^^
   |
   = note: `InputEvent` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis

warning: constant `VK_LSHIFT` is never used
  --> agent\src\viewer\input.rs:36:15
   |
36 |     pub const VK_LSHIFT: u32 = 0xA0;
   |               ^^^^^^^^^

warning: constant `VK_RSHIFT` is never used
  --> agent\src\viewer\input.rs:37:15
   |
37 |     pub const VK_RSHIFT: u32 = 0xA1;
   |               ^^^^^^^^^

warning: constant `VK_LCONTROL` is never used
  --> agent\src\viewer\input.rs:38:15
   |
38 |     pub const VK_LCONTROL: u32 = 0xA2;
   |               ^^^^^^^^^^^

warning: constant `VK_RCONTROL` is never used
  --> agent\src\viewer\input.rs:39:15
   |
39 |     pub const VK_RCONTROL: u32 = 0xA3;
   |               ^^^^^^^^^^^

warning: constant `VK_LMENU` is never used
  --> agent\src\viewer\input.rs:40:15
   |
40 |     pub const VK_LMENU: u32 = 0xA4;  // Left Alt
   |               ^^^^^^^^

warning: constant `VK_RMENU` is never used
  --> agent\src\viewer\input.rs:41:15
   |
41 |     pub const VK_RMENU: u32 = 0xA5;  // Right Alt
   |               ^^^^^^^^

warning: constant `VK_TAB` is never used
  --> agent\src\viewer\input.rs:42:15
   |
42 |     pub const VK_TAB: u32 = 0x09;
   |               ^^^^^^

warning: constant `VK_ESCAPE` is never used
  --> agent\src\viewer\input.rs:43:15
   |
43 |     pub const VK_ESCAPE: u32 = 0x1B;
   |               ^^^^^^^^^

warning: constant `VK_SNAPSHOT` is never used
  --> agent\src\viewer\input.rs:44:15
   |
44 |     pub const VK_SNAPSHOT: u32 = 0x2C;  // Print Screen
   |               ^^^^^^^^^^^

warning: field `is_keyframe` is never read
  --> agent\src\viewer\render.rs:28:9
   |
23 | pub struct FrameData {
   |            --------- field in this struct
...
28 |     pub is_keyframe: bool,
   |         ^^^^^^^^^^^
   |
   = note: `FrameData` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis

warning: unused `BOOL` that must be used
  --> agent\src\capture\gdi.rs:52:17
   |
52 |                 DeleteDC(mem_dc);
   |                 ^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
   |
52 |                 let _ = DeleteDC(mem_dc);
   |                 +++++++

warning: unused `BOOL` that must be used
  --> agent\src\capture\gdi.rs:73:17
   |
73 |                 DeleteObject(bitmap);
   |                 ^^^^^^^^^^^^^^^^^^^^
   |
help: use `let _ = ...` to ignore the resulting value
   |
73 |                 let _ = DeleteObject(bitmap);
   |                 +++++++

warning: unused `BOOL` that must be used
  --> agent\src\capture\gdi.rs:74:17
   |
74 |                 DeleteDC(mem_dc);
   |                 ^^^^^^^^^^^^^^^^
   |
help: use `let _ = ...` to ignore the resulting value
   |
74 |                 let _ = DeleteDC(mem_dc);
   |                 +++++++

warning: unused `BOOL` that must be used
   --> agent\src\capture\gdi.rs:114:13
    |
114 |             DeleteObject(bitmap);
    |             ^^^^^^^^^^^^^^^^^^^^
    |
help: use `let _ = ...` to ignore the resulting value
    |
114 |             let _ = DeleteObject(bitmap);
    |             +++++++

warning: unused `BOOL` that must be used
   --> agent\src\capture\gdi.rs:115:13
    |
115 |             DeleteDC(mem_dc);
    |             ^^^^^^^^^^^^^^^^
    |
help: use `let _ = ...` to ignore the resulting value
    |
115 |             let _ = DeleteDC(mem_dc);
    |             +++++++

warning: `guruconnect` (bin "guruconnect") generated 59 warnings (run `cargo fix --bin "guruconnect" -p guruconnect` to apply 20 suggestions)
    Finished `release` profile [optimized] target(s) in 33.14s


Build succeeded with some warnings. Let me check where the binary is and copy it to the server's downloads folder.


Build successful. Now let me copy it to the server's downloads folder:


Check built binary


Error: Exit code 2
ls: cannot access '/c/Users/MikeSwanson/Claude/guru-connect/target/release/guruconnect.exe': No such file or directory


The binary might be in a different location since we're using a workspace. Let me find it.


find /c/Users/MikeSwanson/Claude/guru-connect/target -name "guruconnect.exe" -type f 2>/dev/null

Find built binary


/c/Users/MikeSwanson/Claude/guru-connect/target/x86_64-pc-windows-msvc/release/deps/guruconnect.exe
/c/Users/MikeSwanson/Claude/guru-connect/target/x86_64-pc-windows-msvc/release/guruconnect.exe


Found it in the Windows target directory. Now I need to copy it to the server's downloads folder. Let me create the downloads directory on the server and copy the file.


ssh guru@172.16.3.30 "mkdir -p ~/guru-connect/server/static/downloads"

Create downloads directory on server


Copy binary to server downloads


The binary has been copied to the server. Let me verify it's there and check the file size.


ssh guru@172.16.3.30 "ls -lh ~/guru-connect/server/static/downloads/"

Verify binary on server


total 4.3M
-rwxr-xr-x 1 guru guru 4.3M Dec 30 04:49 guruconnect.exe


The binary is uploaded. Now when users click "Download GuruConnect" in the dashboard, they'll get the Windows viewer/agent binary. After running it with `guruconnect install`, the protocol handler will be registered and the "Native Viewer" option will work.

Let me verify the static files are being served correctly by the server.


Binary uploaded (4.3MB). Let me verify it's accessible:


curl -s -I "http://172.16.3.30:3002/downloads/guruconnect.exe" | head -5

Test download URL


HTTP/1.1 200 OK
content-type: application/octet-stream
accept-ranges: bytes
last-modified: Tue, 30 Dec 2025 04:49:08 GMT
content-length: 4423680


The download URL is working. Now the full flow is:

1. User clicks "Native Viewer" in dashboard
2. Dashboard tries to launch `guruconnect://view/SESSION_ID?server=...`
3. If protocol handler is registered, native viewer launches
4. If not, after 1.5s shows "not installed" with download link
5. User downloads guruconnect.exe from `/downloads/guruconnect.exe`
6. User runs `guruconnect.exe install` to register protocol handler
7. User clicks "Try Again" - now it works

The user can now test this flow.


Done! The flow is now:

1. **Click "Native Viewer"** → Tries `guruconnect://view/SESSION_ID?server=wss://connect.azcomputerguru.com/ws/viewer`

2. **If not installed** → Shows download prompt with link to `/downloads/guruconnect.exe` (4.3MB)

3. **User downloads and runs** `guruconnect.exe install` → Registers protocol handler

4. **Click "Try Again"** → Native viewer launches

You can test by:
1. Going to https://connect.azcomputerguru.com/dashboard
2. Click Connect on a machine → Native Viewer
3. Download guruconnect.exe
4. Run `guruconnect.exe install` (will prompt for UAC)
5. Try again - should launch the native viewer


The web launcher should pass the "install" option.  It does not.


The user is saying that when the user downloads and runs guruconnect.exe from the web, it should automatically run with the "install" option to register the protocol handler. Currently it just downloads the raw exe which would run in agent mode by default.

I need to either:
1. Create a batch file or installer that runs `guruconnect.exe install`
2. Or make the download link point to a script/installer

Actually, the simpler solution is to make the exe detect that it was launched without arguments and the protocol handler isn't registered, then prompt to install. But that's more complex.

The easiest solution is to provide a batch file or a separate "installer" download that runs the install command. Or we could update the dashboard to tell the user to run `guruconnect.exe install` after downloading.

Actually, looking at this more carefully - the user wants the download to be something that when double-clicked, runs the install. We have a few options:

1. Create a small batch/cmd wrapper
2. Create an actual installer (MSI, NSIS, etc.)
3. Have the exe auto-detect first run and offer to install

Option 3 is probably best for UX. Let me modify main.rs so that when run without arguments and the protocol handler isn't registered, it offers to install.

Actually, looking at the current code in main.rs:
- No args → runs agent mode
- `install` → installs and registers protocol

We could change the behavior so that if running without args AND not already installed, it prompts/auto-installs. Let me update the main.rs to do this.


You're right. Let me update the agent so when run without arguments (double-clicked from downloads), it automatically runs install mode:


Edit

        None => {
            // Legacy mode: if a support code was provided, run as agent
            if let Some(code) = cli.support_code {
                run_agent_mode(Some(code))
            } else {
                // No args: check if we should auto-detect mode
                // For now, default to agent mode
                run_agent_mode(None)
            }
        }

        None => {
            // Legacy mode: if a support code was provided, run as agent
            if let Some(code) = cli.support_code {
                run_agent_mode(Some(code))
            } else {
                // No args: check if protocol handler is installed
                // If not, run install mode (user likely downloaded from web)
                if !install::is_protocol_handler_registered() {
                    info!("Protocol handler not registered, running installer");
                    run_install(false)
                } else {
                    // Protocol handler exists, run as agent
                    run_agent_mode(None)
                }
            }
        }