Explore the GuruRMM server codebase to understand how it uses PostgreSQL:

1. Find the database connection setup (connection pool, sqlx configuration)
2. Find example database models/structs
3. Find migration patterns if any
4. Understand the database schema structure

Look in:
- /home/guru/claude-projects/gururmm/ or similar paths
- C:/Users/MikeSwanson/Claude/gururmm/

Focus on the server/API component that uses PostgreSQL. Provide file paths, code patterns, and connection string handling.


I'll help you explore the GuruRMM server codebase to understand how it uses PostgreSQL. Let me start by searching for the relevant files and directories.


Bash

cd /c/Users/MikeSwanson/Claude && find . -type d -name "gururmm" | head -20

Find GuruRMM project directories


C:\Users\MikeSwanson\Claude\gururmm\server\Cargo.toml
C:\Users\MikeSwanson\Claude\gururmm\agent\Cargo.toml


./gururmm


Great! I found the GuruRMM project. Now let me read the key files to understand the PostgreSQL setup:


     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→//! 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→[package]
     2→name = "gururmm-server"
     3→version = "0.2.0"
     4→edition = "2021"
     5→description = "GuruRMM Server - RMM management server"
     6→authors = ["GuruRMM"]
     7→
     8→[dependencies]
     9→# Web framework
    10→axum = { version = "0.7", features = ["ws", "macros"] }
    11→axum-extra = { version = "0.9", features = ["typed-header"] }
    12→tower = { version = "0.5", features = ["util", "timeout"] }
    13→tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
    14→
    15→# Async runtime
    16→tokio = { version = "1", features = ["full"] }
    17→
    18→# Database
    19→sqlx = { version = "0.8", features = [
    20→    "runtime-tokio",
    21→    "tls-native-tls",
    22→    "postgres",
    23→    "uuid",
    24→    "chrono",
    25→    "migrate"
    26→] }
    27→
    28→# Serialization
    29→serde = { version = "1", features = ["derive"] }
    30→serde_json = "1"
    31→
    32→# Configuration
    33→config = "0.14"
    34→
    35→# Authentication
    36→jsonwebtoken = "9"
    37→argon2 = "0.5"
    38→
    39→# UUID
    40→uuid = { version = "1", features = ["v4", "serde"] }
    41→
    42→# Time
    43→chrono = { version = "0.4", features = ["serde"] }
    44→
    45→# Logging
    46→tracing = "0.1"
    47→tracing-subscriber = { version = "0.3", features = ["env-filter"] }
    48→
    49→# Error handling
    50→anyhow = "1"
    51→thiserror = "1"
    52→
    53→# Random for API key generation
    54→rand = "0.8"
    55→base64 = "0.22"
    56→
    57→# Hashing for API keys
    58→sha2 = "0.10"
    59→
    60→# Semantic versioning for agent updates
    61→semver = "1"
    62→
    63→# Environment variables
    64→dotenvy = "0.15"
    65→
    66→# Futures for WebSocket
    67→futures-util = "0.3"
    68→
    69→# Pin transitive dependencies to stable versions
    70→home = "0.5.9"  # 0.5.12 requires Rust 1.88
    71→
    72→[profile.release]
    73→opt-level = 3
    74→lto = true
    75→codegen-units = 1
    76→

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

[package]
name = "gururmm-server"
version = "0.2.0"
edition = "2021"
description = "GuruRMM Server - RMM management server"
authors = ["GuruRMM"]

[dependencies]
# Web framework
axum = { version = "0.7", features = ["ws", "macros"] }
axum-extra = { version = "0.9", features = ["typed-header"] }
tower = { version = "0.5", features = ["util", "timeout"] }
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }

# Async runtime
tokio = { version = "1", features = ["full"] }

# Database
sqlx = { version = "0.8", features = [
    "runtime-tokio",
    "tls-native-tls",
    "postgres",
    "uuid",
    "chrono",
    "migrate"
] }

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

# Configuration
config = "0.14"

# Authentication
jsonwebtoken = "9"
argon2 = "0.5"

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

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

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

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

# Random for API key generation
rand = "0.8"
base64 = "0.22"

# Hashing for API keys
sha2 = "0.10"

# Semantic versioning for agent updates
semver = "1"

# Environment variables
dotenvy = "0.15"

# Futures for WebSocket
futures-util = "0.3"

# Pin transitive dependencies to stable versions
home = "0.5.9"  # 0.5.12 requires Rust 1.88

