     1→//! Support session codes management
     2→//!
     3→//! Handles generation and validation of 6-digit support codes
     4→//! for one-time remote support sessions.
     5→
     6→use std::collections::HashMap;
     7→use std::sync::Arc;
     8→use tokio::sync::RwLock;
     9→use chrono::{DateTime, Utc};
    10→use rand::Rng;
    11→use serde::{Deserialize, Serialize};
    12→use uuid::Uuid;
    13→
    14→/// A support session code
    15→#[derive(Debug, Clone, Serialize)]
    16→pub struct SupportCode {
    17→    pub code: String,
    18→    pub session_id: Uuid,
    19→    pub created_by: String,
    20→    pub created_at: DateTime<Utc>,
    21→    pub status: CodeStatus,
    22→    pub client_name: Option<String>,
    23→    pub client_machine: Option<String>,
    24→    pub connected_at: Option<DateTime<Utc>>,
    25→}
    26→
    27→#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
    28→#[serde(rename_all = "lowercase")]
    29→pub enum CodeStatus {
    30→    Pending,    // Waiting for client to connect
    31→    Connected,  // Client connected, session active
    32→    Completed,  // Session ended normally
    33→    Cancelled,  // Code cancelled by tech
    34→}
    35→
    36→/// Request to create a new support code
    37→#[derive(Debug, Deserialize)]
    38→pub struct CreateCodeRequest {
    39→    pub technician_id: Option<String>,
    40→    pub technician_name: Option<String>,
    41→}
    42→
    43→/// Response when a code is validated
    44→#[derive(Debug, Serialize)]
    45→pub struct CodeValidation {
    46→    pub valid: bool,
    47→    pub session_id: Option<String>,
    48→    pub server_url: Option<String>,
    49→    #[serde(skip_serializing_if = "Option::is_none")]
    50→    pub error: Option<String>,
    51→}
    52→
    53→/// Manages support codes
    54→#[derive(Clone)]
    55→pub struct SupportCodeManager {
    56→    codes: Arc<RwLock<HashMap<String, SupportCode>>>,
    57→    session_to_code: Arc<RwLock<HashMap<Uuid, String>>>,
    58→}
    59→
    60→impl SupportCodeManager {
    61→    pub fn new() -> Self {
    62→        Self {
    63→            codes: Arc::new(RwLock::new(HashMap::new())),
    64→            session_to_code: Arc::new(RwLock::new(HashMap::new())),
    65→        }
    66→    }
    67→
    68→    /// Generate a unique 6-digit code
    69→    async fn generate_unique_code(&self) -> String {
    70→        let codes = self.codes.read().await;
    71→        let mut rng = rand::thread_rng();
    72→
    73→        loop {
    74→            let code: u32 = rng.gen_range(100000..999999);
    75→            let code_str = code.to_string();
    76→
    77→            if !codes.contains_key(&code_str) {
    78→                return code_str;
    79→            }
    80→        }
    81→    }
    82→
    83→    /// Create a new support code
    84→    pub async fn create_code(&self, request: CreateCodeRequest) -> SupportCode {
    85→        let code = self.generate_unique_code().await;
    86→        let session_id = Uuid::new_v4();
    87→
    88→        let support_code = SupportCode {
    89→            code: code.clone(),
    90→            session_id,
    91→            created_by: request.technician_name.unwrap_or_else(|| "Unknown".to_string()),
    92→            created_at: Utc::now(),
    93→            status: CodeStatus::Pending,
    94→            client_name: None,
    95→            client_machine: None,
    96→            connected_at: None,
    97→        };
    98→
    99→        let mut codes = self.codes.write().await;
   100→        codes.insert(code.clone(), support_code.clone());
   101→
   102→        let mut session_to_code = self.session_to_code.write().await;
   103→        session_to_code.insert(session_id, code);
   104→
   105→        support_code
   106→    }
   107→
   108→    /// Validate a code and return session info
   109→    pub async fn validate_code(&self, code: &str) -> CodeValidation {
   110→        let codes = self.codes.read().await;
   111→
   112→        match codes.get(code) {
   113→            Some(support_code) => {
   114→                if support_code.status == CodeStatus::Pending || support_code.status == CodeStatus::Connected {
   115→                    CodeValidation {
   116→                        valid: true,
   117→                        session_id: Some(support_code.session_id.to_string()),
   118→                        server_url: Some("wss://connect.azcomputerguru.com/ws/support".to_string()),
   119→                        error: None,
   120→                    }
   121→                } else {
   122→                    CodeValidation {
   123→                        valid: false,
   124→                        session_id: None,
   125→                        server_url: None,
   126→                        error: Some("This code has expired or been used".to_string()),
   127→                    }
   128→                }
   129→            }
   130→            None => CodeValidation {
   131→                valid: false,
   132→                session_id: None,
   133→                server_url: None,
   134→                error: Some("Invalid code".to_string()),
   135→            },
   136→        }
   137→    }
   138→
   139→    /// Mark a code as connected
   140→    pub async fn mark_connected(&self, code: &str, client_name: Option<String>, client_machine: Option<String>) {
   141→        let mut codes = self.codes.write().await;
   142→        if let Some(support_code) = codes.get_mut(code) {
   143→            support_code.status = CodeStatus::Connected;
   144→            support_code.client_name = client_name;
   145→            support_code.client_machine = client_machine;
   146→            support_code.connected_at = Some(Utc::now());
   147→        }
   148→    }
   149→
   150→    /// Link a support code to an actual WebSocket session
   151→    pub async fn link_session(&self, code: &str, real_session_id: Uuid) {
   152→        let mut codes = self.codes.write().await;
   153→        if let Some(support_code) = codes.get_mut(code) {
   154→            // Update session_to_code mapping with real session ID
   155→            let old_session_id = support_code.session_id;
   156→            support_code.session_id = real_session_id;
   157→
   158→            // Update the reverse mapping
   159→            let mut session_to_code = self.session_to_code.write().await;
   160→            session_to_code.remove(&old_session_id);
   161→            session_to_code.insert(real_session_id, code.to_string());
   162→        }
   163→    }
   164→
   165→    /// Get code by its code string
   166→    pub async fn get_code(&self, code: &str) -> Option<SupportCode> {
   167→        let codes = self.codes.read().await;
   168→        codes.get(code).cloned()
   169→    }
   170→
   171→    /// Mark a code as completed
   172→    pub async fn mark_completed(&self, code: &str) {
   173→        let mut codes = self.codes.write().await;
   174→        if let Some(support_code) = codes.get_mut(code) {
   175→            support_code.status = CodeStatus::Completed;
   176→        }
   177→    }
   178→
   179→    /// Cancel a code (works for both pending and connected)
   180→    pub async fn cancel_code(&self, code: &str) -> bool {
   181→        let mut codes = self.codes.write().await;
   182→        if let Some(support_code) = codes.get_mut(code) {
   183→            if support_code.status == CodeStatus::Pending || support_code.status == CodeStatus::Connected {
   184→                support_code.status = CodeStatus::Cancelled;
   185→                return true;
   186→            }
   187→        }
   188→        false
   189→    }
   190→
   191→    /// Check if a code is cancelled
   192→    pub async fn is_cancelled(&self, code: &str) -> bool {
   193→        let codes = self.codes.read().await;
   194→        codes.get(code).map(|c| c.status == CodeStatus::Cancelled).unwrap_or(false)
   195→    }
   196→
   197→    /// Check if a code is valid for connection (exists and is pending)
   198→    pub async fn is_valid_for_connection(&self, code: &str) -> bool {
   199→        let codes = self.codes.read().await;
   200→        codes.get(code).map(|c| c.status == CodeStatus::Pending).unwrap_or(false)
   201→    }
   202→
   203→    /// List all codes (for dashboard)
   204→    pub async fn list_codes(&self) -> Vec<SupportCode> {
   205→        let codes = self.codes.read().await;
   206→        codes.values().cloned().collect()
   207→    }
   208→
   209→    /// List active codes only
   210→    pub async fn list_active_codes(&self) -> Vec<SupportCode> {
   211→        let codes = self.codes.read().await;
   212→        codes.values()
   213→            .filter(|c| c.status == CodeStatus::Pending || c.status == CodeStatus::Connected)
   214→            .cloned()
   215→            .collect()
   216→    }
   217→
   218→    /// Get code by session ID
   219→    pub async fn get_by_session(&self, session_id: Uuid) -> Option<SupportCode> {
   220→        let session_to_code = self.session_to_code.read().await;
   221→        let code = session_to_code.get(&session_id)?;
   222→
   223→        let codes = self.codes.read().await;
   224→        codes.get(code).cloned()
   225→    }
   226→}
   227→
   228→impl Default for SupportCodeManager {
   229→    fn default() -> Self {
   230→        Self::new()
   231→    }
   232→}
   233→

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

//! Support session codes management
//!
//! Handles generation and validation of 6-digit support codes
//! for one-time remote support sessions.