[profile.release]
opt-level = 3
lto = true
codegen-units = 1


     1→//! Server configuration
     2→//!
     3→//! Configuration is loaded from environment variables.
     4→//! Required variables:
     5→//! - DATABASE_URL: PostgreSQL connection string
     6→//! - JWT_SECRET: Secret key for JWT token signing
     7→//!
     8→//! Optional variables:
     9→//! - SERVER_HOST: Host to bind to (default: 0.0.0.0)
    10→//! - SERVER_PORT: Port to bind to (default: 3001)
    11→//! - DB_MAX_CONNECTIONS: Max database connections (default: 10)
    12→//! - DOWNLOADS_DIR: Directory containing agent binaries (default: /var/www/downloads)
    13→//! - DOWNLOADS_BASE_URL: Base URL for downloads (default: http://localhost:3001/downloads)
    14→//! - AUTO_UPDATE_ENABLED: Enable automatic agent updates (default: true)
    15→//! - UPDATE_TIMEOUT_SECS: Timeout for agent updates (default: 180)
    16→
    17→use std::path::PathBuf;
    18→
    19→use anyhow::{Context, Result};
    20→use serde::{Deserialize, Serialize};
    21→
    22→/// Root server configuration
    23→#[derive(Debug, Clone, Serialize, Deserialize)]
    24→pub struct ServerConfig {
    25→    /// Server binding configuration
    26→    pub server: ServerBindConfig,
    27→
    28→    /// Database configuration
    29→    pub database: DatabaseConfig,
    30→
    31→    /// Authentication configuration
    32→    pub auth: AuthConfig,
    33→
    34→    /// Agent updates configuration
    35→    pub updates: UpdatesConfig,
    36→}
    37→
    38→/// Server binding configuration
    39→#[derive(Debug, Clone, Serialize, Deserialize)]
    40→pub struct ServerBindConfig {
    41→    /// Host to bind to
    42→    pub host: String,
    43→
    44→    /// Port to bind to
    45→    pub port: u16,
    46→}
    47→
    48→/// Database configuration
    49→#[derive(Debug, Clone, Serialize, Deserialize)]
    50→pub struct DatabaseConfig {
    51→    /// PostgreSQL connection URL
    52→    pub url: String,
    53→
    54→    /// Maximum number of connections in the pool
    55→    pub max_connections: u32,
    56→}
    57→
    58→/// Authentication configuration
    59→#[derive(Debug, Clone, Serialize, Deserialize)]
    60→pub struct AuthConfig {
    61→    /// JWT signing secret
    62→    pub jwt_secret: String,
    63→
    64→    /// JWT token expiry in hours
    65→    pub jwt_expiry_hours: u64,
    66→
    67→    /// API key prefix for agents
    68→    pub api_key_prefix: String,
    69→}
    70→
    71→/// Agent updates configuration
    72→#[derive(Debug, Clone, Serialize, Deserialize)]
    73→pub struct UpdatesConfig {
    74→    /// Directory containing agent binaries
    75→    pub downloads_dir: PathBuf,
    76→
    77→    /// Base URL for agent downloads
    78→    pub downloads_base_url: String,
    79→
    80→    /// Enable automatic agent updates
    81→    pub auto_update_enabled: bool,
    82→
    83→    /// Timeout for agent updates in seconds
    84→    pub update_timeout_secs: u64,
    85→
    86→    /// Interval for scanning downloads directory (seconds)
    87→    pub scan_interval_secs: u64,
    88→}
    89→
    90→impl ServerConfig {
    91→    /// Load configuration from environment variables
    92→    pub fn from_env() -> Result<Self> {
    93→        let database_url =
    94→            std::env::var("DATABASE_URL").context("DATABASE_URL environment variable not set")?;
    95→
    96→        let jwt_secret =
    97→            std::env::var("JWT_SECRET").context("JWT_SECRET environment variable not set")?;
    98→
    99→        let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
   100→
   101→        let port = std::env::var("SERVER_PORT")
   102→            .ok()
   103→            .and_then(|p| p.parse().ok())
   104→            .unwrap_or(3001);
   105→
   106→        let max_connections = std::env::var("DB_MAX_CONNECTIONS")
   107→            .ok()
   108→            .and_then(|p| p.parse().ok())
   109→            .unwrap_or(10);
   110→
   111→        let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
   112→            .ok()
   113→            .and_then(|p| p.parse().ok())
   114→            .unwrap_or(24);
   115→
   116→        let api_key_prefix =
   117→            std::env::var("API_KEY_PREFIX").unwrap_or_else(|_| "grmm_".to_string());
   118→
   119→        // Updates configuration
   120→        let downloads_dir = std::env::var("DOWNLOADS_DIR")
   121→            .map(PathBuf::from)
   122→            .unwrap_or_else(|_| PathBuf::from("/var/www/downloads"));
   123→
   124→        let downloads_base_url = std::env::var("DOWNLOADS_BASE_URL")
   125→            .unwrap_or_else(|_| format!("http://{}:{}/downloads", host, port));
   126→
   127→        let auto_update_enabled = std::env::var("AUTO_UPDATE_ENABLED")
   128→            .map(|v| v.to_lowercase() == "true" || v == "1")
   129→            .unwrap_or(true);
   130→
   131→        let update_timeout_secs = std::env::var("UPDATE_TIMEOUT_SECS")
   132→            .ok()
   133→            .and_then(|p| p.parse().ok())
   134→            .unwrap_or(180);
   135→
   136→        let scan_interval_secs = std::env::var("SCAN_INTERVAL_SECS")
   137→            .ok()
   138→            .and_then(|p| p.parse().ok())
   139→            .unwrap_or(300); // 5 minutes
   140→
   141→        Ok(Self {
   142→            server: ServerBindConfig { host, port },
   143→            database: DatabaseConfig {
   144→                url: database_url,
   145→                max_connections,
   146→            },
   147→            auth: AuthConfig {
   148→                jwt_secret,
   149→                jwt_expiry_hours,
   150→                api_key_prefix,
   151→            },
   152→            updates: UpdatesConfig {
   153→                downloads_dir,
   154→                downloads_base_url,
   155→                auto_update_enabled,
   156→                update_timeout_secs,
   157→                scan_interval_secs,
   158→            },
   159→        })
   160→    }
   161→}
   162→

<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
//!
//! Configuration is loaded from environment variables.
//! Required variables:
//! - DATABASE_URL: PostgreSQL connection string
//! - JWT_SECRET: Secret key for JWT token signing
//!
//! Optional variables:
//! - SERVER_HOST: Host to bind to (default: 0.0.0.0)
//! - SERVER_PORT: Port to bind to (default: 3001)
//! - DB_MAX_CONNECTIONS: Max database connections (default: 10)
//! - DOWNLOADS_DIR: Directory containing agent binaries (default: /var/www/downloads)
//! - DOWNLOADS_BASE_URL: Base URL for downloads (default: http://localhost:3001/downloads)
//! - AUTO_UPDATE_ENABLED: Enable automatic agent updates (default: true)
//! - UPDATE_TIMEOUT_SECS: Timeout for agent updates (default: 180)

use std::path::PathBuf;

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

/// Root server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
    /// Server binding configuration
    pub server: ServerBindConfig,

    /// Database configuration
    pub database: DatabaseConfig,

    /// Authentication configuration
    pub auth: AuthConfig,

    /// Agent updates configuration
    pub updates: UpdatesConfig,
}

/// Server binding configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerBindConfig {
    /// Host to bind to
    pub host: String,

    /// Port to bind to
    pub port: u16,
}

/// Database configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
    /// PostgreSQL connection URL
    pub url: String,

    /// Maximum number of connections in the pool
    pub max_connections: u32,
}

/// Authentication configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
    /// JWT signing secret
    pub jwt_secret: String,

    /// JWT token expiry in hours
    pub jwt_expiry_hours: u64,

    /// API key prefix for agents
    pub api_key_prefix: String,
}

/// Agent updates configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdatesConfig {
    /// Directory containing agent binaries
    pub downloads_dir: PathBuf,

    /// Base URL for agent downloads
    pub downloads_base_url: String,

    /// Enable automatic agent updates
    pub auto_update_enabled: bool,

    /// Timeout for agent updates in seconds
    pub update_timeout_secs: u64,

    /// Interval for scanning downloads directory (seconds)
    pub scan_interval_secs: u64,
}

impl ServerConfig {
    /// Load configuration from environment variables
    pub fn from_env() -> Result<Self> {
        let database_url =
            std::env::var("DATABASE_URL").context("DATABASE_URL environment variable not set")?;

        let jwt_secret =
            std::env::var("JWT_SECRET").context("JWT_SECRET environment variable not set")?;

        let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());

        let port = std::env::var("SERVER_PORT")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(3001);

        let max_connections = std::env::var("DB_MAX_CONNECTIONS")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(10);

        let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(24);

        let api_key_prefix =
            std::env::var("API_KEY_PREFIX").unwrap_or_else(|_| "grmm_".to_string());

        // Updates configuration
        let downloads_dir = std::env::var("DOWNLOADS_DIR")
            .map(PathBuf::from)
            .unwrap_or_else(|_| PathBuf::from("/var/www/downloads"));

        let downloads_base_url = std::env::var("DOWNLOADS_BASE_URL")
            .unwrap_or_else(|_| format!("http://{}:{}/downloads", host, port));

        let auto_update_enabled = std::env::var("AUTO_UPDATE_ENABLED")
            .map(|v| v.to_lowercase() == "true" || v == "1")
            .unwrap_or(true);

        let update_timeout_secs = std::env::var("UPDATE_TIMEOUT_SECS")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(180);

        let scan_interval_secs = std::env::var("SCAN_INTERVAL_SECS")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(300); // 5 minutes

        Ok(Self {
            server: ServerBindConfig { host, port },
            database: DatabaseConfig {
                url: database_url,
                max_connections,
            },
            auth: AuthConfig {
                jwt_secret,
                jwt_expiry_hours,
                api_key_prefix,
            },
            updates: UpdatesConfig {
                downloads_dir,
                downloads_base_url,
                auto_update_enabled,
                update_timeout_secs,
                scan_interval_secs,
            },
        })
    }
}


Now let me look at the database models to understand the structure:


     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(())
}


     1→//! Users database operations
     2→
     3→use chrono::{DateTime, Utc};
     4→use serde::{Deserialize, Serialize};
     5→use sqlx::PgPool;
     6→use uuid::Uuid;
     7→
     8→/// User record from database
     9→#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
    10→pub struct User {
    11→    pub id: Uuid,
    12→    pub email: String,
    13→    #[serde(skip_serializing)]
    14→    pub password_hash: Option<String>,
    15→    pub name: Option<String>,
    16→    pub role: String,
    17→    pub sso_provider: Option<String>,
    18→    pub sso_id: Option<String>,
    19→    pub created_at: DateTime<Utc>,
    20→    pub last_login: Option<DateTime<Utc>>,
    21→}
    22→
    23→/// User response without sensitive fields
    24→#[derive(Debug, Clone, Serialize, Deserialize)]
    25→pub struct UserResponse {
    26→    pub id: Uuid,
    27→    pub email: String,
    28→    pub name: Option<String>,
    29→    pub role: String,
    30→    pub sso_provider: Option<String>,
    31→    pub created_at: DateTime<Utc>,
    32→    pub last_login: Option<DateTime<Utc>>,
    33→}
    34→
    35→impl From<User> for UserResponse {
    36→    fn from(user: User) -> Self {
    37→        Self {
    38→            id: user.id,
    39→            email: user.email,
    40→            name: user.name,
    41→            role: user.role,
    42→            sso_provider: user.sso_provider,
    43→            created_at: user.created_at,
    44→            last_login: user.last_login,
    45→        }
    46→    }
    47→}
    48→
    49→/// Create a new user (local auth)
    50→#[derive(Debug, Clone, Deserialize)]
    51→pub struct CreateUser {
    52→    pub email: String,
    53→    pub password_hash: String,
    54→    pub name: Option<String>,
    55→    pub role: Option<String>,
    56→}
    57→
    58→/// Create a new local user
    59→pub async fn create_user(pool: &PgPool, user: CreateUser) -> Result<User, sqlx::Error> {
    60→    sqlx::query_as::<_, User>(
    61→        r#"
    62→        INSERT INTO users (email, password_hash, name, role)
    63→        VALUES ($1, $2, $3, COALESCE($4, 'user'))
    64→        RETURNING *
    65→        "#,
    66→    )
    67→    .bind(&user.email)
    68→    .bind(&user.password_hash)
    69→    .bind(&user.name)
    70→    .bind(&user.role)
    71→    .fetch_one(pool)
    72→    .await
    73→}
    74→
    75→/// Create or update SSO user
    76→#[derive(Debug, Clone, Deserialize)]
    77→pub struct UpsertSsoUser {
    78→    pub email: String,
    79→    pub name: Option<String>,
    80→    pub sso_provider: String,
    81→    pub sso_id: String,
    82→}
    83→
    84→pub async fn upsert_sso_user(pool: &PgPool, user: UpsertSsoUser) -> Result<User, sqlx::Error> {
    85→    sqlx::query_as::<_, User>(
    86→        r#"
    87→        INSERT INTO users (email, name, sso_provider, sso_id, role)
    88→        VALUES ($1, $2, $3, $4, 'user')
    89→        ON CONFLICT (email)
    90→        DO UPDATE SET
    91→            name = COALESCE(EXCLUDED.name, users.name),
    92→            sso_provider = EXCLUDED.sso_provider,
    93→            sso_id = EXCLUDED.sso_id,
    94→            last_login = NOW()
    95→        RETURNING *
    96→        "#,
    97→    )
    98→    .bind(&user.email)
    99→    .bind(&user.name)
   100→    .bind(&user.sso_provider)
   101→    .bind(&user.sso_id)
   102→    .fetch_one(pool)
   103→    .await
   104→}
   105→
   106→/// Get user by ID
   107→pub async fn get_user_by_id(pool: &PgPool, id: Uuid) -> Result<Option<User>, sqlx::Error> {
   108→    sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
   109→        .bind(id)
   110→        .fetch_optional(pool)
   111→        .await
   112→}
   113→
   114→/// Get user by email
   115→pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result<Option<User>, sqlx::Error> {
   116→    sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = $1")
   117→        .bind(email)
   118→        .fetch_optional(pool)
   119→        .await
   120→}
   121→
   122→/// Get all users
   123→pub async fn get_all_users(pool: &PgPool) -> Result<Vec<User>, sqlx::Error> {
   124→    sqlx::query_as::<_, User>("SELECT * FROM users ORDER BY email")
   125→        .fetch_all(pool)
   126→        .await
   127→}
   128→
   129→/// Update last login timestamp
   130→pub async fn update_last_login(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
   131→    sqlx::query("UPDATE users SET last_login = NOW() WHERE id = $1")
   132→        .bind(id)
   133→        .execute(pool)
   134→        .await?;
   135→    Ok(())
   136→}
   137→
   138→/// Update user role
   139→pub async fn update_user_role(pool: &PgPool, id: Uuid, role: &str) -> Result<(), sqlx::Error> {
   140→    sqlx::query("UPDATE users SET role = $1 WHERE id = $2")
   141→        .bind(role)
   142→        .bind(id)
   143→        .execute(pool)
   144→        .await?;
   145→    Ok(())
   146→}
   147→
   148→/// Update user password
   149→pub async fn update_user_password(
   150→    pool: &PgPool,
   151→    id: Uuid,
   152→    password_hash: &str,
   153→) -> Result<(), sqlx::Error> {
   154→    sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2")
   155→        .bind(password_hash)
   156→        .bind(id)
   157→        .execute(pool)
   158→        .await?;
   159→    Ok(())
   160→}
   161→
   162→/// Delete a user
   163→pub async fn delete_user(pool: &PgPool, id: Uuid) -> Result<bool, sqlx::Error> {
   164→    let result = sqlx::query("DELETE FROM users WHERE id = $1")
   165→        .bind(id)
   166→        .execute(pool)
   167→        .await?;
   168→    Ok(result.rows_affected() > 0)
   169→}
   170→
   171→/// Check if any users exist (for initial setup)
   172→pub async fn has_users(pool: &PgPool) -> Result<bool, sqlx::Error> {
   173→    let result: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
   174→        .fetch_one(pool)
   175→        .await?;
   176→    Ok(result.0 > 0)
   177→}
   178→

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