use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use chrono::{DateTime, Utc};
use rand::Rng;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// A support session code
#[derive(Debug, Clone, Serialize)]
pub struct SupportCode {
    pub code: String,
    pub session_id: Uuid,
    pub created_by: String,
    pub created_at: DateTime<Utc>,
    pub status: CodeStatus,
    pub client_name: Option<String>,
    pub client_machine: Option<String>,
    pub connected_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CodeStatus {
    Pending,    // Waiting for client to connect
    Connected,  // Client connected, session active
    Completed,  // Session ended normally
    Cancelled,  // Code cancelled by tech
}

/// Request to create a new support code
#[derive(Debug, Deserialize)]
pub struct CreateCodeRequest {
    pub technician_id: Option<String>,
    pub technician_name: Option<String>,
}

/// Response when a code is validated
#[derive(Debug, Serialize)]
pub struct CodeValidation {
    pub valid: bool,
    pub session_id: Option<String>,
    pub server_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// Manages support codes
#[derive(Clone)]
pub struct SupportCodeManager {
    codes: Arc<RwLock<HashMap<String, SupportCode>>>,
    session_to_code: Arc<RwLock<HashMap<Uuid, String>>>,
}

impl SupportCodeManager {
    pub fn new() -> Self {
        Self {
            codes: Arc::new(RwLock::new(HashMap::new())),
            session_to_code: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    /// Generate a unique 6-digit code
    async fn generate_unique_code(&self) -> String {
        let codes = self.codes.read().await;
        let mut rng = rand::thread_rng();

        loop {
            let code: u32 = rng.gen_range(100000..999999);
            let code_str = code.to_string();

            if !codes.contains_key(&code_str) {
                return code_str;
            }
        }
    }

    /// Create a new support code
    pub async fn create_code(&self, request: CreateCodeRequest) -> SupportCode {
        let code = self.generate_unique_code().await;
        let session_id = Uuid::new_v4();

        let support_code = SupportCode {
            code: code.clone(),
            session_id,
            created_by: request.technician_name.unwrap_or_else(|| "Unknown".to_string()),
            created_at: Utc::now(),
            status: CodeStatus::Pending,
            client_name: None,
            client_machine: None,
            connected_at: None,
        };

        let mut codes = self.codes.write().await;
        codes.insert(code.clone(), support_code.clone());

        let mut session_to_code = self.session_to_code.write().await;
        session_to_code.insert(session_id, code);

        support_code
    }

    /// Validate a code and return session info
    pub async fn validate_code(&self, code: &str) -> CodeValidation {
        let codes = self.codes.read().await;

        match codes.get(code) {
            Some(support_code) => {
                if support_code.status == CodeStatus::Pending || support_code.status == CodeStatus::Connected {
                    CodeValidation {
                        valid: true,
                        session_id: Some(support_code.session_id.to_string()),
                        server_url: Some("wss://connect.azcomputerguru.com/ws/support".to_string()),
                        error: None,
                    }
                } else {
                    CodeValidation {
                        valid: false,
                        session_id: None,
                        server_url: None,
                        error: Some("This code has expired or been used".to_string()),
                    }
                }
            }
            None => CodeValidation {
                valid: false,
                session_id: None,
                server_url: None,
                error: Some("Invalid code".to_string()),
            },
        }
    }

    /// Mark a code as connected
    pub async fn mark_connected(&self, code: &str, client_name: Option<String>, client_machine: Option<String>) {
        let mut codes = self.codes.write().await;
        if let Some(support_code) = codes.get_mut(code) {
            support_code.status = CodeStatus::Connected;
            support_code.client_name = client_name;
            support_code.client_machine = client_machine;
            support_code.connected_at = Some(Utc::now());
        }
    }

    /// Link a support code to an actual WebSocket session
    pub async fn link_session(&self, code: &str, real_session_id: Uuid) {
        let mut codes = self.codes.write().await;
        if let Some(support_code) = codes.get_mut(code) {
            // Update session_to_code mapping with real session ID
            let old_session_id = support_code.session_id;
            support_code.session_id = real_session_id;

            // Update the reverse mapping
            let mut session_to_code = self.session_to_code.write().await;
            session_to_code.remove(&old_session_id);
            session_to_code.insert(real_session_id, code.to_string());
        }
    }

    /// Get code by its code string
    pub async fn get_code(&self, code: &str) -> Option<SupportCode> {
        let codes = self.codes.read().await;
        codes.get(code).cloned()
    }

    /// Mark a code as completed
    pub async fn mark_completed(&self, code: &str) {
        let mut codes = self.codes.write().await;
        if let Some(support_code) = codes.get_mut(code) {
            support_code.status = CodeStatus::Completed;
        }
    }

    /// Cancel a code (works for both pending and connected)
    pub async fn cancel_code(&self, code: &str) -> bool {
        let mut codes = self.codes.write().await;
        if let Some(support_code) = codes.get_mut(code) {
            if support_code.status == CodeStatus::Pending || support_code.status == CodeStatus::Connected {
                support_code.status = CodeStatus::Cancelled;
                return true;
            }
        }
        false
    }

    /// Check if a code is cancelled
    pub async fn is_cancelled(&self, code: &str) -> bool {
        let codes = self.codes.read().await;
        codes.get(code).map(|c| c.status == CodeStatus::Cancelled).unwrap_or(false)
    }

    /// Check if a code is valid for connection (exists and is pending)
    pub async fn is_valid_for_connection(&self, code: &str) -> bool {
        let codes = self.codes.read().await;
        codes.get(code).map(|c| c.status == CodeStatus::Pending).unwrap_or(false)
    }

    /// List all codes (for dashboard)
    pub async fn list_codes(&self) -> Vec<SupportCode> {
        let codes = self.codes.read().await;
        codes.values().cloned().collect()
    }

    /// List active codes only
    pub async fn list_active_codes(&self) -> Vec<SupportCode> {
        let codes = self.codes.read().await;
        codes.values()
            .filter(|c| c.status == CodeStatus::Pending || c.status == CodeStatus::Connected)
            .cloned()
            .collect()
    }

    /// Get code by session ID
    pub async fn get_by_session(&self, session_id: Uuid) -> Option<SupportCode> {
        let session_to_code = self.session_to_code.read().await;
        let code = session_to_code.get(&session_id)?;

        let codes = self.codes.read().await;
        codes.get(code).cloned()
    }
}

impl Default for SupportCodeManager {
    fn default() -> Self {
        Self::new()
    }
}


Now let me look at the GuruRMM database patterns for reference:


     1→//! Server configuration
     2→
     3→use anyhow::Result;
     4→use serde::Deserialize;
     5→use std::env;
     6→
     7→#[derive(Debug, Clone, Deserialize)]
     8→pub struct Config {
     9→    /// Address to listen on (e.g., "0.0.0.0:8080")
    10→    pub listen_addr: String,
    11→
    12→    /// Database URL (optional for MVP)
    13→    pub database_url: Option<String>,
    14→
    15→    /// JWT secret for authentication
    16→    pub jwt_secret: Option<String>,
    17→
    18→    /// Enable debug logging
    19→    pub debug: bool,
    20→}
    21→
    22→impl Config {
    23→    /// Load configuration from environment variables
    24→    pub fn load() -> Result<Self> {
    25→        Ok(Self {
    26→            listen_addr: env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:8080".to_string()),
    27→            database_url: env::var("DATABASE_URL").ok(),
    28→            jwt_secret: env::var("JWT_SECRET").ok(),
    29→            debug: env::var("DEBUG")
    30→                .map(|v| v == "1" || v.to_lowercase() == "true")
    31→                .unwrap_or(false),
    32→        })
    33→    }
    34→}
    35→
    36→impl Default for Config {
    37→    fn default() -> Self {
    38→        Self {
    39→            listen_addr: "0.0.0.0:8080".to_string(),
    40→            database_url: None,
    41→            jwt_secret: None,
    42→            debug: false,
    43→        }
    44→    }
    45→}
    46→

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

//! Server configuration

use anyhow::Result;
use serde::Deserialize;
use std::env;

#[derive(Debug, Clone, Deserialize)]
pub struct Config {
    /// Address to listen on (e.g., "0.0.0.0:8080")
    pub listen_addr: String,

    /// Database URL (optional for MVP)
    pub database_url: Option<String>,

    /// JWT secret for authentication
    pub jwt_secret: Option<String>,

    /// Enable debug logging
    pub debug: bool,
}

impl Config {
    /// Load configuration from environment variables
    pub fn load() -> Result<Self> {
        Ok(Self {
            listen_addr: env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:8080".to_string()),
            database_url: env::var("DATABASE_URL").ok(),
            jwt_secret: env::var("JWT_SECRET").ok(),
            debug: env::var("DEBUG")
                .map(|v| v == "1" || v.to_lowercase() == "true")
                .unwrap_or(false),
        })
    }
}

impl Default for Config {
    fn default() -> Self {
        Self {
            listen_addr: "0.0.0.0:8080".to_string(),
            database_url: None,
            jwt_secret: None,
            debug: false,
        }
    }
}


     1→//! REST API endpoints
     2→
     3→use axum::{
     4→    extract::{Path, State},
     5→    Json,
     6→};
     7→use serde::Serialize;
     8→use uuid::Uuid;
     9→
    10→use crate::session::SessionManager;
    11→
    12→/// Viewer info returned by API
    13→#[derive(Debug, Serialize)]
    14→pub struct ViewerInfoApi {
    15→    pub id: String,
    16→    pub name: String,
    17→    pub connected_at: String,
    18→}
    19→
    20→impl From<crate::session::ViewerInfo> for ViewerInfoApi {
    21→    fn from(v: crate::session::ViewerInfo) -> Self {
    22→        Self {
    23→            id: v.id,
    24→            name: v.name,
    25→            connected_at: v.connected_at.to_rfc3339(),
    26→        }
    27→    }
    28→}
    29→
    30→/// Session info returned by API
    31→#[derive(Debug, Serialize)]
    32→pub struct SessionInfo {
    33→    pub id: String,
    34→    pub agent_id: String,
    35→    pub agent_name: String,
    36→    pub started_at: String,
    37→    pub viewer_count: usize,
    38→    pub viewers: Vec<ViewerInfoApi>,
    39→    pub is_streaming: bool,
    40→    pub is_online: bool,
    41→    pub is_persistent: bool,
    42→    pub last_heartbeat: String,
    43→    pub os_version: Option<String>,
    44→    pub is_elevated: bool,
    45→    pub uptime_secs: i64,
    46→    pub display_count: i32,
    47→}
    48→
    49→impl From<crate::session::Session> for SessionInfo {
    50→    fn from(s: crate::session::Session) -> Self {
    51→        Self {
    52→            id: s.id.to_string(),
    53→            agent_id: s.agent_id,
    54→            agent_name: s.agent_name,
    55→            started_at: s.started_at.to_rfc3339(),
    56→            viewer_count: s.viewer_count,
    57→            viewers: s.viewers.into_iter().map(ViewerInfoApi::from).collect(),
    58→            is_streaming: s.is_streaming,
    59→            is_online: s.is_online,
    60→            is_persistent: s.is_persistent,
    61→            last_heartbeat: s.last_heartbeat.to_rfc3339(),
    62→            os_version: s.os_version,
    63→            is_elevated: s.is_elevated,
    64→            uptime_secs: s.uptime_secs,
    65→            display_count: s.display_count,
    66→        }
    67→    }
    68→}
    69→
    70→/// List all active sessions
    71→pub async fn list_sessions(
    72→    State(sessions): State<SessionManager>,
    73→) -> Json<Vec<SessionInfo>> {
    74→    let sessions = sessions.list_sessions().await;
    75→    Json(sessions.into_iter().map(SessionInfo::from).collect())
    76→}
    77→
    78→/// Get a specific session by ID
    79→pub async fn get_session(
    80→    State(sessions): State<SessionManager>,
    81→    Path(id): Path<String>,
    82→) -> Result<Json<SessionInfo>, (axum::http::StatusCode, &'static str)> {
    83→    let session_id = Uuid::parse_str(&id)
    84→        .map_err(|_| (axum::http::StatusCode::BAD_REQUEST, "Invalid session ID"))?;
    85→
    86→    let session = sessions.get_session(session_id).await
    87→        .ok_or((axum::http::StatusCode::NOT_FOUND, "Session not found"))?;
    88→
    89→    Ok(Json(SessionInfo::from(session)))
    90→}
    91→

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

//! REST API endpoints

use axum::{
    extract::{Path, State},
    Json,
};
use serde::Serialize;
use uuid::Uuid;

use crate::session::SessionManager;

/// Viewer info returned by API
#[derive(Debug, Serialize)]
pub struct ViewerInfoApi {
    pub id: String,
    pub name: String,
    pub connected_at: String,
}

impl From<crate::session::ViewerInfo> for ViewerInfoApi {
    fn from(v: crate::session::ViewerInfo) -> Self {
        Self {
            id: v.id,
            name: v.name,
            connected_at: v.connected_at.to_rfc3339(),
        }
    }
}

/// Session info returned by API
#[derive(Debug, Serialize)]
pub struct SessionInfo {
    pub id: String,
    pub agent_id: String,
    pub agent_name: String,
    pub started_at: String,
    pub viewer_count: usize,
    pub viewers: Vec<ViewerInfoApi>,
    pub is_streaming: bool,
    pub is_online: bool,
    pub is_persistent: bool,
    pub last_heartbeat: String,
    pub os_version: Option<String>,
    pub is_elevated: bool,
    pub uptime_secs: i64,
    pub display_count: i32,
}

impl From<crate::session::Session> for SessionInfo {
    fn from(s: crate::session::Session) -> Self {
        Self {
            id: s.id.to_string(),
            agent_id: s.agent_id,
            agent_name: s.agent_name,
            started_at: s.started_at.to_rfc3339(),
            viewer_count: s.viewer_count,
            viewers: s.viewers.into_iter().map(ViewerInfoApi::from).collect(),
            is_streaming: s.is_streaming,
            is_online: s.is_online,
            is_persistent: s.is_persistent,
            last_heartbeat: s.last_heartbeat.to_rfc3339(),
            os_version: s.os_version,
            is_elevated: s.is_elevated,
            uptime_secs: s.uptime_secs,
            display_count: s.display_count,
        }
    }
}

/// List all active sessions
pub async fn list_sessions(
    State(sessions): State<SessionManager>,
) -> Json<Vec<SessionInfo>> {
    let sessions = sessions.list_sessions().await;
    Json(sessions.into_iter().map(SessionInfo::from).collect())
}

/// Get a specific session by ID
pub async fn get_session(
    State(sessions): State<SessionManager>,
    Path(id): Path<String>,
) -> Result<Json<SessionInfo>, (axum::http::StatusCode, &'static str)> {
    let session_id = Uuid::parse_str(&id)
        .map_err(|_| (axum::http::StatusCode::BAD_REQUEST, "Invalid session ID"))?;

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

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


Let me read the GuruRMM db module patterns:


     1→//! Database models and queries
     2→//!
     3→//! Provides database access for clients, sites, agents, metrics, commands, users, and updates.
     4→
     5→pub mod agents;
     6→pub mod clients;
     7→pub mod commands;
     8→pub mod metrics;
     9→pub mod sites;
    10→pub mod updates;
    11→pub mod users;
    12→
    13→pub use agents::*;
    14→pub use clients::*;
    15→pub use commands::*;
    16→pub use metrics::*;
    17→pub use sites::*;
    18→pub use updates::*;
    19→pub use users::*;
    20→

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

//! Database models and queries
//!
//! Provides database access for clients, sites, agents, metrics, commands, users, and updates.

pub mod agents;
pub mod clients;
pub mod commands;
pub mod metrics;
pub mod sites;
pub mod updates;
pub mod users;

pub use agents::*;
pub use clients::*;
pub use commands::*;
pub use metrics::*;
pub use sites::*;
pub use updates::*;
pub use users::*;


     1→//! Agent database operations
     2→
     3→use chrono::{DateTime, Utc};
     4→use serde::{Deserialize, Serialize};
     5→use sqlx::PgPool;
     6→use uuid::Uuid;
     7→
     8→/// Agent record from database
     9→#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
    10→pub struct Agent {
    11→    pub id: Uuid,
    12→    pub hostname: String,
    13→    #[serde(skip_serializing)]
    14→    pub api_key_hash: Option<String>,  // Nullable: new agents use site's api_key
    15→    pub os_type: String,
    16→    pub os_version: Option<String>,
    17→    pub agent_version: Option<String>,
    18→    pub last_seen: Option<DateTime<Utc>>,
    19→    pub status: String,
    20→    pub created_at: DateTime<Utc>,
    21→    pub updated_at: DateTime<Utc>,
    22→    // New fields for site-based registration
    23→    pub device_id: Option<String>,     // Hardware-derived unique ID
    24→    pub site_id: Option<Uuid>,         // Which site this agent belongs to
    25→}
    26→
    27→/// Agent without sensitive fields (for API responses)
    28→#[derive(Debug, Clone, Serialize, Deserialize)]
    29→pub struct AgentResponse {
    30→    pub id: Uuid,
    31→    pub hostname: String,
    32→    pub os_type: String,
    33→    pub os_version: Option<String>,
    34→    pub agent_version: Option<String>,
    35→    pub last_seen: Option<DateTime<Utc>>,
    36→    pub status: String,
    37→    pub created_at: DateTime<Utc>,
    38→    pub device_id: Option<String>,
    39→    pub site_id: Option<Uuid>,
    40→    pub site_name: Option<String>,
    41→    pub client_name: Option<String>,
    42→}
    43→
    44→impl From<Agent> for AgentResponse {
    45→    fn from(agent: Agent) -> Self {
    46→        Self {
    47→            id: agent.id,
    48→            hostname: agent.hostname,
    49→            os_type: agent.os_type,
    50→            os_version: agent.os_version,
    51→            agent_version: agent.agent_version,
    52→            last_seen: agent.last_seen,
    53→            status: agent.status,
    54→            created_at: agent.created_at,
    55→            device_id: agent.device_id,
    56→            site_id: agent.site_id,
    57→            site_name: None,
    58→            client_name: None,
    59→        }
    60→    }
    61→}
    62→
    63→/// Create a new agent registration
    64→#[derive(Debug, Clone, Deserialize)]
    65→pub struct CreateAgent {
    66→    pub hostname: String,
    67→    pub api_key_hash: String,
    68→    pub os_type: String,
    69→    pub os_version: Option<String>,
    70→    pub agent_version: Option<String>,
    71→}
    72→
    73→/// Insert a new agent into the database
    74→pub async fn create_agent(pool: &PgPool, agent: CreateAgent) -> Result<Agent, sqlx::Error> {
    75→    sqlx::query_as::<_, Agent>(
    76→        r#"
    77→        INSERT INTO agents (hostname, api_key_hash, os_type, os_version, agent_version, status)
    78→        VALUES ($1, $2, $3, $4, $5, 'offline')
    79→        RETURNING *
    80→        "#,
    81→    )
    82→    .bind(&agent.hostname)
    83→    .bind(&agent.api_key_hash)
    84→    .bind(&agent.os_type)
    85→    .bind(&agent.os_version)
    86→    .bind(&agent.agent_version)
    87→    .fetch_one(pool)
    88→    .await
    89→}
    90→
    91→/// Get an agent by ID
    92→pub async fn get_agent_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Agent>, sqlx::Error> {
    93→    sqlx::query_as::<_, Agent>("SELECT * FROM agents WHERE id = $1")
    94→        .bind(id)
    95→        .fetch_optional(pool)
    96→        .await
    97→}
    98→
    99→/// Get an agent by API key hash
   100→pub async fn get_agent_by_api_key_hash(
   101→    pool: &PgPool,
   102→    api_key_hash: &str,
   103→) -> Result<Option<Agent>, sqlx::Error> {
   104→    sqlx::query_as::<_, Agent>("SELECT * FROM agents WHERE api_key_hash = $1")
   105→        .bind(api_key_hash)
   106→        .fetch_optional(pool)
   107→        .await
   108→}
   109→
   110→/// Get all agents
   111→pub async fn get_all_agents(pool: &PgPool) -> Result<Vec<Agent>, sqlx::Error> {
   112→    sqlx::query_as::<_, Agent>("SELECT * FROM agents ORDER BY hostname")
   113→        .fetch_all(pool)
   114→        .await
   115→}
   116→
   117→/// Get agents by status
   118→pub async fn get_agents_by_status(
   119→    pool: &PgPool,
   120→    status: &str,
   121→) -> Result<Vec<Agent>, sqlx::Error> {
   122→    sqlx::query_as::<_, Agent>("SELECT * FROM agents WHERE status = $1 ORDER BY hostname")
   123→        .bind(status)
   124→        .fetch_all(pool)
   125→        .await
   126→}
   127→
   128→/// Update agent status and last_seen
   129→pub async fn update_agent_status(
   130→    pool: &PgPool,
   131→    id: Uuid,
   132→    status: &str,
   133→) -> Result<(), sqlx::Error> {
   134→    sqlx::query("UPDATE agents SET status = $1, last_seen = NOW() WHERE id = $2")
   135→        .bind(status)
   136→        .bind(id)
   137→        .execute(pool)
   138→        .await?;
   139→    Ok(())
   140→}
   141→
   142→/// Update agent info (on connection)
   143→pub async fn update_agent_info(
   144→    pool: &PgPool,
   145→    id: Uuid,
   146→    hostname: Option<&str>,
   147→    os_version: Option<&str>,
   148→    agent_version: Option<&str>,
   149→) -> Result<(), sqlx::Error> {
   150→    sqlx::query(
   151→        r#"
   152→        UPDATE agents
   153→        SET hostname = COALESCE($1, hostname),
   154→            os_version = COALESCE($2, os_version),
   155→            agent_version = COALESCE($3, agent_version),
   156→            last_seen = NOW(),
   157→            status = 'online'
   158→        WHERE id = $4
   159→        "#,
   160→    )
   161→    .bind(hostname)
   162→    .bind(os_version)
   163→    .bind(agent_version)
   164→    .bind(id)
   165→    .execute(pool)
   166→    .await?;
   167→    Ok(())
   168→}
   169→
   170→/// Delete an agent
   171→pub async fn delete_agent(pool: &PgPool, id: Uuid) -> Result<bool, sqlx::Error> {
   172→    let result = sqlx::query("DELETE FROM agents WHERE id = $1")
   173→        .bind(id)
   174→        .execute(pool)
   175→        .await?;
   176→    Ok(result.rows_affected() > 0)
   177→}
   178→
   179→/// Get agent count statistics
   180→#[derive(Debug, Clone, Serialize, Deserialize)]
   181→pub struct AgentStats {
   182→    pub total: i64,
   183→    pub online: i64,
   184→    pub offline: i64,
   185→}
   186→
   187→pub async fn get_agent_stats(pool: &PgPool) -> Result<AgentStats, sqlx::Error> {
   188→    let total: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM agents")
   189→        .fetch_one(pool)
   190→        .await?;
   191→
   192→    let online: (i64,) =
   193→        sqlx::query_as("SELECT COUNT(*) FROM agents WHERE status = 'online'")
   194→            .fetch_one(pool)
   195→            .await?;
   196→
   197→    Ok(AgentStats {
   198→        total: total.0,
   199→        online: online.0,
   200→        offline: total.0 - online.0,
   201→    })
   202→}
   203→
   204→// ============================================================================
   205→// Site-based agent operations
   206→// ============================================================================
   207→
   208→/// Data for creating an agent via site registration
   209→#[derive(Debug, Clone)]
   210→pub struct CreateAgentWithSite {
   211→    pub site_id: Uuid,
   212→    pub device_id: String,
   213→    pub hostname: String,
   214→    pub os_type: String,
   215→    pub os_version: Option<String>,
   216→    pub agent_version: Option<String>,
   217→}
   218→
   219→/// Create a new agent under a site (site-based registration)
   220→pub async fn create_agent_with_site(
   221→    pool: &PgPool,
   222→    agent: CreateAgentWithSite,
   223→) -> Result<Agent, sqlx::Error> {
   224→    sqlx::query_as::<_, Agent>(
   225→        r#"
   226→        INSERT INTO agents (site_id, device_id, hostname, os_type, os_version, agent_version, status)
   227→        VALUES ($1, $2, $3, $4, $5, $6, 'offline')
   228→        RETURNING *
   229→        "#,
   230→    )
   231→    .bind(&agent.site_id)
   232→    .bind(&agent.device_id)
   233→    .bind(&agent.hostname)
   234→    .bind(&agent.os_type)
   235→    .bind(&agent.os_version)
   236→    .bind(&agent.agent_version)
   237→    .fetch_one(pool)
   238→    .await
   239→}
   240→
   241→/// Get an agent by site_id and device_id (for site-based auth)
   242→pub async fn get_agent_by_site_and_device(
   243→    pool: &PgPool,
   244→    site_id: Uuid,
   245→    device_id: &str,
   246→) -> Result<Option<Agent>, sqlx::Error> {
   247→    sqlx::query_as::<_, Agent>(
   248→        "SELECT * FROM agents WHERE site_id = $1 AND device_id = $2"
   249→    )
   250→    .bind(site_id)
   251→    .bind(device_id)
   252→    .fetch_optional(pool)
   253→    .await
   254→}
   255→
   256→/// Get all agents for a site
   257→pub async fn get_agents_by_site(pool: &PgPool, site_id: Uuid) -> Result<Vec<Agent>, sqlx::Error> {
   258→    sqlx::query_as::<_, Agent>(
   259→        "SELECT * FROM agents WHERE site_id = $1 ORDER BY hostname"
   260→    )
   261→    .bind(site_id)
   262→    .fetch_all(pool)
   263→    .await
   264→}
   265→
   266→/// Agent with site and client details for API responses
   267→#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
   268→pub struct AgentWithDetails {
   269→    pub id: Uuid,
   270→    pub hostname: String,
   271→    pub os_type: String,
   272→    pub os_version: Option<String>,
   273→    pub agent_version: Option<String>,
   274→    pub last_seen: Option<DateTime<Utc>>,
   275→    pub status: String,
   276→    pub created_at: DateTime<Utc>,
   277→    pub device_id: Option<String>,
   278→    pub site_id: Option<Uuid>,
   279→    pub site_name: Option<String>,
   280→    pub client_id: Option<Uuid>,
   281→    pub client_name: Option<String>,
   282→}
   283→
   284→/// Get all agents with site/client details
   285→pub async fn get_all_agents_with_details(pool: &PgPool) -> Result<Vec<AgentWithDetails>, sqlx::Error> {
   286→    sqlx::query_as::<_, AgentWithDetails>(
   287→        r#"
   288→        SELECT
   289→            a.id, a.hostname, a.os_type, a.os_version, a.agent_version,
   290→            a.last_seen, a.status, a.created_at, a.device_id, a.site_id,
   291→            s.name as site_name,
   292→            c.id as client_id,
   293→            c.name as client_name
   294→        FROM agents a
   295→        LEFT JOIN sites s ON a.site_id = s.id
   296→        LEFT JOIN clients c ON s.client_id = c.id
   297→        ORDER BY c.name, s.name, a.hostname
   298→        "#,
   299→    )
   300→    .fetch_all(pool)
   301→    .await
   302→}
   303→
   304→/// Update agent info including device_id (on connection)
   305→pub async fn update_agent_info_full(
   306→    pool: &PgPool,
   307→    id: Uuid,
   308→    hostname: Option<&str>,
   309→    device_id: Option<&str>,
   310→    os_version: Option<&str>,
   311→    agent_version: Option<&str>,
   312→) -> Result<(), sqlx::Error> {
   313→    sqlx::query(
   314→        r#"
   315→        UPDATE agents
   316→        SET hostname = COALESCE($1, hostname),
   317→            device_id = COALESCE($2, device_id),
   318→            os_version = COALESCE($3, os_version),
   319→            agent_version = COALESCE($4, agent_version),
   320→            last_seen = NOW(),
   321→            status = 'online'
   322→        WHERE id = $5
   323→        "#,
   324→    )
   325→    .bind(hostname)
   326→    .bind(device_id)
   327→    .bind(os_version)
   328→    .bind(agent_version)
   329→    .bind(id)
   330→    .execute(pool)
   331→    .await?;
   332→    Ok(())
   333→}
   334→
   335→/// Move an agent to a different site (or remove from site if site_id is None)
   336→pub async fn move_agent_to_site(
   337→    pool: &PgPool,
   338→    agent_id: Uuid,
   339→    site_id: Option<Uuid>,
   340→) -> Result<Option<Agent>, sqlx::Error> {
   341→    sqlx::query_as::<_, Agent>(
   342→        r#"
   343→        UPDATE agents
   344→        SET site_id = $1,
   345→            updated_at = NOW()
   346→        WHERE id = $2
   347→        RETURNING *
   348→        "#,
   349→    )
   350→    .bind(site_id)
   351→    .bind(agent_id)
   352→    .fetch_optional(pool)
   353→    .await
   354→}
   355→
   356→/// Get agents that are not assigned to any site
   357→pub async fn get_unassigned_agents(pool: &PgPool) -> Result<Vec<Agent>, sqlx::Error> {
   358→    sqlx::query_as::<_, Agent>(
   359→        "SELECT * FROM agents WHERE site_id IS NULL ORDER BY hostname"
   360→    )
   361→    .fetch_all(pool)
   362→    .await
   363→}
   364→
   365→/// Update agent last_seen timestamp (for heartbeat)
   366→pub async fn update_agent_last_seen(
   367→    pool: &PgPool,
   368→    id: Uuid,
   369→) -> Result<(), sqlx::Error> {
   370→    sqlx::query("UPDATE agents SET last_seen = NOW(), status = 'online' WHERE id = $1")
   371→        .bind(id)
   372→        .execute(pool)
   373→        .await?;
   374→    Ok(())
   375→}
   376→

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

//! Agent database operations

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use uuid::Uuid;

/// Agent record from database
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Agent {
    pub id: Uuid,
    pub hostname: String,
    #[serde(skip_serializing)]
    pub api_key_hash: Option<String>,  // Nullable: new agents use site's api_key
    pub os_type: String,
    pub os_version: Option<String>,
    pub agent_version: Option<String>,
    pub last_seen: Option<DateTime<Utc>>,
    pub status: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    // New fields for site-based registration
    pub device_id: Option<String>,     // Hardware-derived unique ID
    pub site_id: Option<Uuid>,         // Which site this agent belongs to
}

/// Agent without sensitive fields (for API responses)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResponse {
    pub id: Uuid,
    pub hostname: String,
    pub os_type: String,
    pub os_version: Option<String>,
    pub agent_version: Option<String>,
    pub last_seen: Option<DateTime<Utc>>,
    pub status: String,
    pub created_at: DateTime<Utc>,
    pub device_id: Option<String>,
    pub site_id: Option<Uuid>,
    pub site_name: Option<String>,
    pub client_name: Option<String>,
}

impl From<Agent> for AgentResponse {
    fn from(agent: Agent) -> Self {
        Self {
            id: agent.id,
            hostname: agent.hostname,
            os_type: agent.os_type,
            os_version: agent.os_version,
            agent_version: agent.agent_version,
            last_seen: agent.last_seen,
            status: agent.status,
            created_at: agent.created_at,
            device_id: agent.device_id,
            site_id: agent.site_id,
            site_name: None,
            client_name: None,
        }
    }
}

/// Create a new agent registration
#[derive(Debug, Clone, Deserialize)]
pub struct CreateAgent {
    pub hostname: String,
    pub api_key_hash: String,
    pub os_type: String,
    pub os_version: Option<String>,
    pub agent_version: Option<String>,
}

/// Insert a new agent into the database
pub async fn create_agent(pool: &PgPool, agent: CreateAgent) -> Result<Agent, sqlx::Error> {
    sqlx::query_as::<_, Agent>(
        r#"
        INSERT INTO agents (hostname, api_key_hash, os_type, os_version, agent_version, status)
        VALUES ($1, $2, $3, $4, $5, 'offline')
        RETURNING *
        "#,
    )
    .bind(&agent.hostname)
    .bind(&agent.api_key_hash)
    .bind(&agent.os_type)
    .bind(&agent.os_version)
    .bind(&agent.agent_version)
    .fetch_one(pool)
    .await
}

/// Get an agent by ID
pub async fn get_agent_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Agent>, sqlx::Error> {
    sqlx::query_as::<_, Agent>("SELECT * FROM agents WHERE id = $1")
        .bind(id)
        .fetch_optional(pool)
        .await
}

/// Get an agent by API key hash
pub async fn get_agent_by_api_key_hash(
    pool: &PgPool,
    api_key_hash: &str,
) -> Result<Option<Agent>, sqlx::Error> {
    sqlx::query_as::<_, Agent>("SELECT * FROM agents WHERE api_key_hash = $1")
        .bind(api_key_hash)
        .fetch_optional(pool)
        .await
}

/// Get all agents
pub async fn get_all_agents(pool: &PgPool) -> Result<Vec<Agent>, sqlx::Error> {
    sqlx::query_as::<_, Agent>("SELECT * FROM agents ORDER BY hostname")
        .fetch_all(pool)
        .await
}

/// Get agents by status
pub async fn get_agents_by_status(
    pool: &PgPool,
    status: &str,
) -> Result<Vec<Agent>, sqlx::Error> {
    sqlx::query_as::<_, Agent>("SELECT * FROM agents WHERE status = $1 ORDER BY hostname")
        .bind(status)
        .fetch_all(pool)
        .await
}

/// Update agent status and last_seen
pub async fn update_agent_status(
    pool: &PgPool,
    id: Uuid,
    status: &str,
) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE agents SET status = $1, last_seen = NOW() WHERE id = $2")
        .bind(status)
        .bind(id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Update agent info (on connection)
pub async fn update_agent_info(
    pool: &PgPool,
    id: Uuid,
    hostname: Option<&str>,
    os_version: Option<&str>,
    agent_version: Option<&str>,
) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        UPDATE agents
        SET hostname = COALESCE($1, hostname),
            os_version = COALESCE($2, os_version),
            agent_version = COALESCE($3, agent_version),
            last_seen = NOW(),
            status = 'online'
        WHERE id = $4
        "#,
    )
    .bind(hostname)
    .bind(os_version)
    .bind(agent_version)
    .bind(id)
    .execute(pool)
    .await?;
    Ok(())
}

/// Delete an agent
pub async fn delete_agent(pool: &PgPool, id: Uuid) -> Result<bool, sqlx::Error> {
    let result = sqlx::query("DELETE FROM agents WHERE id = $1")
        .bind(id)
        .execute(pool)
        .await?;
    Ok(result.rows_affected() > 0)
}

/// Get agent count statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStats {
    pub total: i64,
    pub online: i64,
    pub offline: i64,
}

pub async fn get_agent_stats(pool: &PgPool) -> Result<AgentStats, sqlx::Error> {
    let total: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM agents")
        .fetch_one(pool)
        .await?;

    let online: (i64,) =
        sqlx::query_as("SELECT COUNT(*) FROM agents WHERE status = 'online'")
            .fetch_one(pool)
            .await?;

    Ok(AgentStats {
        total: total.0,
        online: online.0,
        offline: total.0 - online.0,
    })
}

// ============================================================================
// Site-based agent operations
// ============================================================================

/// Data for creating an agent via site registration
#[derive(Debug, Clone)]
pub struct CreateAgentWithSite {
    pub site_id: Uuid,
    pub device_id: String,
    pub hostname: String,
    pub os_type: String,
    pub os_version: Option<String>,
    pub agent_version: Option<String>,
}

/// Create a new agent under a site (site-based registration)
pub async fn create_agent_with_site(
    pool: &PgPool,
    agent: CreateAgentWithSite,
) -> Result<Agent, sqlx::Error> {
    sqlx::query_as::<_, Agent>(
        r#"
        INSERT INTO agents (site_id, device_id, hostname, os_type, os_version, agent_version, status)
        VALUES ($1, $2, $3, $4, $5, $6, 'offline')
        RETURNING *
        "#,
    )
    .bind(&agent.site_id)
    .bind(&agent.device_id)
    .bind(&agent.hostname)
    .bind(&agent.os_type)
    .bind(&agent.os_version)
    .bind(&agent.agent_version)
    .fetch_one(pool)
    .await
}

/// Get an agent by site_id and device_id (for site-based auth)
pub async fn get_agent_by_site_and_device(
    pool: &PgPool,
    site_id: Uuid,
    device_id: &str,
) -> Result<Option<Agent>, sqlx::Error> {
    sqlx::query_as::<_, Agent>(
        "SELECT * FROM agents WHERE site_id = $1 AND device_id = $2"
    )
    .bind(site_id)
    .bind(device_id)
    .fetch_optional(pool)
    .await
}

/// Get all agents for a site
pub async fn get_agents_by_site(pool: &PgPool, site_id: Uuid) -> Result<Vec<Agent>, sqlx::Error> {
    sqlx::query_as::<_, Agent>(
        "SELECT * FROM agents WHERE site_id = $1 ORDER BY hostname"
    )
    .bind(site_id)
    .fetch_all(pool)
    .await
}

/// Agent with site and client details for API responses
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
pub struct AgentWithDetails {
    pub id: Uuid,
    pub hostname: String,
    pub os_type: String,
    pub os_version: Option<String>,
    pub agent_version: Option<String>,
    pub last_seen: Option<DateTime<Utc>>,
    pub status: String,
    pub created_at: DateTime<Utc>,
    pub device_id: Option<String>,
    pub site_id: Option<Uuid>,
    pub site_name: Option<String>,
    pub client_id: Option<Uuid>,
    pub client_name: Option<String>,
}

/// Get all agents with site/client details
pub async fn get_all_agents_with_details(pool: &PgPool) -> Result<Vec<AgentWithDetails>, sqlx::Error> {
    sqlx::query_as::<_, AgentWithDetails>(
        r#"
        SELECT
            a.id, a.hostname, a.os_type, a.os_version, a.agent_version,
            a.last_seen, a.status, a.created_at, a.device_id, a.site_id,
            s.name as site_name,
            c.id as client_id,
            c.name as client_name
        FROM agents a
        LEFT JOIN sites s ON a.site_id = s.id
        LEFT JOIN clients c ON s.client_id = c.id
        ORDER BY c.name, s.name, a.hostname
        "#,
    )
    .fetch_all(pool)
    .await
}

/// Update agent info including device_id (on connection)
pub async fn update_agent_info_full(
    pool: &PgPool,
    id: Uuid,
    hostname: Option<&str>,
    device_id: Option<&str>,
    os_version: Option<&str>,
    agent_version: Option<&str>,
) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        UPDATE agents
        SET hostname = COALESCE($1, hostname),
            device_id = COALESCE($2, device_id),
            os_version = COALESCE($3, os_version),
            agent_version = COALESCE($4, agent_version),
            last_seen = NOW(),
            status = 'online'
        WHERE id = $5
        "#,
    )
    .bind(hostname)
    .bind(device_id)
    .bind(os_version)
    .bind(agent_version)
    .bind(id)
    .execute(pool)
    .await?;
    Ok(())
}

/// Move an agent to a different site (or remove from site if site_id is None)
pub async fn move_agent_to_site(
    pool: &PgPool,
    agent_id: Uuid,
    site_id: Option<Uuid>,
) -> Result<Option<Agent>, sqlx::Error> {
    sqlx::query_as::<_, Agent>(
        r#"
        UPDATE agents
        SET site_id = $1,
            updated_at = NOW()
        WHERE id = $2
        RETURNING *
        "#,
    )
    .bind(site_id)
    .bind(agent_id)
    .fetch_optional(pool)
    .await
}

/// Get agents that are not assigned to any site
pub async fn get_unassigned_agents(pool: &PgPool) -> Result<Vec<Agent>, sqlx::Error> {
    sqlx::query_as::<_, Agent>(
        "SELECT * FROM agents WHERE site_id IS NULL ORDER BY hostname"
    )
    .fetch_all(pool)
    .await
}

/// Update agent last_seen timestamp (for heartbeat)
pub async fn update_agent_last_seen(
    pool: &PgPool,
    id: Uuid,
) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE agents SET last_seen = NOW(), status = 'online' WHERE id = $1")
        .bind(id)
        .execute(pool)
        .await?;
    Ok(())
}


Let me also check the GuruRMM main.rs for connection pool initialization patterns and any existing migrations:


     1→//! GuruRMM Server - RMM Management Server
     2→//!
     3→//! Provides the backend API and WebSocket endpoint for GuruRMM agents.
     4→//! Features:
     5→//! - Agent registration and management
     6→//! - Real-time WebSocket communication with agents
     7→//! - Metrics storage and retrieval
     8→//! - Command execution via agents
     9→//! - Dashboard authentication
    10→
    11→mod api;
    12→mod auth;
    13→mod config;
    14→mod db;
    15→mod updates;
    16→mod ws;
    17→
    18→use std::sync::Arc;
    19→
    20→use anyhow::Result;
    21→use axum::{
    22→    routing::{get, post},
    23→    Router,
    24→};
    25→use sqlx::postgres::PgPoolOptions;
    26→use tokio::sync::RwLock;
    27→use tower_http::cors::{Any, CorsLayer};
    28→use tower_http::trace::TraceLayer;
    29→use tracing::info;
    30→
    31→use crate::config::ServerConfig;
    32→use crate::updates::UpdateManager;
    33→use crate::ws::AgentConnections;
    34→
    35→/// Shared application state
    36→#[derive(Clone)]
    37→pub struct AppState {
    38→    /// Database connection pool
    39→    pub db: sqlx::PgPool,
    40→
    41→    /// Server configuration
    42→    pub config: Arc<ServerConfig>,
    43→
    44→    /// Connected agents (WebSocket connections)
    45→    pub agents: Arc<RwLock<AgentConnections>>,
    46→
    47→    /// Agent update manager
    48→    pub updates: Arc<UpdateManager>,
    49→}
    50→
    51→#[tokio::main]
    52→async fn main() -> Result<()> {
    53→    // Load environment variables from .env file
    54→    dotenvy::dotenv().ok();
    55→
    56→    // Initialize logging
    57→    tracing_subscriber::fmt()
    58→        .with_env_filter(
    59→            tracing_subscriber::EnvFilter::from_default_env()
    60→                .add_directive("gururmm_server=info".parse()?)
    61→                .add_directive("tower_http=debug".parse()?)
    62→                .add_directive("info".parse()?),
    63→        )
    64→        .init();
    65→
    66→    info!("GuruRMM Server starting...");
    67→
    68→    // Load configuration
    69→    let config = ServerConfig::from_env()?;
    70→    info!("Server configuration loaded");
    71→
    72→    // Connect to database
    73→    info!("Connecting to database...");
    74→    let db = PgPoolOptions::new()
    75→        .max_connections(config.database.max_connections)
    76→        .connect(&config.database.url)
    77→        .await?;
    78→    info!("Database connected");
    79→
    80→    // Run migrations
    81→    info!("Running database migrations...");
    82→    sqlx::migrate!("./migrations").run(&db).await?;
    83→    info!("Migrations complete");
    84→
    85→    // Initialize update manager
    86→    let update_manager = UpdateManager::new(
    87→        config.updates.downloads_dir.clone(),
    88→        config.updates.downloads_base_url.clone(),
    89→        config.updates.auto_update_enabled,
    90→        config.updates.update_timeout_secs,
    91→    );
    92→
    93→    // Initial scan for available versions
    94→    info!("Scanning for available agent versions...");
    95→    if let Err(e) = update_manager.scan_versions().await {
    96→        tracing::warn!("Failed to scan agent versions: {} (continuing without auto-update)", e);
    97→    }
    98→
    99→    let update_manager = Arc::new(update_manager);
   100→
   101→    // Spawn background scanner (handle is intentionally not awaited - runs until server shutdown)
   102→    let _scanner_handle = update_manager.spawn_scanner(config.updates.scan_interval_secs);
   103→    info!(
   104→        "Auto-update: {} (scan interval: {}s)",
   105→        if config.updates.auto_update_enabled { "enabled" } else { "disabled" },
   106→        config.updates.scan_interval_secs
   107→    );
   108→
   109→    // Create shared state
   110→    let state = AppState {
   111→        db,
   112→        config: Arc::new(config.clone()),
   113→        agents: Arc::new(RwLock::new(AgentConnections::new())),
   114→        updates: update_manager,
   115→    };
   116→
   117→    // Build router
   118→    let app = build_router(state);
   119→
   120→    // Start server
   121→    let addr = format!("{}:{}", config.server.host, config.server.port);
   122→    info!("Starting server on {}", addr);
   123→
   124→    let listener = tokio::net::TcpListener::bind(&addr).await?;
   125→    axum::serve(listener, app).await?;
   126→
   127→    Ok(())
   128→}
   129→
   130→/// Build the application router
   131→fn build_router(state: AppState) -> Router {
   132→    // CORS configuration (allow dashboard access)
   133→    let cors = CorsLayer::new()
   134→        .allow_origin(Any)
   135→        .allow_methods(Any)
   136→        .allow_headers(Any);
   137→
   138→    Router::new()
   139→        // Health check
   140→        .route("/health", get(health_check))
   141→        // WebSocket endpoint for agents
   142→        .route("/ws", get(ws::ws_handler))
   143→        // API routes
   144→        .nest("/api", api::routes())
   145→        // Middleware
   146→        .layer(TraceLayer::new_for_http())
   147→        .layer(cors)
   148→        // State
   149→        .with_state(state)
   150→}
   151→
   152→/// Health check endpoint
   153→async fn health_check() -> &'static str {
   154→    "OK"
   155→}
   156→

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

//! GuruRMM Server - RMM Management Server
//!
//! Provides the backend API and WebSocket endpoint for GuruRMM agents.
//! Features:
//! - Agent registration and management
//! - Real-time WebSocket communication with agents
//! - Metrics storage and retrieval
//! - Command execution via agents
//! - Dashboard authentication

mod api;
mod auth;
mod config;
mod db;
mod updates;
mod ws;

use std::sync::Arc;

use anyhow::Result;
use axum::{
    routing::{get, post},
    Router,
};
use sqlx::postgres::PgPoolOptions;
use tokio::sync::RwLock;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::info;

use crate::config::ServerConfig;
use crate::updates::UpdateManager;
use crate::ws::AgentConnections;

/// Shared application state
#[derive(Clone)]
pub struct AppState {
    /// Database connection pool
    pub db: sqlx::PgPool,

    /// Server configuration
    pub config: Arc<ServerConfig>,

    /// Connected agents (WebSocket connections)
    pub agents: Arc<RwLock<AgentConnections>>,

    /// Agent update manager
    pub updates: Arc<UpdateManager>,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Load environment variables from .env file
    dotenvy::dotenv().ok();

    // Initialize logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("gururmm_server=info".parse()?)
                .add_directive("tower_http=debug".parse()?)
                .add_directive("info".parse()?),
        )
        .init();

    info!("GuruRMM Server starting...");

    // Load configuration
    let config = ServerConfig::from_env()?;
    info!("Server configuration loaded");

    // Connect to database
    info!("Connecting to database...");
    let db = PgPoolOptions::new()
        .max_connections(config.database.max_connections)
        .connect(&config.database.url)
        .await?;
    info!("Database connected");

    // Run migrations
    info!("Running database migrations...");
    sqlx::migrate!("./migrations").run(&db).await?;
    info!("Migrations complete");

    // Initialize update manager
    let update_manager = UpdateManager::new(
        config.updates.downloads_dir.clone(),
        config.updates.downloads_base_url.clone(),
        config.updates.auto_update_enabled,
        config.updates.update_timeout_secs,
    );

    // Initial scan for available versions
    info!("Scanning for available agent versions...");
    if let Err(e) = update_manager.scan_versions().await {
        tracing::warn!("Failed to scan agent versions: {} (continuing without auto-update)", e);
    }

    let update_manager = Arc::new(update_manager);

    // Spawn background scanner (handle is intentionally not awaited - runs until server shutdown)
    let _scanner_handle = update_manager.spawn_scanner(config.updates.scan_interval_secs);
    info!(
        "Auto-update: {} (scan interval: {}s)",
        if config.updates.auto_update_enabled { "enabled" } else { "disabled" },
        config.updates.scan_interval_secs
    );

    // Create shared state
    let state = AppState {
        db,
        config: Arc::new(config.clone()),
        agents: Arc::new(RwLock::new(AgentConnections::new())),
        updates: update_manager,
    };

    // Build router
    let app = build_router(state);

    // Start server
    let addr = format!("{}:{}", config.server.host, config.server.port);
    info!("Starting server on {}", addr);

    let listener = tokio::net::TcpListener::bind(&addr).await?;
    axum::serve(listener, app).await?;

    Ok(())
}

/// Build the application router
fn build_router(state: AppState) -> Router {
    // CORS configuration (allow dashboard access)
    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods(Any)
        .allow_headers(Any);

    Router::new()
        // Health check
        .route("/health", get(health_check))
        // WebSocket endpoint for agents
        .route("/ws", get(ws::ws_handler))
        // API routes
        .nest("/api", api::routes())
        // Middleware
        .layer(TraceLayer::new_for_http())
        .layer(cors)
        // State
        .with_state(state)
}

/// Health check endpoint
async fn health_check() -> &'static str {
    "OK"
}


     1→-- GuruRMM Initial Schema
     2→-- Creates tables for agents, metrics, commands, watchdog events, and users
     3→
     4→-- Enable UUID extension
     5→CREATE EXTENSION IF NOT EXISTS "pgcrypto";
     6→
     7→-- Agents table
     8→-- Stores registered agents and their current status
     9→CREATE TABLE agents (
    10→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    11→    hostname VARCHAR(255) NOT NULL,
    12→    api_key_hash VARCHAR(255) NOT NULL,
    13→    os_type VARCHAR(50) NOT NULL,
    14→    os_version VARCHAR(100),
    15→    agent_version VARCHAR(50),
    16→    last_seen TIMESTAMPTZ,
    17→    status VARCHAR(20) DEFAULT 'offline',
    18→    created_at TIMESTAMPTZ DEFAULT NOW(),
    19→    updated_at TIMESTAMPTZ DEFAULT NOW()
    20→);
    21→
    22→-- Index for looking up agents by hostname
    23→CREATE INDEX idx_agents_hostname ON agents(hostname);
    24→
    25→-- Index for finding online agents
    26→CREATE INDEX idx_agents_status ON agents(status);
    27→
    28→-- Metrics table
    29→-- Time-series data for system metrics from agents
    30→CREATE TABLE metrics (
    31→    id BIGSERIAL PRIMARY KEY,
    32→    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    33→    timestamp TIMESTAMPTZ DEFAULT NOW(),
    34→    cpu_percent REAL,
    35→    memory_percent REAL,
    36→    memory_used_bytes BIGINT,
    37→    disk_percent REAL,
    38→    disk_used_bytes BIGINT,
    39→    network_rx_bytes BIGINT,
    40→    network_tx_bytes BIGINT
    41→);
    42→
    43→-- Index for querying metrics by agent and time
    44→CREATE INDEX idx_metrics_agent_time ON metrics(agent_id, timestamp DESC);
    45→
    46→-- Index for finding recent metrics
    47→CREATE INDEX idx_metrics_timestamp ON metrics(timestamp DESC);
    48→
    49→-- Users table
    50→-- Dashboard users for authentication
    51→CREATE TABLE users (
    52→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    53→    email VARCHAR(255) UNIQUE NOT NULL,
    54→    password_hash VARCHAR(255),
    55→    name VARCHAR(255),
    56→    role VARCHAR(50) DEFAULT 'user',
    57→    sso_provider VARCHAR(50),
    58→    sso_id VARCHAR(255),
    59→    created_at TIMESTAMPTZ DEFAULT NOW(),
    60→    last_login TIMESTAMPTZ
    61→);
    62→
    63→-- Index for email lookups during login
    64→CREATE INDEX idx_users_email ON users(email);
    65→
    66→-- Index for SSO lookups
    67→CREATE INDEX idx_users_sso ON users(sso_provider, sso_id);
    68→
    69→-- Commands table
    70→-- Commands sent to agents and their results
    71→CREATE TABLE commands (
    72→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    73→    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    74→    command_type VARCHAR(50) NOT NULL,
    75→    command_text TEXT NOT NULL,
    76→    status VARCHAR(20) DEFAULT 'pending',
    77→    exit_code INTEGER,
    78→    stdout TEXT,
    79→    stderr TEXT,
    80→    created_at TIMESTAMPTZ DEFAULT NOW(),
    81→    started_at TIMESTAMPTZ,
    82→    completed_at TIMESTAMPTZ,
    83→    created_by UUID REFERENCES users(id) ON DELETE SET NULL
    84→);
    85→
    86→-- Index for finding pending commands for an agent
    87→CREATE INDEX idx_commands_agent_status ON commands(agent_id, status);
    88→
    89→-- Index for command history queries
    90→CREATE INDEX idx_commands_created ON commands(created_at DESC);
    91→
    92→-- Watchdog events table
    93→-- Events from agent watchdog monitoring
    94→CREATE TABLE watchdog_events (
    95→    id BIGSERIAL PRIMARY KEY,
    96→    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    97→    timestamp TIMESTAMPTZ DEFAULT NOW(),
    98→    service_name VARCHAR(255) NOT NULL,
    99→    event_type VARCHAR(50) NOT NULL,
   100→    details TEXT
   101→);
   102→
   103→-- Index for querying events by agent and time
   104→CREATE INDEX idx_watchdog_agent_time ON watchdog_events(agent_id, timestamp DESC);
   105→
   106→-- Index for finding recent events
   107→CREATE INDEX idx_watchdog_timestamp ON watchdog_events(timestamp DESC);
   108→
   109→-- Function to update updated_at timestamp
   110→CREATE OR REPLACE FUNCTION update_updated_at_column()
   111→RETURNS TRIGGER AS $$
   112→BEGIN
   113→    NEW.updated_at = NOW();
   114→    RETURN NEW;
   115→END;
   116→$$ LANGUAGE plpgsql;
   117→
   118→-- Trigger for agents table
   119→CREATE TRIGGER update_agents_updated_at
   120→    BEFORE UPDATE ON agents
   121→    FOR EACH ROW
   122→    EXECUTE FUNCTION update_updated_at_column();
   123→

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

-- GuruRMM Initial Schema
-- Creates tables for agents, metrics, commands, watchdog events, and users

-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Agents table
-- Stores registered agents and their current status
CREATE TABLE agents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    hostname VARCHAR(255) NOT NULL,
    api_key_hash VARCHAR(255) NOT NULL,
    os_type VARCHAR(50) NOT NULL,
    os_version VARCHAR(100),
    agent_version VARCHAR(50),
    last_seen TIMESTAMPTZ,
    status VARCHAR(20) DEFAULT 'offline',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for looking up agents by hostname
CREATE INDEX idx_agents_hostname ON agents(hostname);

-- Index for finding online agents
CREATE INDEX idx_agents_status ON agents(status);

-- Metrics table
-- Time-series data for system metrics from agents
CREATE TABLE metrics (
    id BIGSERIAL PRIMARY KEY,
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    cpu_percent REAL,
    memory_percent REAL,
    memory_used_bytes BIGINT,
    disk_percent REAL,
    disk_used_bytes BIGINT,
    network_rx_bytes BIGINT,
    network_tx_bytes BIGINT
);

-- Index for querying metrics by agent and time
CREATE INDEX idx_metrics_agent_time ON metrics(agent_id, timestamp DESC);

-- Index for finding recent metrics
CREATE INDEX idx_metrics_timestamp ON metrics(timestamp DESC);

-- Users table
-- Dashboard users for authentication
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255),
    name VARCHAR(255),
    role VARCHAR(50) DEFAULT 'user',
    sso_provider VARCHAR(50),
    sso_id VARCHAR(255),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    last_login TIMESTAMPTZ
);

-- Index for email lookups during login
CREATE INDEX idx_users_email ON users(email);

-- Index for SSO lookups
CREATE INDEX idx_users_sso ON users(sso_provider, sso_id);

-- Commands table
-- Commands sent to agents and their results
CREATE TABLE commands (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    command_type VARCHAR(50) NOT NULL,
    command_text TEXT NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    exit_code INTEGER,
    stdout TEXT,
    stderr TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    started_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    created_by UUID REFERENCES users(id) ON DELETE SET NULL
);

-- Index for finding pending commands for an agent
CREATE INDEX idx_commands_agent_status ON commands(agent_id, status);

-- Index for command history queries
CREATE INDEX idx_commands_created ON commands(created_at DESC);

-- Watchdog events table
-- Events from agent watchdog monitoring
CREATE TABLE watchdog_events (
    id BIGSERIAL PRIMARY KEY,
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    service_name VARCHAR(255) NOT NULL,
    event_type VARCHAR(50) NOT NULL,
    details TEXT
);

-- Index for querying events by agent and time
CREATE INDEX idx_watchdog_agent_time ON watchdog_events(agent_id, timestamp DESC);

-- Index for finding recent events
CREATE INDEX idx_watchdog_timestamp ON watchdog_events(timestamp DESC);

-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Trigger for agents table
CREATE TRIGGER update_agents_updated_at
    BEFORE UPDATE ON agents
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();


Now I have a comprehensive understanding of the codebase. Let me provide the detailed implementation plan.

---

## PostgreSQL Database Integration Plan for GuruConnect

### Executive Summary

This plan adds PostgreSQL persistence to GuruConnect while maintaining the in-memory SessionManager for real-time state. The database will persist machines (agents), audit session events, and store support codes across server restarts.

---

## 1. Database Schema Design

### Table 1: `connect_machines` (Agent/Machine Persistence)

```sql
-- Machines/Agents that connect to GuruConnect
-- Persistent agents survive server restarts and show as "offline" until reconnect
CREATE TABLE connect_machines (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id VARCHAR(255) UNIQUE NOT NULL,  -- Hardware-derived unique ID from agent
    hostname VARCHAR(255) NOT NULL,
    os_version VARCHAR(255),
    is_elevated BOOLEAN DEFAULT FALSE,
    is_persistent BOOLEAN DEFAULT TRUE,     -- Persistent vs one-time support session
    first_seen TIMESTAMPTZ DEFAULT NOW(),
    last_seen TIMESTAMPTZ DEFAULT NOW(),
    last_session_id UUID,                   -- Most recent session
    status VARCHAR(20) DEFAULT 'offline',   -- 'online', 'offline'
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_connect_machines_agent_id ON connect_machines(agent_id);
CREATE INDEX idx_connect_machines_status ON connect_machines(status);
```

### Table 2: `connect_sessions` (Session History)

```sql
-- Session records - created when agent connects, updated on disconnect
CREATE TABLE connect_sessions (
    id UUID PRIMARY KEY,                    -- Matches runtime SessionId
    machine_id UUID REFERENCES connect_machines(id) ON DELETE CASCADE,
    started_at TIMESTAMPTZ DEFAULT NOW(),
    ended_at TIMESTAMPTZ,
    duration_secs INTEGER,                  -- Computed on session end
    is_support_session BOOLEAN DEFAULT FALSE, -- True if created via support code
    support_code VARCHAR(10),               -- The 6-digit code if applicable
    status VARCHAR(20) DEFAULT 'active'     -- 'active', 'ended', 'disconnected'
);

CREATE INDEX idx_connect_sessions_machine ON connect_sessions(machine_id);
CREATE INDEX idx_connect_sessions_started ON connect_sessions(started_at DESC);
CREATE INDEX idx_connect_sessions_support_code ON connect_sessions(support_code);
```

### Table 3: `connect_session_events` (Audit Log)

```sql
-- Audit trail of all session events
CREATE TABLE connect_session_events (
    id BIGSERIAL PRIMARY KEY,
    session_id UUID REFERENCES connect_sessions(id) ON DELETE CASCADE,
    event_type VARCHAR(50) NOT NULL,        -- See event types below
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    viewer_id VARCHAR(255),                 -- Technician ID if applicable
    viewer_name VARCHAR(255),               -- Technician name if applicable
    details JSONB,                          -- Additional event-specific data
    ip_address INET                         -- Source IP for audit
);

-- Event types:
-- 'session_started' - Agent connected
-- 'session_ended' - Agent disconnected cleanly
-- 'session_timeout' - Agent timed out
-- 'viewer_joined' - Technician started viewing
-- 'viewer_left' - Technician stopped viewing
-- 'streaming_started' - First viewer joined (stream began)
-- 'streaming_stopped' - Last viewer left (stream ended)

CREATE INDEX idx_connect_events_session ON connect_session_events(session_id);
CREATE INDEX idx_connect_events_time ON connect_session_events(timestamp DESC);
CREATE INDEX idx_connect_events_type ON connect_session_events(event_type);
```

### Table 4: `connect_support_codes` (Support Code Persistence)

```sql
-- Support codes for one-time remote support sessions
CREATE TABLE connect_support_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code VARCHAR(10) UNIQUE NOT NULL,       -- 6-digit code
    session_id UUID,                        -- Links to connect_sessions when connected
    created_by VARCHAR(255) NOT NULL,       -- Technician who created the code
    created_at TIMESTAMPTZ DEFAULT NOW(),
    expires_at TIMESTAMPTZ,                 -- Optional expiration
    status VARCHAR(20) DEFAULT 'pending',   -- 'pending', 'connected', 'completed', 'cancelled', 'expired'
    client_name VARCHAR(255),               -- Name entered by client
    client_machine VARCHAR(255),            -- Machine name from agent
    connected_at TIMESTAMPTZ                -- When client connected
);

CREATE INDEX idx_support_codes_code ON connect_support_codes(code);
CREATE INDEX idx_support_codes_status ON connect_support_codes(status);
CREATE INDEX idx_support_codes_session ON connect_support_codes(session_id);
```

---

## 2. Migration Files Structure

Create directory: `C:/Users/MikeSwanson/Claude/guru-connect/server/migrations/`

### Migration 001: `001_initial_schema.sql`

```sql
-- GuruConnect Initial Schema
-- Persistence for machines, sessions, audit events, and support codes

CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Machines table
CREATE TABLE connect_machines (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id VARCHAR(255) UNIQUE NOT NULL,
    hostname VARCHAR(255) NOT NULL,
    os_version VARCHAR(255),
    is_elevated BOOLEAN DEFAULT FALSE,
    is_persistent BOOLEAN DEFAULT TRUE,
    first_seen TIMESTAMPTZ DEFAULT NOW(),
    last_seen TIMESTAMPTZ DEFAULT NOW(),
    last_session_id UUID,
    status VARCHAR(20) DEFAULT 'offline',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_connect_machines_agent_id ON connect_machines(agent_id);
CREATE INDEX idx_connect_machines_status ON connect_machines(status);

-- Sessions table
CREATE TABLE connect_sessions (
    id UUID PRIMARY KEY,
    machine_id UUID REFERENCES connect_machines(id) ON DELETE CASCADE,
    started_at TIMESTAMPTZ DEFAULT NOW(),
    ended_at TIMESTAMPTZ,
    duration_secs INTEGER,
    is_support_session BOOLEAN DEFAULT FALSE,
    support_code VARCHAR(10),
    status VARCHAR(20) DEFAULT 'active'
);

CREATE INDEX idx_connect_sessions_machine ON connect_sessions(machine_id);
CREATE INDEX idx_connect_sessions_started ON connect_sessions(started_at DESC);
CREATE INDEX idx_connect_sessions_support_code ON connect_sessions(support_code);

-- Session events (audit log)
CREATE TABLE connect_session_events (
    id BIGSERIAL PRIMARY KEY,
    session_id UUID REFERENCES connect_sessions(id) ON DELETE CASCADE,
    event_type VARCHAR(50) NOT NULL,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    viewer_id VARCHAR(255),
    viewer_name VARCHAR(255),
    details JSONB,
    ip_address INET
);

CREATE INDEX idx_connect_events_session ON connect_session_events(session_id);
CREATE INDEX idx_connect_events_time ON connect_session_events(timestamp DESC);
CREATE INDEX idx_connect_events_type ON connect_session_events(event_type);

-- Support codes
CREATE TABLE connect_support_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code VARCHAR(10) UNIQUE NOT NULL,
    session_id UUID,
    created_by VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    expires_at TIMESTAMPTZ,
    status VARCHAR(20) DEFAULT 'pending',
    client_name VARCHAR(255),
    client_machine VARCHAR(255),
    connected_at TIMESTAMPTZ
);

CREATE INDEX idx_support_codes_code ON connect_support_codes(code);
CREATE INDEX idx_support_codes_status ON connect_support_codes(status);
CREATE INDEX idx_support_codes_session ON connect_support_codes(session_id);

-- Trigger for updated_at
CREATE OR REPLACE FUNCTION update_connect_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_connect_machines_updated_at
    BEFORE UPDATE ON connect_machines
    FOR EACH ROW
    EXECUTE FUNCTION update_connect_updated_at();
```

---

## 3. Rust db Module Structure

### Directory Structure

```
src/db/
    mod.rs           -- Module exports and Database struct with pool
    machines.rs      -- Machine CRUD operations
    sessions.rs      -- Session logging operations
    events.rs        -- Audit event logging
    support_codes.rs -- Support code persistence
```

### `src/db/mod.rs` (Updated)

```rust
//! Database module for GuruConnect
//!
//! Handles persistence for machines, sessions, and audit logging.

pub mod machines;
pub mod sessions;
pub mod events;
pub mod support_codes;

use anyhow::Result;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;

pub use machines::*;
pub use sessions::*;
pub use events::*;
pub use support_codes::*;

/// Database connection pool wrapper
#[derive(Clone)]
pub struct Database {
    pool: PgPool,
}

impl Database {
    /// Initialize database connection pool
    pub async fn connect(database_url: &str, max_connections: u32) -> Result<Self> {
        let pool = PgPoolOptions::new()
            .max_connections(max_connections)
            .connect(database_url)
            .await?;

        Ok(Self { pool })
    }

    /// Run database migrations
    pub async fn migrate(&self) -> Result<()> {
        sqlx::migrate!("./migrations").run(&self.pool).await?;
        Ok(())
    }

    /// Get reference to the connection pool
    pub fn pool(&self) -> &PgPool {
        &self.pool
    }
}
```

### `src/db/machines.rs`

```rust
//! Machine/Agent database operations

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Machine {
    pub id: Uuid,
    pub agent_id: String,
    pub hostname: String,
    pub os_version: Option<String>,
    pub is_elevated: bool,
    pub is_persistent: bool,
    pub first_seen: DateTime<Utc>,
    pub last_seen: DateTime<Utc>,
    pub last_session_id: Option<Uuid>,
    pub status: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Get or create a machine by agent_id
pub async fn upsert_machine(
    pool: &PgPool,
    agent_id: &str,
    hostname: &str,
    is_persistent: bool,
) -> Result<Machine, sqlx::Error> {
    sqlx::query_as::<_, Machine>(
        r#"
        INSERT INTO connect_machines (agent_id, hostname, is_persistent, status, last_seen)
        VALUES ($1, $2, $3, 'online', NOW())
        ON CONFLICT (agent_id) DO UPDATE SET
            hostname = EXCLUDED.hostname,
            status = 'online',
            last_seen = NOW()
        RETURNING *
        "#,
    )
    .bind(agent_id)
    .bind(hostname)
    .bind(is_persistent)
    .fetch_one(pool)
    .await
}

/// Update machine status and info
pub async fn update_machine_status(
    pool: &PgPool,
    agent_id: &str,
    status: &str,
    os_version: Option<&str>,
    is_elevated: bool,
    session_id: Option<Uuid>,
) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        UPDATE connect_machines SET
            status = $1,
            os_version = COALESCE($2, os_version),
            is_elevated = $3,
            last_seen = NOW(),
            last_session_id = COALESCE($4, last_session_id)
        WHERE agent_id = $5
        "#,
    )
    .bind(status)
    .bind(os_version)
    .bind(is_elevated)
    .bind(session_id)
    .bind(agent_id)
    .execute(pool)
    .await?;
    Ok(())
}

/// Get all machines (for restore on startup)
pub async fn get_all_machines(pool: &PgPool) -> Result<Vec<Machine>, sqlx::Error> {
    sqlx::query_as::<_, Machine>(
        "SELECT * FROM connect_machines WHERE is_persistent = true ORDER BY hostname"
    )
    .fetch_all(pool)
    .await
}

/// Get machine by agent_id
pub async fn get_machine_by_agent_id(
    pool: &PgPool,
    agent_id: &str,
) -> Result<Option<Machine>, sqlx::Error> {
    sqlx::query_as::<_, Machine>(
        "SELECT * FROM connect_machines WHERE agent_id = $1"
    )
    .bind(agent_id)
    .fetch_optional(pool)
    .await
}

/// Mark machine as offline
pub async fn mark_machine_offline(pool: &PgPool, agent_id: &str) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE connect_machines SET status = 'offline' WHERE agent_id = $1")
        .bind(agent_id)
        .execute(pool)
        .await?;
    Ok(())
}
```

### `src/db/sessions.rs`

```rust
//! Session database operations

use chrono::{DateTime, Utc};
use sqlx::PgPool;
use uuid::Uuid;

#[derive(Debug, Clone, sqlx::FromRow)]
pub struct DbSession {
    pub id: Uuid,
    pub machine_id: Uuid,
    pub started_at: DateTime<Utc>,
    pub ended_at: Option<DateTime<Utc>>,
    pub duration_secs: Option<i32>,
    pub is_support_session: bool,
    pub support_code: Option<String>,
    pub status: String,
}

/// Create a new session record
pub async fn create_session(
    pool: &PgPool,
    session_id: Uuid,
    machine_id: Uuid,
    is_support_session: bool,
    support_code: Option<&str>,
) -> Result<DbSession, sqlx::Error> {
    sqlx::query_as::<_, DbSession>(
        r#"
        INSERT INTO connect_sessions (id, machine_id, is_support_session, support_code, status)
        VALUES ($1, $2, $3, $4, 'active')
        RETURNING *
        "#,
    )
    .bind(session_id)
    .bind(machine_id)
    .bind(is_support_session)
    .bind(support_code)
    .fetch_one(pool)
    .await
}

/// End a session
pub async fn end_session(
    pool: &PgPool,
    session_id: Uuid,
    status: &str,  // 'ended' or 'disconnected'
) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        UPDATE connect_sessions SET
            ended_at = NOW(),
            duration_secs = EXTRACT(EPOCH FROM (NOW() - started_at))::INTEGER,
            status = $1
        WHERE id = $2
        "#,
    )
    .bind(status)
    .bind(session_id)
    .execute(pool)
    .await?;
    Ok(())
}

/// Get session by ID
pub async fn get_session(pool: &PgPool, session_id: Uuid) -> Result<Option<DbSession>, sqlx::Error> {
    sqlx::query_as::<_, DbSession>("SELECT * FROM connect_sessions WHERE id = $1")
        .bind(session_id)
        .fetch_optional(pool)
        .await
}
```

### `src/db/events.rs`

```rust
//! Audit event logging

use serde_json::Value as JsonValue;
use sqlx::PgPool;
use uuid::Uuid;
use std::net::IpAddr;

/// Log a session event
pub async fn log_event(
    pool: &PgPool,
    session_id: Uuid,
    event_type: &str,
    viewer_id: Option<&str>,
    viewer_name: Option<&str>,
    details: Option<JsonValue>,
    ip_address: Option<IpAddr>,
) -> Result<i64, sqlx::Error> {
    let result = sqlx::query_scalar::<_, i64>(
        r#"
        INSERT INTO connect_session_events
            (session_id, event_type, viewer_id, viewer_name, details, ip_address)
        VALUES ($1, $2, $3, $4, $5, $6)
        RETURNING id
        "#,
    )
    .bind(session_id)
    .bind(event_type)
    .bind(viewer_id)
    .bind(viewer_name)
    .bind(details)
    .bind(ip_address.map(|ip| ip.to_string()))
    .fetch_one(pool)
    .await?;
    Ok(result)
}

/// Get events for a session
pub async fn get_session_events(
    pool: &PgPool,
    session_id: Uuid,
) -> Result<Vec<SessionEvent>, sqlx::Error> {
    sqlx::query_as::<_, SessionEvent>(
        "SELECT * FROM connect_session_events WHERE session_id = $1 ORDER BY timestamp"
    )
    .bind(session_id)
    .fetch_all(pool)
    .await
}

#[derive(Debug, Clone, sqlx::FromRow, serde::Serialize)]
pub struct SessionEvent {
    pub id: i64,
    pub session_id: Uuid,
    pub event_type: String,
    pub timestamp: chrono::DateTime<chrono::Utc>,
    pub viewer_id: Option<String>,
    pub viewer_name: Option<String>,
    pub details: Option<JsonValue>,
    pub ip_address: Option<String>,
}
```

### `src/db/support_codes.rs`

```rust
//! Support code persistence

use chrono::{DateTime, Utc};
use sqlx::PgPool;
use uuid::Uuid;

#[derive(Debug, Clone, sqlx::FromRow, serde::Serialize)]
pub struct DbSupportCode {
    pub id: Uuid,
    pub code: String,
    pub session_id: Option<Uuid>,
    pub created_by: String,
    pub created_at: DateTime<Utc>,
    pub expires_at: Option<DateTime<Utc>>,
    pub status: String,
    pub client_name: Option<String>,
    pub client_machine: Option<String>,
    pub connected_at: Option<DateTime<Utc>>,
}

/// Create a new support code
pub async fn create_support_code(
    pool: &PgPool,
    code: &str,
    created_by: &str,
) -> Result<DbSupportCode, sqlx::Error> {
    sqlx::query_as::<_, DbSupportCode>(
        r#"
        INSERT INTO connect_support_codes (code, created_by, status)
        VALUES ($1, $2, 'pending')
        RETURNING *
        "#,
    )
    .bind(code)
    .bind(created_by)
    .fetch_one(pool)
    .await
}

/// Get support code by code string
pub async fn get_support_code(pool: &PgPool, code: &str) -> Result<Option<DbSupportCode>, sqlx::Error> {
    sqlx::query_as::<_, DbSupportCode>(
        "SELECT * FROM connect_support_codes WHERE code = $1"
    )
    .bind(code)
    .fetch_optional(pool)
    .await
}

/// Update support code status
pub async fn update_support_code_status(
    pool: &PgPool,
    code: &str,
    status: &str,
    session_id: Option<Uuid>,
    client_name: Option<&str>,
    client_machine: Option<&str>,
) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        UPDATE connect_support_codes SET
            status = $1,
            session_id = COALESCE($2, session_id),
            client_name = COALESCE($3, client_name),
            client_machine = COALESCE($4, client_machine),
            connected_at = CASE WHEN $1 = 'connected' THEN NOW() ELSE connected_at END
        WHERE code = $5
        "#,
    )
    .bind(status)
    .bind(session_id)
    .bind(client_name)
    .bind(client_machine)
    .bind(code)
    .execute(pool)
    .await?;
    Ok(())
}

/// Get active support codes (pending or connected)
pub async fn get_active_support_codes(pool: &PgPool) -> Result<Vec<DbSupportCode>, sqlx::Error> {
    sqlx::query_as::<_, DbSupportCode>(
        "SELECT * FROM connect_support_codes WHERE status IN ('pending', 'connected') ORDER BY created_at DESC"
    )
    .fetch_all(pool)
    .await
}

/// Check if code exists and is valid
pub async fn is_code_valid(pool: &PgPool, code: &str) -> Result<bool, sqlx::Error> {
    let result = sqlx::query_scalar::<_, bool>(
        "SELECT EXISTS(SELECT 1 FROM connect_support_codes WHERE code = $1 AND status IN ('pending', 'connected'))"
    )
    .bind(code)
    .fetch_one(pool)
    .await?;
    Ok(result)
}
```

---

## 4. Integration Points

### 4.1 Update `AppState` in `main.rs`

```rust
// Add to AppState struct
pub struct AppState {
    sessions: session::SessionManager,
    support_codes: SupportCodeManager,
    db: Option<db::Database>,  // Optional for backward compatibility
}

// In main():
let db = if let Some(url) = config.database_url.as_ref() {
    let database = db::Database::connect(url, 5).await?;
    database.migrate().await?;

    // Restore persistent machines from database
    let machines = db::machines::get_all_machines(database.pool()).await?;
    for machine in machines {
        // Pre-populate SessionManager with offline sessions for persistent machines
        sessions.restore_offline_machine(&machine.agent_id, &machine.hostname).await;
    }

    Some(database)
} else {
    info!("No DATABASE_URL set, running without persistence");
    None
};
```

### 4.2 Update `SessionManager` in `session/mod.rs`

Add method for restoring offline machines on startup:

```rust
impl SessionManager {
    /// Restore a machine as an offline session (called on startup from database)
    pub async fn restore_offline_machine(&self, agent_id: &str, hostname: &str) -> SessionId {
        let session_id = Uuid::new_v4();
        let now = chrono::Utc::now();

        let session = Session {
            id: session_id,
            agent_id: agent_id.to_string(),
            agent_name: hostname.to_string(),
            started_at: now,
            viewer_count: 0,
            viewers: Vec::new(),
            is_streaming: false,
            is_online: false,  // Offline until agent reconnects
            is_persistent: true,
            last_heartbeat: now,
            os_version: None,
            is_elevated: false,
            uptime_secs: 0,
            display_count: 1,
        };

        // Create placeholder channels (will be replaced on reconnect)
        let (frame_tx, _) = broadcast::channel(16);
        let (input_tx, input_rx) = tokio::sync::mpsc::channel(64);

        let session_data = SessionData {
            info: session,
            frame_tx,
            input_tx,
            input_rx: Some(input_rx),
            viewers: HashMap::new(),
            last_heartbeat_instant: Instant::now(),
        };

        let mut sessions = self.sessions.write().await;
        sessions.insert(session_id, session_data);

        let mut agents = self.agents.write().await;
        agents.insert(agent_id.to_string(), session_id);

        session_id
    }
}
```

### 4.3 Update `relay/mod.rs` - Agent Connection Handler

Add database logging at key points in `handle_agent_connection`:

```rust
async fn handle_agent_connection(
    socket: WebSocket,
    sessions: SessionManager,
    support_codes: SupportCodeManager,
    db: Option<Database>,  // Add db parameter
    agent_id: String,
    agent_name: String,
    support_code: Option<String>,
) {
    // After register_agent:
    if let Some(ref db) = db {
        // Upsert machine record
        if let Ok(machine) = db::machines::upsert_machine(
            db.pool(), &agent_id, &agent_name, is_persistent
        ).await {
            // Create session record
            let _ = db::sessions::create_session(
                db.pool(),
                session_id,
                machine.id,
                support_code.is_some(),
                support_code.as_deref(),
            ).await;

            // Log session start event
            let _ = db::events::log_event(
                db.pool(),
                session_id,
                "session_started",
                None, None, None, None,
            ).await;
        }
    }

    // ... existing code ...

    // On cleanup (after sessions_cleanup.mark_agent_disconnected):
    if let Some(ref db) = db {
        let _ = db::sessions::end_session(db.pool(), session_id, "ended").await;
        let _ = db::machines::mark_machine_offline(db.pool(), &agent_id).await;
        let _ = db::events::log_event(
            db.pool(), session_id, "session_ended",
            None, None, None, None,
        ).await;
    }
}
```

### 4.4 Update `relay/mod.rs` - Viewer Connection Handler

Add audit logging in `handle_viewer_connection`:

```rust
async fn handle_viewer_connection(
    socket: WebSocket,
    sessions: SessionManager,
    db: Option<Database>,  // Add db parameter
    session_id_str: String,
    viewer_name: String,
) {
    // After join_session:
    if let Some(ref db) = db {
        let _ = db::events::log_event(
            db.pool(),
            session_id,
            "viewer_joined",
            Some(&viewer_id),
            Some(&viewer_name),
            None,
            None,
        ).await;
    }

    // ... existing code ...

    // On cleanup (after leave_session):
    if let Some(ref db) = db {
        let _ = db::events::log_event(
            db.pool(),
            session_id,
            "viewer_left",
            Some(&viewer_id),
            Some(&viewer_name),
            None,
            None,
        ).await;
    }
}
```

### 4.5 Update `SupportCodeManager` in `support_codes.rs`

Add database backing while keeping in-memory for performance:

```rust
impl SupportCodeManager {
    /// Create a new support code (with database persistence)
    pub async fn create_code_with_db(
        &self,
        request: CreateCodeRequest,
        db: Option<&Database>,
    ) -> SupportCode {
        let code = self.generate_unique_code().await;

        // Persist to database first
        if let Some(db) = db {
            let _ = db::support_codes::create_support_code(
                db.pool(),
                &code,
                request.technician_name.as_deref().unwrap_or("Unknown"),
            ).await;
        }

        // Then add to in-memory cache (existing logic)
        // ...
    }

    /// Restore support codes from database on startup
    pub async fn restore_from_db(&self, db: &Database) -> anyhow::Result<()> {
        let codes = db::support_codes::get_active_support_codes(db.pool()).await?;
        let mut cache = self.codes.write().await;

        for db_code in codes {
            let support_code = SupportCode {
                code: db_code.code.clone(),
                session_id: db_code.session_id.unwrap_or_else(Uuid::new_v4),
                created_by: db_code.created_by,
                created_at: db_code.created_at,
                status: match db_code.status.as_str() {
                    "pending" => CodeStatus::Pending,
                    "connected" => CodeStatus::Connected,
                    "completed" => CodeStatus::Completed,
                    "cancelled" => CodeStatus::Cancelled,
                    _ => CodeStatus::Pending,
                },
                client_name: db_code.client_name,
                client_machine: db_code.client_machine,
                connected_at: db_code.connected_at,
            };
            cache.insert(db_code.code.clone(), support_code);
        }

        Ok(())
    }
}
```

---

## 5. Environment Configuration

### Update `config.rs`

The existing config already supports `DATABASE_URL` as optional. Add pool size:

```rust
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
    pub listen_addr: String,
    pub database_url: Option<String>,
    pub database_max_connections: u32,  // Add this
    pub jwt_secret: Option<String>,
    pub debug: bool,
}

impl Config {
    pub fn load() -> Result<Self> {
        Ok(Self {
            listen_addr: env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string()),
            database_url: env::var("DATABASE_URL").ok(),
            database_max_connections: env::var("DATABASE_MAX_CONNECTIONS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(5),
            jwt_secret: env::var("JWT_SECRET").ok(),
            debug: env::var("DEBUG")
                .map(|v| v == "1" || v.to_lowercase() == "true")
                .unwrap_or(false),
        })
    }
}
```

### Example `.env` file

```bash
# GuruConnect Server Configuration
LISTEN_ADDR=0.0.0.0:3002
DATABASE_URL=postgres://guruconnect:password@172.16.3.30:5432/guruconnect
DATABASE_MAX_CONNECTIONS=5
JWT_SECRET=your-secret-here
```

---

## 6. Implementation Sequence

1. **Create migrations directory and SQL file** (001_initial_schema.sql)

2. **Update `src/db/mod.rs`** with Database struct and connection pool

3. **Create db submodules** in order:
   - `machines.rs` (no dependencies)
   - `sessions.rs` (references machines)
   - `events.rs` (references sessions)
   - `support_codes.rs` (references sessions)

4. **Update `config.rs`** with database pool size setting

5. **Update `main.rs`**:
   - Add database initialization
   - Add migration running
   - Add machine restoration
   - Add support code restoration
   - Update AppState

6. **Update `session/mod.rs`**:
   - Add `restore_offline_machine` method

7. **Update `relay/mod.rs`**:
   - Pass db to handlers
   - Add event logging calls

8. **Update `support_codes.rs`**:
   - Add `create_code_with_db` method
   - Add `restore_from_db` method

---

## 7. Potential Challenges

1. **Database connection failures**: The server should continue working without persistence if DATABASE_URL is not set or database is unavailable. All db operations should be wrapped in `if let Some(db)` checks.

2. **Session ID mismatches**: On reconnect, existing session IDs from in-memory state may not match database. The `upsert_machine` approach handles this by keying on `agent_id` rather than session UUID.

3. **Support code session linking**: Support codes create a temporary session ID before the agent connects. The `link_session` method updates both database and in-memory state when the real session is created.

4. **Migration conflicts with GuruRMM**: GuruConnect tables are prefixed with `connect_` to avoid any naming conflicts if sharing the same database.

---

### Critical Files for Implementation

- **C:/Users/MikeSwanson/Claude/guru-connect/server/src/db/mod.rs** - Core database module to expand with pool and submodule exports
- **C:/Users/MikeSwanson/Claude/guru-connect/server/src/session/mod.rs** - SessionManager needs restore_offline_machine method
- **C:/Users/MikeSwanson/Claude/guru-connect/server/src/relay/mod.rs** - Add database logging at connection/disconnection points
- **C:/Users/MikeSwanson/Claude/guru-connect/server/src/main.rs** - Database initialization and startup restoration
- **C:/Users/MikeSwanson/Claude/gururmm/server/src/db/agents.rs** - Reference pattern for sqlx::FromRow structs and query patterns