//! Users database operations

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

/// User record from database
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
    pub id: Uuid,
    pub email: String,
    #[serde(skip_serializing)]
    pub password_hash: Option<String>,
    pub name: Option<String>,
    pub role: String,
    pub sso_provider: Option<String>,
    pub sso_id: Option<String>,
    pub created_at: DateTime<Utc>,
    pub last_login: Option<DateTime<Utc>>,
}

/// User response without sensitive fields
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserResponse {
    pub id: Uuid,
    pub email: String,
    pub name: Option<String>,
    pub role: String,
    pub sso_provider: Option<String>,
    pub created_at: DateTime<Utc>,
    pub last_login: Option<DateTime<Utc>>,
}

impl From<User> for UserResponse {
    fn from(user: User) -> Self {
        Self {
            id: user.id,
            email: user.email,
            name: user.name,
            role: user.role,
            sso_provider: user.sso_provider,
            created_at: user.created_at,
            last_login: user.last_login,
        }
    }
}

/// Create a new user (local auth)
#[derive(Debug, Clone, Deserialize)]
pub struct CreateUser {
    pub email: String,
    pub password_hash: String,
    pub name: Option<String>,
    pub role: Option<String>,
}

/// Create a new local user
pub async fn create_user(pool: &PgPool, user: CreateUser) -> Result<User, sqlx::Error> {
    sqlx::query_as::<_, User>(
        r#"
        INSERT INTO users (email, password_hash, name, role)
        VALUES ($1, $2, $3, COALESCE($4, 'user'))
        RETURNING *
        "#,
    )
    .bind(&user.email)
    .bind(&user.password_hash)
    .bind(&user.name)
    .bind(&user.role)
    .fetch_one(pool)
    .await
}

/// Create or update SSO user
#[derive(Debug, Clone, Deserialize)]
pub struct UpsertSsoUser {
    pub email: String,
    pub name: Option<String>,
    pub sso_provider: String,
    pub sso_id: String,
}

pub async fn upsert_sso_user(pool: &PgPool, user: UpsertSsoUser) -> Result<User, sqlx::Error> {
    sqlx::query_as::<_, User>(
        r#"
        INSERT INTO users (email, name, sso_provider, sso_id, role)
        VALUES ($1, $2, $3, $4, 'user')
        ON CONFLICT (email)
        DO UPDATE SET
            name = COALESCE(EXCLUDED.name, users.name),
            sso_provider = EXCLUDED.sso_provider,
            sso_id = EXCLUDED.sso_id,
            last_login = NOW()
        RETURNING *
        "#,
    )
    .bind(&user.email)
    .bind(&user.name)
    .bind(&user.sso_provider)
    .bind(&user.sso_id)
    .fetch_one(pool)
    .await
}

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

/// Get user by email
pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result<Option<User>, sqlx::Error> {
    sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = $1")
        .bind(email)
        .fetch_optional(pool)
        .await
}

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

/// Update last login timestamp
pub async fn update_last_login(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE users SET last_login = NOW() WHERE id = $1")
        .bind(id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Update user role
pub async fn update_user_role(pool: &PgPool, id: Uuid, role: &str) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE users SET role = $1 WHERE id = $2")
        .bind(role)
        .bind(id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Update user password
pub async fn update_user_password(
    pool: &PgPool,
    id: Uuid,
    password_hash: &str,
) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2")
        .bind(password_hash)
        .bind(id)
        .execute(pool)
        .await?;
    Ok(())
}

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

/// Check if any users exist (for initial setup)
pub async fn has_users(pool: &PgPool) -> Result<bool, sqlx::Error> {
    let result: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
        .fetch_one(pool)
        .await?;
    Ok(result.0 > 0)
}


     1→//! Client (organization) database operations
     2→
     3→use serde::{Deserialize, Serialize};
     4→use sqlx::PgPool;
     5→use uuid::Uuid;
     6→
     7→/// Client record from database
     8→#[derive(Debug, Clone, sqlx::FromRow)]
     9→pub struct Client {
    10→    pub id: Uuid,
    11→    pub name: String,
    12→    pub code: Option<String>,
    13→    pub notes: Option<String>,
    14→    pub is_active: bool,
    15→    pub created_at: chrono::DateTime<chrono::Utc>,
    16→    pub updated_at: chrono::DateTime<chrono::Utc>,
    17→}
    18→
    19→/// Client response for API
    20→#[derive(Debug, Clone, Serialize)]
    21→pub struct ClientResponse {
    22→    pub id: Uuid,
    23→    pub name: String,
    24→    pub code: Option<String>,
    25→    pub notes: Option<String>,
    26→    pub is_active: bool,
    27→    pub created_at: chrono::DateTime<chrono::Utc>,
    28→    pub site_count: Option<i64>,
    29→    pub agent_count: Option<i64>,
    30→}
    31→
    32→impl From<Client> for ClientResponse {
    33→    fn from(c: Client) -> Self {
    34→        ClientResponse {
    35→            id: c.id,
    36→            name: c.name,
    37→            code: c.code,
    38→            notes: c.notes,
    39→            is_active: c.is_active,
    40→            created_at: c.created_at,
    41→            site_count: None,
    42→            agent_count: None,
    43→        }
    44→    }
    45→}
    46→
    47→/// Data for creating a new client
    48→#[derive(Debug, Deserialize)]
    49→pub struct CreateClient {
    50→    pub name: String,
    51→    pub code: Option<String>,
    52→    pub notes: Option<String>,
    53→}
    54→
    55→/// Data for updating a client
    56→#[derive(Debug, Deserialize)]
    57→pub struct UpdateClient {
    58→    pub name: Option<String>,
    59→    pub code: Option<String>,
    60→    pub notes: Option<String>,
    61→    pub is_active: Option<bool>,
    62→}
    63→
    64→/// Create a new client
    65→pub async fn create_client(pool: &PgPool, client: CreateClient) -> Result<Client, sqlx::Error> {
    66→    sqlx::query_as::<_, Client>(
    67→        r#"
    68→        INSERT INTO clients (name, code, notes)
    69→        VALUES ($1, $2, $3)
    70→        RETURNING *
    71→        "#,
    72→    )
    73→    .bind(&client.name)
    74→    .bind(&client.code)
    75→    .bind(&client.notes)
    76→    .fetch_one(pool)
    77→    .await
    78→}
    79→
    80→/// Get a client by ID
    81→pub async fn get_client_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Client>, sqlx::Error> {
    82→    sqlx::query_as::<_, Client>("SELECT * FROM clients WHERE id = $1")
    83→        .bind(id)
    84→        .fetch_optional(pool)
    85→        .await
    86→}
    87→
    88→/// Get all clients
    89→pub async fn get_all_clients(pool: &PgPool) -> Result<Vec<Client>, sqlx::Error> {
    90→    sqlx::query_as::<_, Client>("SELECT * FROM clients ORDER BY name")
    91→        .fetch_all(pool)
    92→        .await
    93→}
    94→
    95→/// Get all clients with counts
    96→#[derive(Debug, Clone, sqlx::FromRow)]
    97→pub struct ClientWithCounts {
    98→    pub id: Uuid,
    99→    pub name: String,
   100→    pub code: Option<String>,
   101→    pub notes: Option<String>,
   102→    pub is_active: bool,
   103→    pub created_at: chrono::DateTime<chrono::Utc>,
   104→    pub updated_at: chrono::DateTime<chrono::Utc>,
   105→    pub site_count: i64,
   106→    pub agent_count: i64,
   107→}
   108→
   109→pub async fn get_all_clients_with_counts(pool: &PgPool) -> Result<Vec<ClientWithCounts>, sqlx::Error> {
   110→    sqlx::query_as::<_, ClientWithCounts>(
   111→        r#"
   112→        SELECT
   113→            c.*,
   114→            COALESCE((SELECT COUNT(*) FROM sites WHERE client_id = c.id), 0) as site_count,
   115→            COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as agent_count
   116→        FROM clients c
   117→        ORDER BY c.name
   118→        "#,
   119→    )
   120→    .fetch_all(pool)
   121→    .await
   122→}
   123→
   124→/// Update a client
   125→pub async fn update_client(
   126→    pool: &PgPool,
   127→    id: Uuid,
   128→    update: UpdateClient,
   129→) -> Result<Option<Client>, sqlx::Error> {
   130→    sqlx::query_as::<_, Client>(
   131→        r#"
   132→        UPDATE clients
   133→        SET name = COALESCE($1, name),
   134→            code = COALESCE($2, code),
   135→            notes = COALESCE($3, notes),
   136→            is_active = COALESCE($4, is_active)
   137→        WHERE id = $5
   138→        RETURNING *
   139→        "#,
   140→    )
   141→    .bind(&update.name)
   142→    .bind(&update.code)
   143→    .bind(&update.notes)
   144→    .bind(&update.is_active)
   145→    .bind(id)
   146→    .fetch_optional(pool)
   147→    .await
   148→}
   149→
   150→/// Delete a client
   151→pub async fn delete_client(pool: &PgPool, id: Uuid) -> Result<bool, sqlx::Error> {
   152→    let result = sqlx::query("DELETE FROM clients WHERE id = $1")
   153→        .bind(id)
   154→        .execute(pool)
   155→        .await?;
   156→    Ok(result.rows_affected() > 0)
   157→}
   158→

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

//! Client (organization) database operations

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

/// Client record from database
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Client {
    pub id: Uuid,
    pub name: String,
    pub code: Option<String>,
    pub notes: Option<String>,
    pub is_active: bool,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

/// Client response for API
#[derive(Debug, Clone, Serialize)]
pub struct ClientResponse {
    pub id: Uuid,
    pub name: String,
    pub code: Option<String>,
    pub notes: Option<String>,
    pub is_active: bool,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub site_count: Option<i64>,
    pub agent_count: Option<i64>,
}

impl From<Client> for ClientResponse {
    fn from(c: Client) -> Self {
        ClientResponse {
            id: c.id,
            name: c.name,
            code: c.code,
            notes: c.notes,
            is_active: c.is_active,
            created_at: c.created_at,
            site_count: None,
            agent_count: None,
        }
    }
}

/// Data for creating a new client
#[derive(Debug, Deserialize)]
pub struct CreateClient {
    pub name: String,
    pub code: Option<String>,
    pub notes: Option<String>,
}

/// Data for updating a client
#[derive(Debug, Deserialize)]
pub struct UpdateClient {
    pub name: Option<String>,
    pub code: Option<String>,
    pub notes: Option<String>,
    pub is_active: Option<bool>,
}

/// Create a new client
pub async fn create_client(pool: &PgPool, client: CreateClient) -> Result<Client, sqlx::Error> {
    sqlx::query_as::<_, Client>(
        r#"
        INSERT INTO clients (name, code, notes)
        VALUES ($1, $2, $3)
        RETURNING *
        "#,
    )
    .bind(&client.name)
    .bind(&client.code)
    .bind(&client.notes)
    .fetch_one(pool)
    .await
}

/// Get a client by ID
pub async fn get_client_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Client>, sqlx::Error> {
    sqlx::query_as::<_, Client>("SELECT * FROM clients WHERE id = $1")
        .bind(id)
        .fetch_optional(pool)
        .await
}

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

/// Get all clients with counts
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct ClientWithCounts {
    pub id: Uuid,
    pub name: String,
    pub code: Option<String>,
    pub notes: Option<String>,
    pub is_active: bool,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
    pub site_count: i64,
    pub agent_count: i64,
}

pub async fn get_all_clients_with_counts(pool: &PgPool) -> Result<Vec<ClientWithCounts>, sqlx::Error> {
    sqlx::query_as::<_, ClientWithCounts>(
        r#"
        SELECT
            c.*,
            COALESCE((SELECT COUNT(*) FROM sites WHERE client_id = c.id), 0) as site_count,
            COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as agent_count
        FROM clients c
        ORDER BY c.name
        "#,
    )
    .fetch_all(pool)
    .await
}

/// Update a client
pub async fn update_client(
    pool: &PgPool,
    id: Uuid,
    update: UpdateClient,
) -> Result<Option<Client>, sqlx::Error> {
    sqlx::query_as::<_, Client>(
        r#"
        UPDATE clients
        SET name = COALESCE($1, name),
            code = COALESCE($2, code),
            notes = COALESCE($3, notes),
            is_active = COALESCE($4, is_active)
        WHERE id = $5
        RETURNING *
        "#,
    )
    .bind(&update.name)
    .bind(&update.code)
    .bind(&update.notes)
    .bind(&update.is_active)
    .bind(id)
    .fetch_optional(pool)
    .await
}

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


     1→//! Metrics database operations
     2→
     3→use chrono::{DateTime, Utc};
     4→use serde::{Deserialize, Serialize};
     5→use sqlx::PgPool;
     6→use uuid::Uuid;
     7→
     8→/// Metrics record from database
     9→#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
    10→pub struct Metrics {
    11→    pub id: i64,
    12→    pub agent_id: Uuid,
    13→    pub timestamp: DateTime<Utc>,
    14→    pub cpu_percent: Option<f32>,
    15→    pub memory_percent: Option<f32>,
    16→    pub memory_used_bytes: Option<i64>,
    17→    pub disk_percent: Option<f32>,
    18→    pub disk_used_bytes: Option<i64>,
    19→    pub network_rx_bytes: Option<i64>,
    20→    pub network_tx_bytes: Option<i64>,
    21→    // Extended metrics
    22→    pub uptime_seconds: Option<i64>,
    23→    pub boot_time: Option<i64>,
    24→    pub logged_in_user: Option<String>,
    25→    pub user_idle_seconds: Option<i64>,
    26→    pub public_ip: Option<String>,
    27→    pub memory_total_bytes: Option<i64>,
    28→    pub disk_total_bytes: Option<i64>,
    29→}
    30→
    31→/// Create metrics data from agent
    32→#[derive(Debug, Clone, Deserialize)]
    33→pub struct CreateMetrics {
    34→    pub agent_id: Uuid,
    35→    pub cpu_percent: Option<f32>,
    36→    pub memory_percent: Option<f32>,
    37→    pub memory_used_bytes: Option<i64>,
    38→    pub disk_percent: Option<f32>,
    39→    pub disk_used_bytes: Option<i64>,
    40→    pub network_rx_bytes: Option<i64>,
    41→    pub network_tx_bytes: Option<i64>,
    42→    // Extended metrics
    43→    pub uptime_seconds: Option<i64>,
    44→    pub boot_time: Option<i64>,
    45→    pub logged_in_user: Option<String>,
    46→    pub user_idle_seconds: Option<i64>,
    47→    pub public_ip: Option<String>,
    48→    pub memory_total_bytes: Option<i64>,
    49→    pub disk_total_bytes: Option<i64>,
    50→}
    51→
    52→/// Insert metrics into the database
    53→pub async fn insert_metrics(pool: &PgPool, metrics: CreateMetrics) -> Result<i64, sqlx::Error> {
    54→    let result: (i64,) = sqlx::query_as(
    55→        r#"
    56→        INSERT INTO metrics (
    57→            agent_id, cpu_percent, memory_percent, memory_used_bytes,
    58→            disk_percent, disk_used_bytes, network_rx_bytes, network_tx_bytes,
    59→            uptime_seconds, boot_time, logged_in_user, user_idle_seconds,
    60→            public_ip, memory_total_bytes, disk_total_bytes
    61→        )
    62→        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
    63→        RETURNING id
    64→        "#,
    65→    )
    66→    .bind(metrics.agent_id)
    67→    .bind(metrics.cpu_percent)
    68→    .bind(metrics.memory_percent)
    69→    .bind(metrics.memory_used_bytes)
    70→    .bind(metrics.disk_percent)
    71→    .bind(metrics.disk_used_bytes)
    72→    .bind(metrics.network_rx_bytes)
    73→    .bind(metrics.network_tx_bytes)
    74→    .bind(metrics.uptime_seconds)
    75→    .bind(metrics.boot_time)
    76→    .bind(&metrics.logged_in_user)
    77→    .bind(metrics.user_idle_seconds)
    78→    .bind(&metrics.public_ip)
    79→    .bind(metrics.memory_total_bytes)
    80→    .bind(metrics.disk_total_bytes)
    81→    .fetch_one(pool)
    82→    .await?;
    83→
    84→    Ok(result.0)
    85→}
    86→
    87→/// Get recent metrics for an agent
    88→pub async fn get_agent_metrics(
    89→    pool: &PgPool,
    90→    agent_id: Uuid,
    91→    limit: i64,
    92→) -> Result<Vec<Metrics>, sqlx::Error> {
    93→    sqlx::query_as::<_, Metrics>(
    94→        r#"
    95→        SELECT * FROM metrics
    96→        WHERE agent_id = $1
    97→        ORDER BY timestamp DESC
    98→        LIMIT $2
    99→        "#,
   100→    )
   101→    .bind(agent_id)
   102→    .bind(limit)
   103→    .fetch_all(pool)
   104→    .await
   105→}
   106→
   107→/// Get metrics for an agent within a time range
   108→pub async fn get_agent_metrics_range(
   109→    pool: &PgPool,
   110→    agent_id: Uuid,
   111→    start: DateTime<Utc>,
   112→    end: DateTime<Utc>,
   113→) -> Result<Vec<Metrics>, sqlx::Error> {
   114→    sqlx::query_as::<_, Metrics>(
   115→        r#"
   116→        SELECT * FROM metrics
   117→        WHERE agent_id = $1 AND timestamp >= $2 AND timestamp <= $3
   118→        ORDER BY timestamp ASC
   119→        "#,
   120→    )
   121→    .bind(agent_id)
   122→    .bind(start)
   123→    .bind(end)
   124→    .fetch_all(pool)
   125→    .await
   126→}
   127→
   128→/// Get latest metrics for an agent
   129→pub async fn get_latest_metrics(
   130→    pool: &PgPool,
   131→    agent_id: Uuid,
   132→) -> Result<Option<Metrics>, sqlx::Error> {
   133→    sqlx::query_as::<_, Metrics>(
   134→        r#"
   135→        SELECT * FROM metrics
   136→        WHERE agent_id = $1
   137→        ORDER BY timestamp DESC
   138→        LIMIT 1
   139→        "#,
   140→    )
   141→    .bind(agent_id)
   142→    .fetch_optional(pool)
   143→    .await
   144→}
   145→
   146→/// Delete old metrics (for cleanup jobs)
   147→pub async fn delete_old_metrics(
   148→    pool: &PgPool,
   149→    older_than: DateTime<Utc>,
   150→) -> Result<u64, sqlx::Error> {
   151→    let result = sqlx::query("DELETE FROM metrics WHERE timestamp < $1")
   152→        .bind(older_than)
   153→        .execute(pool)
   154→        .await?;
   155→
   156→    Ok(result.rows_affected())
   157→}
   158→
   159→/// Summary statistics for dashboard
   160→#[derive(Debug, Clone, Serialize, Deserialize)]
   161→pub struct MetricsSummary {
   162→    pub avg_cpu: f32,
   163→    pub avg_memory: f32,
   164→    pub avg_disk: f32,
   165→    pub total_network_rx: i64,
   166→    pub total_network_tx: i64,
   167→}
   168→
   169→/// Get summary metrics across all agents (last hour)
   170→pub async fn get_metrics_summary(pool: &PgPool) -> Result<MetricsSummary, sqlx::Error> {
   171→    let result: (Option<f64>, Option<f64>, Option<f64>, Option<i64>, Option<i64>) = sqlx::query_as(
   172→        r#"
   173→        SELECT
   174→            AVG(cpu_percent)::float8,
   175→            AVG(memory_percent)::float8,
   176→            AVG(disk_percent)::float8,
   177→            SUM(network_rx_bytes),
   178→            SUM(network_tx_bytes)
   179→        FROM metrics
   180→        WHERE timestamp > NOW() - INTERVAL '1 hour'
   181→        "#,
   182→    )
   183→    .fetch_one(pool)
   184→    .await?;
   185→
   186→    Ok(MetricsSummary {
   187→        avg_cpu: result.0.unwrap_or(0.0) as f32,
   188→        avg_memory: result.1.unwrap_or(0.0) as f32,
   189→        avg_disk: result.2.unwrap_or(0.0) as f32,
   190→        total_network_rx: result.3.unwrap_or(0),
   191→        total_network_tx: result.4.unwrap_or(0),
   192→    })
   193→}
   194→
   195→// ============================================================================
   196→// Agent State (network interfaces, extended info snapshot)
   197→// ============================================================================
   198→
   199→/// Agent state record from database
   200→#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
   201→pub struct AgentState {
   202→    pub agent_id: Uuid,
   203→    pub network_interfaces: Option<serde_json::Value>,
   204→    pub network_state_hash: Option<String>,
   205→    pub uptime_seconds: Option<i64>,
   206→    pub boot_time: Option<i64>,
   207→    pub logged_in_user: Option<String>,
   208→    pub user_idle_seconds: Option<i64>,
   209→    pub public_ip: Option<String>,
   210→    pub network_updated_at: Option<DateTime<Utc>>,
   211→    pub metrics_updated_at: Option<DateTime<Utc>>,
   212→}
   213→
   214→/// Update or insert agent state (upsert)
   215→pub async fn upsert_agent_state(
   216→    pool: &PgPool,
   217→    agent_id: Uuid,
   218→    uptime_seconds: Option<i64>,
   219→    boot_time: Option<i64>,
   220→    logged_in_user: Option<&str>,
   221→    user_idle_seconds: Option<i64>,
   222→    public_ip: Option<&str>,
   223→) -> Result<(), sqlx::Error> {
   224→    sqlx::query(
   225→        r#"
   226→        INSERT INTO agent_state (
   227→            agent_id, uptime_seconds, boot_time, logged_in_user,
   228→            user_idle_seconds, public_ip, metrics_updated_at
   229→        )
   230→        VALUES ($1, $2, $3, $4, $5, $6, NOW())
   231→        ON CONFLICT (agent_id) DO UPDATE SET
   232→            uptime_seconds = EXCLUDED.uptime_seconds,
   233→            boot_time = EXCLUDED.boot_time,
   234→            logged_in_user = EXCLUDED.logged_in_user,
   235→            user_idle_seconds = EXCLUDED.user_idle_seconds,
   236→            public_ip = EXCLUDED.public_ip,
   237→            metrics_updated_at = NOW()
   238→        "#,
   239→    )
   240→    .bind(agent_id)
   241→    .bind(uptime_seconds)
   242→    .bind(boot_time)
   243→    .bind(logged_in_user)
   244→    .bind(user_idle_seconds)
   245→    .bind(public_ip)
   246→    .execute(pool)
   247→    .await?;
   248→
   249→    Ok(())
   250→}
   251→
   252→/// Update network state for an agent
   253→pub async fn update_agent_network_state(
   254→    pool: &PgPool,
   255→    agent_id: Uuid,
   256→    interfaces: &serde_json::Value,
   257→    state_hash: &str,
   258→) -> Result<(), sqlx::Error> {
   259→    sqlx::query(
   260→        r#"
   261→        INSERT INTO agent_state (agent_id, network_interfaces, network_state_hash, network_updated_at)
   262→        VALUES ($1, $2, $3, NOW())
   263→        ON CONFLICT (agent_id) DO UPDATE SET
   264→            network_interfaces = EXCLUDED.network_interfaces,
   265→            network_state_hash = EXCLUDED.network_state_hash,
   266→            network_updated_at = NOW()
   267→        "#,
   268→    )
   269→    .bind(agent_id)
   270→    .bind(interfaces)
   271→    .bind(state_hash)
   272→    .execute(pool)
   273→    .await?;
   274→
   275→    Ok(())
   276→}
   277→
   278→/// Get agent state by agent ID
   279→pub async fn get_agent_state(pool: &PgPool, agent_id: Uuid) -> Result<Option<AgentState>, sqlx::Error> {
   280→    sqlx::query_as::<_, AgentState>("SELECT * FROM agent_state WHERE agent_id = $1")
   281→        .bind(agent_id)
   282→        .fetch_optional(pool)
   283→        .await
   284→}
   285→

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

//! Metrics database operations

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

/// Metrics record from database
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Metrics {
    pub id: i64,
    pub agent_id: Uuid,
    pub timestamp: DateTime<Utc>,
    pub cpu_percent: Option<f32>,
    pub memory_percent: Option<f32>,
    pub memory_used_bytes: Option<i64>,
    pub disk_percent: Option<f32>,
    pub disk_used_bytes: Option<i64>,
    pub network_rx_bytes: Option<i64>,
    pub network_tx_bytes: Option<i64>,
    // Extended metrics
    pub uptime_seconds: Option<i64>,
    pub boot_time: Option<i64>,
    pub logged_in_user: Option<String>,
    pub user_idle_seconds: Option<i64>,
    pub public_ip: Option<String>,
    pub memory_total_bytes: Option<i64>,
    pub disk_total_bytes: Option<i64>,
}

/// Create metrics data from agent
#[derive(Debug, Clone, Deserialize)]
pub struct CreateMetrics {
    pub agent_id: Uuid,
    pub cpu_percent: Option<f32>,
    pub memory_percent: Option<f32>,
    pub memory_used_bytes: Option<i64>,
    pub disk_percent: Option<f32>,
    pub disk_used_bytes: Option<i64>,
    pub network_rx_bytes: Option<i64>,
    pub network_tx_bytes: Option<i64>,
    // Extended metrics
    pub uptime_seconds: Option<i64>,
    pub boot_time: Option<i64>,
    pub logged_in_user: Option<String>,
    pub user_idle_seconds: Option<i64>,
    pub public_ip: Option<String>,
    pub memory_total_bytes: Option<i64>,
    pub disk_total_bytes: Option<i64>,
}

/// Insert metrics into the database
pub async fn insert_metrics(pool: &PgPool, metrics: CreateMetrics) -> Result<i64, sqlx::Error> {
    let result: (i64,) = sqlx::query_as(
        r#"
        INSERT INTO metrics (
            agent_id, cpu_percent, memory_percent, memory_used_bytes,
            disk_percent, disk_used_bytes, network_rx_bytes, network_tx_bytes,
            uptime_seconds, boot_time, logged_in_user, user_idle_seconds,
            public_ip, memory_total_bytes, disk_total_bytes
        )
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
        RETURNING id
        "#,
    )
    .bind(metrics.agent_id)
    .bind(metrics.cpu_percent)
    .bind(metrics.memory_percent)
    .bind(metrics.memory_used_bytes)
    .bind(metrics.disk_percent)
    .bind(metrics.disk_used_bytes)
    .bind(metrics.network_rx_bytes)
    .bind(metrics.network_tx_bytes)
    .bind(metrics.uptime_seconds)
    .bind(metrics.boot_time)
    .bind(&metrics.logged_in_user)
    .bind(metrics.user_idle_seconds)
    .bind(&metrics.public_ip)
    .bind(metrics.memory_total_bytes)
    .bind(metrics.disk_total_bytes)
    .fetch_one(pool)
    .await?;

    Ok(result.0)
}

/// Get recent metrics for an agent
pub async fn get_agent_metrics(
    pool: &PgPool,
    agent_id: Uuid,
    limit: i64,
) -> Result<Vec<Metrics>, sqlx::Error> {
    sqlx::query_as::<_, Metrics>(
        r#"
        SELECT * FROM metrics
        WHERE agent_id = $1
        ORDER BY timestamp DESC
        LIMIT $2
        "#,
    )
    .bind(agent_id)
    .bind(limit)
    .fetch_all(pool)
    .await
}

/// Get metrics for an agent within a time range
pub async fn get_agent_metrics_range(
    pool: &PgPool,
    agent_id: Uuid,
    start: DateTime<Utc>,
    end: DateTime<Utc>,
) -> Result<Vec<Metrics>, sqlx::Error> {
    sqlx::query_as::<_, Metrics>(
        r#"
        SELECT * FROM metrics
        WHERE agent_id = $1 AND timestamp >= $2 AND timestamp <= $3
        ORDER BY timestamp ASC
        "#,
    )
    .bind(agent_id)
    .bind(start)
    .bind(end)
    .fetch_all(pool)
    .await
}

/// Get latest metrics for an agent
pub async fn get_latest_metrics(
    pool: &PgPool,
    agent_id: Uuid,
) -> Result<Option<Metrics>, sqlx::Error> {
    sqlx::query_as::<_, Metrics>(
        r#"
        SELECT * FROM metrics
        WHERE agent_id = $1
        ORDER BY timestamp DESC
        LIMIT 1
        "#,
    )
    .bind(agent_id)
    .fetch_optional(pool)
    .await
}

/// Delete old metrics (for cleanup jobs)
pub async fn delete_old_metrics(
    pool: &PgPool,
    older_than: DateTime<Utc>,
) -> Result<u64, sqlx::Error> {
    let result = sqlx::query("DELETE FROM metrics WHERE timestamp < $1")
        .bind(older_than)
        .execute(pool)
        .await?;

    Ok(result.rows_affected())
}

/// Summary statistics for dashboard
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsSummary {
    pub avg_cpu: f32,
    pub avg_memory: f32,
    pub avg_disk: f32,
    pub total_network_rx: i64,
    pub total_network_tx: i64,
}

/// Get summary metrics across all agents (last hour)
pub async fn get_metrics_summary(pool: &PgPool) -> Result<MetricsSummary, sqlx::Error> {
    let result: (Option<f64>, Option<f64>, Option<f64>, Option<i64>, Option<i64>) = sqlx::query_as(
        r#"
        SELECT
            AVG(cpu_percent)::float8,
            AVG(memory_percent)::float8,
            AVG(disk_percent)::float8,
            SUM(network_rx_bytes),
            SUM(network_tx_bytes)
        FROM metrics
        WHERE timestamp > NOW() - INTERVAL '1 hour'
        "#,
    )
    .fetch_one(pool)
    .await?;

    Ok(MetricsSummary {
        avg_cpu: result.0.unwrap_or(0.0) as f32,
        avg_memory: result.1.unwrap_or(0.0) as f32,
        avg_disk: result.2.unwrap_or(0.0) as f32,
        total_network_rx: result.3.unwrap_or(0),
        total_network_tx: result.4.unwrap_or(0),
    })
}

// ============================================================================
// Agent State (network interfaces, extended info snapshot)
// ============================================================================

/// Agent state record from database
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct AgentState {
    pub agent_id: Uuid,
    pub network_interfaces: Option<serde_json::Value>,
    pub network_state_hash: Option<String>,
    pub uptime_seconds: Option<i64>,
    pub boot_time: Option<i64>,
    pub logged_in_user: Option<String>,
    pub user_idle_seconds: Option<i64>,
    pub public_ip: Option<String>,
    pub network_updated_at: Option<DateTime<Utc>>,
    pub metrics_updated_at: Option<DateTime<Utc>>,
}

/// Update or insert agent state (upsert)
pub async fn upsert_agent_state(
    pool: &PgPool,
    agent_id: Uuid,
    uptime_seconds: Option<i64>,
    boot_time: Option<i64>,
    logged_in_user: Option<&str>,
    user_idle_seconds: Option<i64>,
    public_ip: Option<&str>,
) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        INSERT INTO agent_state (
            agent_id, uptime_seconds, boot_time, logged_in_user,
            user_idle_seconds, public_ip, metrics_updated_at
        )
        VALUES ($1, $2, $3, $4, $5, $6, NOW())
        ON CONFLICT (agent_id) DO UPDATE SET
            uptime_seconds = EXCLUDED.uptime_seconds,
            boot_time = EXCLUDED.boot_time,
            logged_in_user = EXCLUDED.logged_in_user,
            user_idle_seconds = EXCLUDED.user_idle_seconds,
            public_ip = EXCLUDED.public_ip,
            metrics_updated_at = NOW()
        "#,
    )
    .bind(agent_id)
    .bind(uptime_seconds)
    .bind(boot_time)
    .bind(logged_in_user)
    .bind(user_idle_seconds)
    .bind(public_ip)
    .execute(pool)
    .await?;

    Ok(())
}

/// Update network state for an agent
pub async fn update_agent_network_state(
    pool: &PgPool,
    agent_id: Uuid,
    interfaces: &serde_json::Value,
    state_hash: &str,
) -> Result<(), sqlx::Error> {
    sqlx::query(
        r#"
        INSERT INTO agent_state (agent_id, network_interfaces, network_state_hash, network_updated_at)
        VALUES ($1, $2, $3, NOW())
        ON CONFLICT (agent_id) DO UPDATE SET
            network_interfaces = EXCLUDED.network_interfaces,
            network_state_hash = EXCLUDED.network_state_hash,
            network_updated_at = NOW()
        "#,
    )
    .bind(agent_id)
    .bind(interfaces)
    .bind(state_hash)
    .execute(pool)
    .await?;

    Ok(())
}

/// Get agent state by agent ID
pub async fn get_agent_state(pool: &PgPool, agent_id: Uuid) -> Result<Option<AgentState>, sqlx::Error> {
    sqlx::query_as::<_, AgentState>("SELECT * FROM agent_state WHERE agent_id = $1")
        .bind(agent_id)
        .fetch_optional(pool)
        .await
}


Now let me check the migrations directory to understand the schema:


Perfect! Let me read the migration files to understand the complete schema: