     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 understand the current structure. I need to:

1. **Agent side** - Add temperature collection using sysinfo's Components API
2. **Database** - Add temperature columns to the metrics table (need a new migration)
3. **Server** - Update the Metrics struct and insert query
4. **Dashboard** - Display temperature data

Let me check for existing migrations to see the naming pattern and what migration number to use next.


The next migration should be `005_temperature_metrics.sql`. Let me also check the agent's Cargo.toml to see what features of sysinfo are enabled.


     1→[package]
     2→name = "gururmm-agent"
     3→version = "0.5.0"
     4→edition = "2021"
     5→description = "GuruRMM Agent - Cross-platform RMM agent"
     6→authors = ["GuruRMM"]
     7→
     8→[features]
     9→default = ["native-service"]
    10→# Modern Windows (10+, Server 2016+): Native Windows Service integration
    11→native-service = ["dep:windows-service", "dep:windows"]
    12→# Legacy Windows (7, Server 2008 R2): Console mode, use NSSM for service wrapper
    13→legacy = []
    14→
    15→[dependencies]
    16→# Async runtime
    17→tokio = { version = "1", features = ["full"] }
    18→
    19→# System information (cross-platform metrics)
    20→sysinfo = "0.31"
    21→
    22→# WebSocket client (native-tls for Windows 7/2008R2 compatibility)
    23→tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
    24→futures-util = "0.3"
    25→
    26→# HTTP client (fallback/registration) - native-tls for Windows 7/2008R2 compatibility
    27→reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
    28→
    29→# Serialization
    30→serde = { version = "1", features = ["derive"] }
    31→serde_json = "1"
    32→toml = "0.8"
    33→
    34→# CLI arguments
    35→clap = { version = "4", features = ["derive"] }
    36→
    37→# Logging
    38→tracing = "0.1"
    39→tracing-subscriber = { version = "0.3", features = ["env-filter"] }
    40→
    41→# Error handling
    42→anyhow = "1"
    43→thiserror = "1"
    44→
    45→# UUID for identifiers
    46→uuid = { version = "1", features = ["v4", "serde"] }
    47→
    48→# SHA256 checksums for update verification
    49→sha2 = "0.10"
    50→
    51→# Time handling
    52→chrono = { version = "0.4", features = ["serde"] }
    53→
    54→# Hostname detection
    55→hostname = "0.4"
    56→
    57→# Network interface enumeration (LAN IPs)
    58→local-ip-address = "0.6"
    59→
    60→# Async file operations
    61→tokio-util = "0.7"
    62→
    63→[target.'cfg(windows)'.dependencies]
    64→# Windows service support (optional, only for native-service feature)
    65→windows-service = { version = "0.7", optional = true }
    66→# Windows-specific APIs for service management and IPC (optional for native-service)
    67→windows = { version = "0.58", optional = true, features = [
    68→    "Win32_System_Services",
    69→    "Win32_Foundation",
    70→    "Win32_Security",
    71→    "Win32_System_Pipes",
    72→    "Win32_Storage_FileSystem",
    73→    "Win32_System_IO",
    74→    "Win32_System_RemoteDesktop",
    75→    "Win32_System_Threading",
    76→    "Win32_System_StationsAndDesktops",
    77→    "Win32_UI_WindowsAndMessaging",
    78→] }
    79→
    80→[target.'cfg(unix)'.dependencies]
    81→# Unix signal handling and user detection
    82→nix = { version = "0.29", features = ["signal", "user"] }
    83→
    84→[profile.release]
    85→# Optimize for size while maintaining performance
    86→opt-level = "z"
    87→lto = true
    88→codegen-units = 1
    89→strip = true
    90→

<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-agent"
version = "0.5.0"
edition = "2021"
description = "GuruRMM Agent - Cross-platform RMM agent"
authors = ["GuruRMM"]

[features]
default = ["native-service"]
# Modern Windows (10+, Server 2016+): Native Windows Service integration
native-service = ["dep:windows-service", "dep:windows"]
# Legacy Windows (7, Server 2008 R2): Console mode, use NSSM for service wrapper
legacy = []

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

# System information (cross-platform metrics)
sysinfo = "0.31"

# WebSocket client (native-tls for Windows 7/2008R2 compatibility)
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
futures-util = "0.3"

# HTTP client (fallback/registration) - native-tls for Windows 7/2008R2 compatibility
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }

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

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

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

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

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

# SHA256 checksums for update verification
sha2 = "0.10"

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

# Hostname detection
hostname = "0.4"

# Network interface enumeration (LAN IPs)
local-ip-address = "0.6"

# Async file operations
tokio-util = "0.7"

[target.'cfg(windows)'.dependencies]
# Windows service support (optional, only for native-service feature)
windows-service = { version = "0.7", optional = true }
# Windows-specific APIs for service management and IPC (optional for native-service)
windows = { version = "0.58", optional = true, features = [
    "Win32_System_Services",
    "Win32_Foundation",
    "Win32_Security",
    "Win32_System_Pipes",
    "Win32_Storage_FileSystem",
    "Win32_System_IO",
    "Win32_System_RemoteDesktop",
    "Win32_System_Threading",
    "Win32_System_StationsAndDesktops",
    "Win32_UI_WindowsAndMessaging",
] }

[target.'cfg(unix)'.dependencies]
# Unix signal handling and user detection
nix = { version = "0.29", features = ["signal", "user"] }

[profile.release]
# Optimize for size while maintaining performance
opt-level = "z"
lto = true
codegen-units = 1
strip = true


Good. The agent uses sysinfo 0.31. I need to check if sysinfo's Components API is available for temperature reading. Looking at the sysinfo documentation for version 0.31, Components should be available.

Now let me implement the changes:

1. **Agent metrics module** - Add temperature collection with Components
2. **New migration** - Add temperature columns to metrics table
3. **Server db/metrics.rs** - Update structs and queries
4. **Dashboard** - Add temperature display

Let me start with the agent changes.


Now let me implement temperature collection. I'll update the agent, add a database migration, update the server, and update the dashboard.


Edit

use sysinfo::{CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};

use sysinfo::{Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};


The file C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\metrics\mod.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
     9→use serde::{Deserialize, Serialize};
    10→use std::collections::HashMap;
    11→use std::net::IpAddr;
    12→use std::sync::Mutex;
    13→use sysinfo::{Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};
    14→
    15→/// System metrics data structure
    16→#[derive(Debug, Clone, Serialize, Deserialize)]
    17→pub struct SystemMetrics {

//! System metrics collection module
//!
//! Uses the `sysinfo` crate for cross-platform system metrics collection.
//! Collects CPU, memory, disk, and network statistics.
//! Uses `local-ip-address` for network interface enumeration.

use chrono::{DateTime, Utc};
use local_ip_address::list_afinet_netifas;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Mutex;
use sysinfo::{CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};

/// System metrics data structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMetrics {
    /// Timestamp when metrics were collected
    pub timestamp: DateTime<Utc>,

    /// CPU usage percentage (0-100)
    pub cpu_percent: f32,

    /// Memory usage percentage (0-100)
    pub memory_percent: f32,

    /// Memory used in bytes
    pub memory_used_bytes: u64,

    /// Total memory in bytes
    pub memory_total_bytes: u64,

    /// Disk usage percentage (0-100) - primary disk
    pub disk_percent: f32,

    /// Disk used in bytes - primary disk
    pub disk_used_bytes: u64,

    /// Total disk space in bytes - primary disk
    pub disk_total_bytes: u64,

    /// Network bytes received since last collection
    pub network_rx_bytes: u64,

    /// Network bytes transmitted since last collection
    pub network_tx_bytes: u64,

    /// Operating system type
    pub os_type: String,

    /// Operating system version
    pub os_version: String,

    /// System hostname
    pub hostname: String,

    /// System uptime in seconds
    #[serde(default)]
    pub uptime_seconds: u64,

    /// Boot time as Unix timestamp
    #[serde(default)]
    pub boot_time: i64,

    /// Logged in username (if available)
    #[serde(default)]
    pub logged_in_user: Option<String>,

    /// User idle time in seconds (time since last input)
    #[serde(default)]
    pub user_idle_seconds: Option<u64>,

    /// Public/WAN IP address (fetched periodically)
    #[serde(default)]
    pub public_ip: Option<String>,
}

/// Metrics collector using sysinfo
pub struct MetricsCollector {
    /// System info instance (needs to be refreshed for each collection)
    system: Mutex<System>,

    /// Previous network stats for delta calculation
    prev_network_rx: Mutex<u64>,
    prev_network_tx: Mutex<u64>,

    /// Cached public IP (refreshed less frequently)
    cached_public_ip: Mutex<Option<String>>,

    /// Last time public IP was fetched
    last_public_ip_fetch: Mutex<Option<std::time::Instant>>,
}

impl MetricsCollector {
    /// Create a new metrics collector
    pub fn new() -> Self {
        // Create system with minimal initial refresh
        let system = System::new_with_specifics(
            RefreshKind::new()
                .with_cpu(CpuRefreshKind::everything())
                .with_memory(MemoryRefreshKind::everything()),
        );

        Self {
            system: Mutex::new(system),
            prev_network_rx: Mutex::new(0),
            prev_network_tx: Mutex::new(0),
            cached_public_ip: Mutex::new(None),
            last_public_ip_fetch: Mutex::new(None),
        }
    }

    /// Collect current system metrics
    pub async fn collect(&self) -> SystemMetrics {
        // Collect CPU - need to do two refreshes with delay for accurate reading
        // We release the lock between operations to avoid holding MutexGuard across await
        {
            let mut system = self.system.lock().unwrap();
            system.refresh_cpu_all();
        }

        // Small delay for CPU measurement accuracy
        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;

        // Collect all synchronous metrics first, in a block that releases all locks
        let (
            cpu_percent,
            memory_percent,
            memory_used,
            memory_total,
            disk_percent,
            disk_used,
            disk_total,
            delta_rx,
            delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
        ) = {
            // Acquire system lock
            let mut system = self.system.lock().unwrap();
            system.refresh_cpu_all();
            system.refresh_memory();

            // Calculate CPU usage (average across all cores)
            let cpu_percent = system.global_cpu_usage();

            // Memory metrics
            let memory_used = system.used_memory();
            let memory_total = system.total_memory();
            let memory_percent = if memory_total > 0 {
                (memory_used as f32 / memory_total as f32) * 100.0
            } else {
                0.0
            };

            // Disk metrics (use first/primary disk)
            let disks = Disks::new_with_refreshed_list();
            let (disk_used, disk_total, disk_percent) = disks
                .iter()
                .next()
                .map(|d| {
                    let total = d.total_space();
                    let available = d.available_space();
                    let used = total.saturating_sub(available);
                    let percent = if total > 0 {
                        (used as f32 / total as f32) * 100.0
                    } else {
                        0.0
                    };
                    (used, total, percent)
                })
                .unwrap_or((0, 0, 0.0));

            // Network metrics (sum all interfaces)
            let networks = Networks::new_with_refreshed_list();
            let (total_rx, total_tx): (u64, u64) = networks
                .iter()
                .map(|(_, data)| (data.total_received(), data.total_transmitted()))
                .fold((0, 0), |(acc_rx, acc_tx), (rx, tx)| {
                    (acc_rx + rx, acc_tx + tx)
                });

            // Calculate delta from previous collection
            let (delta_rx, delta_tx) = {
                let mut prev_rx = self.prev_network_rx.lock().unwrap();
                let mut prev_tx = self.prev_network_tx.lock().unwrap();

                let delta_rx = total_rx.saturating_sub(*prev_rx);
                let delta_tx = total_tx.saturating_sub(*prev_tx);

                *prev_rx = total_rx;
                *prev_tx = total_tx;

                (delta_rx, delta_tx)
            };

            // Get OS info
            let os_type = std::env::consts::OS.to_string();
            let os_version = System::os_version().unwrap_or_else(|| "unknown".to_string());
            let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());

            // Get uptime and boot time
            let uptime_seconds = System::uptime();
            let boot_time = System::boot_time() as i64;

            // Get logged in user
            let logged_in_user = self.get_logged_in_user();

            // Get user idle time (platform-specific)
            let user_idle_seconds = self.get_user_idle_time();

            // Return all values - locks are dropped at end of this block
            (
                cpu_percent,
                memory_percent,
                memory_used,
                memory_total,
                disk_percent,
                disk_used,
                disk_total,
                delta_rx,
                delta_tx,
                os_type,
                os_version,
                hostname,
                uptime_seconds,
                boot_time,
                logged_in_user,
                user_idle_seconds,
            )
        };

        // All locks are now released - safe to do async work
        // Get public IP (cached, refreshed every 5 minutes)
        let public_ip = self.get_public_ip().await;

        SystemMetrics {
            timestamp: Utc::now(),
            cpu_percent,
            memory_percent,
            memory_used_bytes: memory_used,
            memory_total_bytes: memory_total,
            disk_percent,
            disk_used_bytes: disk_used,
            disk_total_bytes: disk_total,
            network_rx_bytes: delta_rx,
            network_tx_bytes: delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
            public_ip,
        }
    }

    /// Get the currently logged in user
    fn get_logged_in_user(&self) -> Option<String> {
        let users = Users::new_with_refreshed_list();
        // Return the first user found (typically the console user)
        users.iter().next().map(|u| u.name().to_string())
    }

    /// Get user idle time in seconds (time since last keyboard/mouse input)
    #[cfg(target_os = "windows")]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Windows: Use GetLastInputInfo API
        use std::mem;

        #[repr(C)]
        struct LASTINPUTINFO {
            cb_size: u32,
            dw_time: u32,
        }

        extern "system" {
            fn GetLastInputInfo(plii: *mut LASTINPUTINFO) -> i32;
            fn GetTickCount() -> u32;
        }

        unsafe {
            let mut lii = LASTINPUTINFO {
                cb_size: mem::size_of::<LASTINPUTINFO>() as u32,
                dw_time: 0,
            };

            if GetLastInputInfo(&mut lii) != 0 {
                let idle_ms = GetTickCount().wrapping_sub(lii.dw_time);
                Some((idle_ms / 1000) as u64)
            } else {
                None
            }
        }
    }

    /// Get user idle time in seconds (Unix/macOS)
    #[cfg(not(target_os = "windows"))]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Unix: Check /dev/tty* or use platform-specific APIs
        // For now, return None - can be enhanced with X11/Wayland idle detection
        None
    }

    /// Get public IP address (cached for 5 minutes)
    async fn get_public_ip(&self) -> Option<String> {
        use std::time::{Duration, Instant};

        const REFRESH_INTERVAL: Duration = Duration::from_secs(300); // 5 minutes

        // Check if we have a cached value that's still fresh
        {
            let last_fetch = self.last_public_ip_fetch.lock().unwrap();
            let cached_ip = self.cached_public_ip.lock().unwrap();

            if let Some(last) = *last_fetch {
                if last.elapsed() < REFRESH_INTERVAL {
                    return cached_ip.clone();
                }
            }
        }

        // Fetch new public IP
        let new_ip = self.fetch_public_ip().await;

        // Update cache
        {
            let mut last_fetch = self.last_public_ip_fetch.lock().unwrap();
            let mut cached_ip = self.cached_public_ip.lock().unwrap();
            *last_fetch = Some(Instant::now());
            *cached_ip = new_ip.clone();
        }

        new_ip
    }

    /// Fetch public IP from external service
    async fn fetch_public_ip(&self) -> Option<String> {
        // Try multiple services for reliability
        let services = [
            "https://api.ipify.org",
            "https://ifconfig.me/ip",
            "https://icanhazip.com",
        ];

        for service in &services {
            match reqwest::get(*service).await {
                Ok(resp) if resp.status().is_success() => {
                    if let Ok(ip) = resp.text().await {
                        let ip = ip.trim().to_string();
                        // Basic validation: should look like an IP
                        if ip.parse::<std::net::IpAddr>().is_ok() {
                            return Some(ip);
                        }
                    }
                }
                _ => continue,
            }
        }

        None
    }

    /// Get basic system info (for registration)
    pub fn get_system_info(&self) -> SystemInfo {
        let system = self.system.lock().unwrap();

        SystemInfo {
            os_type: std::env::consts::OS.to_string(),
            os_version: System::os_version().unwrap_or_else(|| "unknown".to_string()),
            hostname: System::host_name().unwrap_or_else(|| "unknown".to_string()),
            cpu_count: system.cpus().len() as u32,
            total_memory_bytes: system.total_memory(),
        }
    }
}

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

/// Basic system information (for agent registration)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemInfo {
    /// Operating system type (windows, linux, macos)
    pub os_type: String,

    /// Operating system version
    pub os_version: String,

    /// System hostname
    pub hostname: String,

    /// Number of CPU cores
    pub cpu_count: u32,

    /// Total memory in bytes
    pub total_memory_bytes: u64,
}

/// Network interface information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkInterface {
    /// Interface name (e.g., "eth0", "Wi-Fi", "Ethernet")
    pub name: String,

    /// MAC address (if available from sysinfo)
    pub mac_address: Option<String>,

    /// IPv4 addresses assigned to this interface
    pub ipv4_addresses: Vec<String>,

    /// IPv6 addresses assigned to this interface
    pub ipv6_addresses: Vec<String>,
}

/// Complete network state (sent on connect and on change)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkState {
    /// Timestamp when network state was collected
    pub timestamp: DateTime<Utc>,

    /// All network interfaces with their addresses
    pub interfaces: Vec<NetworkInterface>,

    /// Hash of the network state for quick change detection
    pub state_hash: String,
}

impl NetworkState {
    /// Collect current network state from the system
    pub fn collect() -> Self {
        let mut interface_map: HashMap<String, NetworkInterface> = HashMap::new();

        // Get IP addresses from local-ip-address crate
        if let Ok(netifas) = list_afinet_netifas() {
            for (name, ip) in netifas {
                let entry = interface_map.entry(name.clone()).or_insert_with(|| {
                    NetworkInterface {
                        name: name.clone(),
                        mac_address: None,
                        ipv4_addresses: Vec::new(),
                        ipv6_addresses: Vec::new(),
                    }
                });

                match ip {
                    IpAddr::V4(addr) => {
                        let addr_str = addr.to_string();
                        if !entry.ipv4_addresses.contains(&addr_str) {
                            entry.ipv4_addresses.push(addr_str);
                        }
                    }
                    IpAddr::V6(addr) => {
                        let addr_str = addr.to_string();
                        if !entry.ipv6_addresses.contains(&addr_str) {
                            entry.ipv6_addresses.push(addr_str);
                        }
                    }
                }
            }
        }

        // Get MAC addresses from sysinfo
        let networks = Networks::new_with_refreshed_list();
        for (name, data) in &networks {
            if let Some(entry) = interface_map.get_mut(name) {
                let mac = data.mac_address();
                let mac_str = format!(
                    "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
                    mac.0[0], mac.0[1], mac.0[2], mac.0[3], mac.0[4], mac.0[5]
                );
                // Don't store empty/null MACs
                if mac_str != "00:00:00:00:00:00" {
                    entry.mac_address = Some(mac_str);
                }
            }
        }

        // Convert to sorted vec for consistent ordering
        let mut interfaces: Vec<NetworkInterface> = interface_map.into_values().collect();
        interfaces.sort_by(|a, b| a.name.cmp(&b.name));

        // Filter out loopback and link-local only interfaces
        interfaces.retain(|iface| {
            // Keep if has any non-loopback IPv4
            let has_real_ipv4 = iface.ipv4_addresses.iter().any(|ip| {
                !ip.starts_with("127.") && !ip.starts_with("169.254.")
            });
            // Keep if has any non-link-local IPv6
            let has_real_ipv6 = iface.ipv6_addresses.iter().any(|ip| {
                !ip.starts_with("fe80:") && !ip.starts_with("::1")
            });
            has_real_ipv4 || has_real_ipv6
        });

        // Generate hash for change detection
        let state_hash = Self::compute_hash(&interfaces);

        NetworkState {
            timestamp: Utc::now(),
            interfaces,
            state_hash,
        }
    }

    /// Compute a simple hash of the network state for change detection
    fn compute_hash(interfaces: &[NetworkInterface]) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let mut hasher = DefaultHasher::new();
        for iface in interfaces {
            iface.name.hash(&mut hasher);
            iface.mac_address.hash(&mut hasher);
            for ip in &iface.ipv4_addresses {
                ip.hash(&mut hasher);
            }
            for ip in &iface.ipv6_addresses {
                ip.hash(&mut hasher);
            }
        }
        format!("{:016x}", hasher.finish())
    }

    /// Check if network state has changed compared to another state
    pub fn has_changed(&self, other: &NetworkState) -> bool {
        self.state_hash != other.state_hash
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_metrics_collection() {
        let collector = MetricsCollector::new();
        let metrics = collector.collect().await;

        // Basic sanity checks
        assert!(metrics.cpu_percent >= 0.0 && metrics.cpu_percent <= 100.0);
        assert!(metrics.memory_percent >= 0.0 && metrics.memory_percent <= 100.0);
        assert!(metrics.memory_total_bytes > 0);
        assert!(!metrics.os_type.is_empty());
        assert!(!metrics.hostname.is_empty());
    }

    #[test]
    fn test_system_info() {
        let collector = MetricsCollector::new();
        let info = collector.get_system_info();

        assert!(!info.os_type.is_empty());
        assert!(!info.hostname.is_empty());
        assert!(info.cpu_count > 0);
        assert!(info.total_memory_bytes > 0);
    }

    #[test]
    fn test_network_state_collection() {
        let state = NetworkState::collect();

        // Should have a valid timestamp
        assert!(state.timestamp <= Utc::now());

        // Should have a hash
        assert!(!state.state_hash.is_empty());
        assert_eq!(state.state_hash.len(), 16); // 64-bit hash as hex

        // Print for debugging
        println!("Network state collected:");
        for iface in &state.interfaces {
            println!("  {}: IPv4={:?}, IPv6={:?}, MAC={:?}",
                iface.name, iface.ipv4_addresses, iface.ipv6_addresses, iface.mac_address);
        }
    }

    #[test]
    fn test_network_state_change_detection() {
        let state1 = NetworkState::collect();
        let state2 = NetworkState::collect();

        // Same state should have same hash
        assert!(!state1.has_changed(&state2));

        // Create a modified state
        let mut modified = state1.clone();
        if let Some(iface) = modified.interfaces.first_mut() {
            iface.ipv4_addresses.push("10.99.99.99".to_string());
        }
        modified.state_hash = NetworkState::compute_hash(&modified.interfaces);

        // Modified state should be detected as changed
        assert!(state1.has_changed(&modified));
    }
}

-use sysinfo::{CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};

+use sysinfo::{Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};


Now I need to add the temperature fields to the SystemMetrics struct. Let me add them after the public_ip field.


    /// Public/WAN IP address (fetched periodically)
    #[serde(default)]
    pub public_ip: Option<String>,
}

    /// Public/WAN IP address (fetched periodically)
    #[serde(default)]
    pub public_ip: Option<String>,

    /// CPU temperature in Celsius (average across all CPU sensors)
    #[serde(default)]
    pub cpu_temp_celsius: Option<f32>,

    /// GPU temperature in Celsius (primary GPU if available)
    #[serde(default)]
    pub gpu_temp_celsius: Option<f32>,

    /// All temperature sensors with their labels and values
    #[serde(default)]
    pub temperatures: Vec<TemperatureSensor>,
}

/// Temperature sensor reading
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemperatureSensor {
    /// Sensor label/name (e.g., "CPU Package", "GPU", "nvme0")
    pub label: String,

    /// Temperature in Celsius
    pub temp_celsius: f32,

    /// Critical temperature threshold (if available)
    #[serde(default)]
    pub critical_celsius: Option<f32>,
}


The file C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\metrics\mod.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
    69→    /// User idle time in seconds (time since last input)
    70→    #[serde(default)]
    71→    pub user_idle_seconds: Option<u64>,
    72→
    73→    /// Public/WAN IP address (fetched periodically)
    74→    #[serde(default)]
    75→    pub public_ip: Option<String>,
    76→
    77→    /// CPU temperature in Celsius (average across all CPU sensors)
    78→    #[serde(default)]
    79→    pub cpu_temp_celsius: Option<f32>,
    80→
    81→    /// GPU temperature in Celsius (primary GPU if available)
    82→    #[serde(default)]
    83→    pub gpu_temp_celsius: Option<f32>,
    84→
    85→    /// All temperature sensors with their labels and values
    86→    #[serde(default)]
    87→    pub temperatures: Vec<TemperatureSensor>,
    88→}
    89→
    90→/// Temperature sensor reading
    91→#[derive(Debug, Clone, Serialize, Deserialize)]
    92→pub struct TemperatureSensor {
    93→    /// Sensor label/name (e.g., "CPU Package", "GPU", "nvme0")
    94→    pub label: String,
    95→
    96→    /// Temperature in Celsius
    97→    pub temp_celsius: f32,
    98→
    99→    /// Critical temperature threshold (if available)
   100→    #[serde(default)]
   101→    pub critical_celsius: Option<f32>,
   102→}
   103→
   104→/// Metrics collector using sysinfo
   105→pub struct MetricsCollector {
   106→    /// System info instance (needs to be refreshed for each collection)

    /// Public/WAN IP address (fetched periodically)
    #[serde(default)]
    pub public_ip: Option<String>,

    /// CPU temperature in Celsius (average across all CPU sensors)
    #[serde(default)]
    pub cpu_temp_celsius: Option<f32>,

    /// GPU temperature in Celsius (primary GPU if available)
    #[serde(default)]
    pub gpu_temp_celsius: Option<f32>,

    /// All temperature sensors with their labels and values
    #[serde(default)]
    pub temperatures: Vec<TemperatureSensor>,
}

/// Temperature sensor reading
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemperatureSensor {
    /// Sensor label/name (e.g., "CPU Package", "GPU", "nvme0")
    pub label: String,

    /// Temperature in Celsius
    pub temp_celsius: f32,

    /// Critical temperature threshold (if available)
    #[serde(default)]
    pub critical_celsius: Option<f32>,
}

//! System metrics collection module
//!
//! Uses the `sysinfo` crate for cross-platform system metrics collection.
//! Collects CPU, memory, disk, and network statistics.
//! Uses `local-ip-address` for network interface enumeration.

use chrono::{DateTime, Utc};
use local_ip_address::list_afinet_netifas;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Mutex;
use sysinfo::{Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};

/// System metrics data structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMetrics {
    /// Timestamp when metrics were collected
    pub timestamp: DateTime<Utc>,

    /// CPU usage percentage (0-100)
    pub cpu_percent: f32,

    /// Memory usage percentage (0-100)
    pub memory_percent: f32,

    /// Memory used in bytes
    pub memory_used_bytes: u64,

    /// Total memory in bytes
    pub memory_total_bytes: u64,

    /// Disk usage percentage (0-100) - primary disk
    pub disk_percent: f32,

    /// Disk used in bytes - primary disk
    pub disk_used_bytes: u64,

    /// Total disk space in bytes - primary disk
    pub disk_total_bytes: u64,

    /// Network bytes received since last collection
    pub network_rx_bytes: u64,

    /// Network bytes transmitted since last collection
    pub network_tx_bytes: u64,

    /// Operating system type
    pub os_type: String,

    /// Operating system version
    pub os_version: String,

    /// System hostname
    pub hostname: String,

    /// System uptime in seconds
    #[serde(default)]
    pub uptime_seconds: u64,

    /// Boot time as Unix timestamp
    #[serde(default)]
    pub boot_time: i64,

    /// Logged in username (if available)
    #[serde(default)]
    pub logged_in_user: Option<String>,

    /// User idle time in seconds (time since last input)
    #[serde(default)]
    pub user_idle_seconds: Option<u64>,

    /// Public/WAN IP address (fetched periodically)
    #[serde(default)]
    pub public_ip: Option<String>,
}

/// Metrics collector using sysinfo
pub struct MetricsCollector {
    /// System info instance (needs to be refreshed for each collection)
    system: Mutex<System>,

    /// Previous network stats for delta calculation
    prev_network_rx: Mutex<u64>,
    prev_network_tx: Mutex<u64>,

    /// Cached public IP (refreshed less frequently)
    cached_public_ip: Mutex<Option<String>>,

    /// Last time public IP was fetched
    last_public_ip_fetch: Mutex<Option<std::time::Instant>>,
}

impl MetricsCollector {
    /// Create a new metrics collector
    pub fn new() -> Self {
        // Create system with minimal initial refresh
        let system = System::new_with_specifics(
            RefreshKind::new()
                .with_cpu(CpuRefreshKind::everything())
                .with_memory(MemoryRefreshKind::everything()),
        );

        Self {
            system: Mutex::new(system),
            prev_network_rx: Mutex::new(0),
            prev_network_tx: Mutex::new(0),
            cached_public_ip: Mutex::new(None),
            last_public_ip_fetch: Mutex::new(None),
        }
    }

    /// Collect current system metrics
    pub async fn collect(&self) -> SystemMetrics {
        // Collect CPU - need to do two refreshes with delay for accurate reading
        // We release the lock between operations to avoid holding MutexGuard across await
        {
            let mut system = self.system.lock().unwrap();
            system.refresh_cpu_all();
        }

        // Small delay for CPU measurement accuracy
        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;

        // Collect all synchronous metrics first, in a block that releases all locks
        let (
            cpu_percent,
            memory_percent,
            memory_used,
            memory_total,
            disk_percent,
            disk_used,
            disk_total,
            delta_rx,
            delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
        ) = {
            // Acquire system lock
            let mut system = self.system.lock().unwrap();
            system.refresh_cpu_all();
            system.refresh_memory();

            // Calculate CPU usage (average across all cores)
            let cpu_percent = system.global_cpu_usage();

            // Memory metrics
            let memory_used = system.used_memory();
            let memory_total = system.total_memory();
            let memory_percent = if memory_total > 0 {
                (memory_used as f32 / memory_total as f32) * 100.0
            } else {
                0.0
            };

            // Disk metrics (use first/primary disk)
            let disks = Disks::new_with_refreshed_list();
            let (disk_used, disk_total, disk_percent) = disks
                .iter()
                .next()
                .map(|d| {
                    let total = d.total_space();
                    let available = d.available_space();
                    let used = total.saturating_sub(available);
                    let percent = if total > 0 {
                        (used as f32 / total as f32) * 100.0
                    } else {
                        0.0
                    };
                    (used, total, percent)
                })
                .unwrap_or((0, 0, 0.0));

            // Network metrics (sum all interfaces)
            let networks = Networks::new_with_refreshed_list();
            let (total_rx, total_tx): (u64, u64) = networks
                .iter()
                .map(|(_, data)| (data.total_received(), data.total_transmitted()))
                .fold((0, 0), |(acc_rx, acc_tx), (rx, tx)| {
                    (acc_rx + rx, acc_tx + tx)
                });

            // Calculate delta from previous collection
            let (delta_rx, delta_tx) = {
                let mut prev_rx = self.prev_network_rx.lock().unwrap();
                let mut prev_tx = self.prev_network_tx.lock().unwrap();

                let delta_rx = total_rx.saturating_sub(*prev_rx);
                let delta_tx = total_tx.saturating_sub(*prev_tx);

                *prev_rx = total_rx;
                *prev_tx = total_tx;

                (delta_rx, delta_tx)
            };

            // Get OS info
            let os_type = std::env::consts::OS.to_string();
            let os_version = System::os_version().unwrap_or_else(|| "unknown".to_string());
            let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());

            // Get uptime and boot time
            let uptime_seconds = System::uptime();
            let boot_time = System::boot_time() as i64;

            // Get logged in user
            let logged_in_user = self.get_logged_in_user();

            // Get user idle time (platform-specific)
            let user_idle_seconds = self.get_user_idle_time();

            // Return all values - locks are dropped at end of this block
            (
                cpu_percent,
                memory_percent,
                memory_used,
                memory_total,
                disk_percent,
                disk_used,
                disk_total,
                delta_rx,
                delta_tx,
                os_type,
                os_version,
                hostname,
                uptime_seconds,
                boot_time,
                logged_in_user,
                user_idle_seconds,
            )
        };

        // All locks are now released - safe to do async work
        // Get public IP (cached, refreshed every 5 minutes)
        let public_ip = self.get_public_ip().await;

        SystemMetrics {
            timestamp: Utc::now(),
            cpu_percent,
            memory_percent,
            memory_used_bytes: memory_used,
            memory_total_bytes: memory_total,
            disk_percent,
            disk_used_bytes: disk_used,
            disk_total_bytes: disk_total,
            network_rx_bytes: delta_rx,
            network_tx_bytes: delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
            public_ip,
        }
    }

    /// Get the currently logged in user
    fn get_logged_in_user(&self) -> Option<String> {
        let users = Users::new_with_refreshed_list();
        // Return the first user found (typically the console user)
        users.iter().next().map(|u| u.name().to_string())
    }

    /// Get user idle time in seconds (time since last keyboard/mouse input)
    #[cfg(target_os = "windows")]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Windows: Use GetLastInputInfo API
        use std::mem;

        #[repr(C)]
        struct LASTINPUTINFO {
            cb_size: u32,
            dw_time: u32,
        }

        extern "system" {
            fn GetLastInputInfo(plii: *mut LASTINPUTINFO) -> i32;
            fn GetTickCount() -> u32;
        }

        unsafe {
            let mut lii = LASTINPUTINFO {
                cb_size: mem::size_of::<LASTINPUTINFO>() as u32,
                dw_time: 0,
            };

            if GetLastInputInfo(&mut lii) != 0 {
                let idle_ms = GetTickCount().wrapping_sub(lii.dw_time);
                Some((idle_ms / 1000) as u64)
            } else {
                None
            }
        }
    }

    /// Get user idle time in seconds (Unix/macOS)
    #[cfg(not(target_os = "windows"))]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Unix: Check /dev/tty* or use platform-specific APIs
        // For now, return None - can be enhanced with X11/Wayland idle detection
        None
    }

    /// Get public IP address (cached for 5 minutes)
    async fn get_public_ip(&self) -> Option<String> {
        use std::time::{Duration, Instant};

        const REFRESH_INTERVAL: Duration = Duration::from_secs(300); // 5 minutes

        // Check if we have a cached value that's still fresh
        {
            let last_fetch = self.last_public_ip_fetch.lock().unwrap();
            let cached_ip = self.cached_public_ip.lock().unwrap();

            if let Some(last) = *last_fetch {
                if last.elapsed() < REFRESH_INTERVAL {
                    return cached_ip.clone();
                }
            }
        }

        // Fetch new public IP
        let new_ip = self.fetch_public_ip().await;

        // Update cache
        {
            let mut last_fetch = self.last_public_ip_fetch.lock().unwrap();
            let mut cached_ip = self.cached_public_ip.lock().unwrap();
            *last_fetch = Some(Instant::now());
            *cached_ip = new_ip.clone();
        }

        new_ip
    }

    /// Fetch public IP from external service
    async fn fetch_public_ip(&self) -> Option<String> {
        // Try multiple services for reliability
        let services = [
            "https://api.ipify.org",
            "https://ifconfig.me/ip",
            "https://icanhazip.com",
        ];

        for service in &services {
            match reqwest::get(*service).await {
                Ok(resp) if resp.status().is_success() => {
                    if let Ok(ip) = resp.text().await {
                        let ip = ip.trim().to_string();
                        // Basic validation: should look like an IP
                        if ip.parse::<std::net::IpAddr>().is_ok() {
                            return Some(ip);
                        }
                    }
                }
                _ => continue,
            }
        }

        None
    }

    /// Get basic system info (for registration)
    pub fn get_system_info(&self) -> SystemInfo {
        let system = self.system.lock().unwrap();

        SystemInfo {
            os_type: std::env::consts::OS.to_string(),
            os_version: System::os_version().unwrap_or_else(|| "unknown".to_string()),
            hostname: System::host_name().unwrap_or_else(|| "unknown".to_string()),
            cpu_count: system.cpus().len() as u32,
            total_memory_bytes: system.total_memory(),
        }
    }
}

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

/// Basic system information (for agent registration)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemInfo {
    /// Operating system type (windows, linux, macos)
    pub os_type: String,

    /// Operating system version
    pub os_version: String,

    /// System hostname
    pub hostname: String,

    /// Number of CPU cores
    pub cpu_count: u32,

    /// Total memory in bytes
    pub total_memory_bytes: u64,
}

/// Network interface information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkInterface {
    /// Interface name (e.g., "eth0", "Wi-Fi", "Ethernet")
    pub name: String,

    /// MAC address (if available from sysinfo)
    pub mac_address: Option<String>,

    /// IPv4 addresses assigned to this interface
    pub ipv4_addresses: Vec<String>,

    /// IPv6 addresses assigned to this interface
    pub ipv6_addresses: Vec<String>,
}

/// Complete network state (sent on connect and on change)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkState {
    /// Timestamp when network state was collected
    pub timestamp: DateTime<Utc>,

    /// All network interfaces with their addresses
    pub interfaces: Vec<NetworkInterface>,

    /// Hash of the network state for quick change detection
    pub state_hash: String,
}

impl NetworkState {
    /// Collect current network state from the system
    pub fn collect() -> Self {
        let mut interface_map: HashMap<String, NetworkInterface> = HashMap::new();

        // Get IP addresses from local-ip-address crate
        if let Ok(netifas) = list_afinet_netifas() {
            for (name, ip) in netifas {
                let entry = interface_map.entry(name.clone()).or_insert_with(|| {
                    NetworkInterface {
                        name: name.clone(),
                        mac_address: None,
                        ipv4_addresses: Vec::new(),
                        ipv6_addresses: Vec::new(),
                    }
                });

                match ip {
                    IpAddr::V4(addr) => {
                        let addr_str = addr.to_string();
                        if !entry.ipv4_addresses.contains(&addr_str) {
                            entry.ipv4_addresses.push(addr_str);
                        }
                    }
                    IpAddr::V6(addr) => {
                        let addr_str = addr.to_string();
                        if !entry.ipv6_addresses.contains(&addr_str) {
                            entry.ipv6_addresses.push(addr_str);
                        }
                    }
                }
            }
        }

        // Get MAC addresses from sysinfo
        let networks = Networks::new_with_refreshed_list();
        for (name, data) in &networks {
            if let Some(entry) = interface_map.get_mut(name) {
                let mac = data.mac_address();
                let mac_str = format!(
                    "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
                    mac.0[0], mac.0[1], mac.0[2], mac.0[3], mac.0[4], mac.0[5]
                );
                // Don't store empty/null MACs
                if mac_str != "00:00:00:00:00:00" {
                    entry.mac_address = Some(mac_str);
                }
            }
        }

        // Convert to sorted vec for consistent ordering
        let mut interfaces: Vec<NetworkInterface> = interface_map.into_values().collect();
        interfaces.sort_by(|a, b| a.name.cmp(&b.name));

        // Filter out loopback and link-local only interfaces
        interfaces.retain(|iface| {
            // Keep if has any non-loopback IPv4
            let has_real_ipv4 = iface.ipv4_addresses.iter().any(|ip| {
                !ip.starts_with("127.") && !ip.starts_with("169.254.")
            });
            // Keep if has any non-link-local IPv6
            let has_real_ipv6 = iface.ipv6_addresses.iter().any(|ip| {
                !ip.starts_with("fe80:") && !ip.starts_with("::1")
            });
            has_real_ipv4 || has_real_ipv6
        });

        // Generate hash for change detection
        let state_hash = Self::compute_hash(&interfaces);

        NetworkState {
            timestamp: Utc::now(),
            interfaces,
            state_hash,
        }
    }

    /// Compute a simple hash of the network state for change detection
    fn compute_hash(interfaces: &[NetworkInterface]) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let mut hasher = DefaultHasher::new();
        for iface in interfaces {
            iface.name.hash(&mut hasher);
            iface.mac_address.hash(&mut hasher);
            for ip in &iface.ipv4_addresses {
                ip.hash(&mut hasher);
            }
            for ip in &iface.ipv6_addresses {
                ip.hash(&mut hasher);
            }
        }
        format!("{:016x}", hasher.finish())
    }

    /// Check if network state has changed compared to another state
    pub fn has_changed(&self, other: &NetworkState) -> bool {
        self.state_hash != other.state_hash
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_metrics_collection() {
        let collector = MetricsCollector::new();
        let metrics = collector.collect().await;

        // Basic sanity checks
        assert!(metrics.cpu_percent >= 0.0 && metrics.cpu_percent <= 100.0);
        assert!(metrics.memory_percent >= 0.0 && metrics.memory_percent <= 100.0);
        assert!(metrics.memory_total_bytes > 0);
        assert!(!metrics.os_type.is_empty());
        assert!(!metrics.hostname.is_empty());
    }

    #[test]
    fn test_system_info() {
        let collector = MetricsCollector::new();
        let info = collector.get_system_info();

        assert!(!info.os_type.is_empty());
        assert!(!info.hostname.is_empty());
        assert!(info.cpu_count > 0);
        assert!(info.total_memory_bytes > 0);
    }

    #[test]
    fn test_network_state_collection() {
        let state = NetworkState::collect();

        // Should have a valid timestamp
        assert!(state.timestamp <= Utc::now());

        // Should have a hash
        assert!(!state.state_hash.is_empty());
        assert_eq!(state.state_hash.len(), 16); // 64-bit hash as hex

        // Print for debugging
        println!("Network state collected:");
        for iface in &state.interfaces {
            println!("  {}: IPv4={:?}, IPv6={:?}, MAC={:?}",
                iface.name, iface.ipv4_addresses, iface.ipv6_addresses, iface.mac_address);
        }
    }

    #[test]
    fn test_network_state_change_detection() {
        let state1 = NetworkState::collect();
        let state2 = NetworkState::collect();

        // Same state should have same hash
        assert!(!state1.has_changed(&state2));

        // Create a modified state
        let mut modified = state1.clone();
        if let Some(iface) = modified.interfaces.first_mut() {
            iface.ipv4_addresses.push("10.99.99.99".to_string());
        }
        modified.state_hash = NetworkState::compute_hash(&modified.interfaces);

        // Modified state should be detected as changed
        assert!(state1.has_changed(&modified));
    }
}

+    /// CPU temperature in Celsius (average across all CPU sensors)

+    /// GPU temperature in Celsius (primary GPU if available)

+    /// All temperature sensors with their labels and values

+    pub temperatures: Vec<TemperatureSensor>,

+#[derive(Debug, Clone, Serialize, Deserialize)]

+    /// Sensor label/name (e.g., "CPU Package", "GPU", "nvme0")

+    /// Critical temperature threshold (if available)


Now I need to add a method to collect temperatures and integrate it into the `collect()` method. Let me add the temperature collection logic.


Now let me add the temperature collection method and integrate it into the collect function:


    /// Get user idle time in seconds (Unix/macOS)
    #[cfg(not(target_os = "windows"))]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Unix: Check /dev/tty* or use platform-specific APIs
        // For now, return None - can be enhanced with X11/Wayland idle detection
        None
    }

    /// Get public IP address (cached for 5 minutes)

    /// Get user idle time in seconds (Unix/macOS)
    #[cfg(not(target_os = "windows"))]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Unix: Check /dev/tty* or use platform-specific APIs
        // For now, return None - can be enhanced with X11/Wayland idle detection
        None
    }

    /// Collect temperature readings from all available sensors
    fn collect_temperatures(&self) -> (Option<f32>, Option<f32>, Vec<TemperatureSensor>) {
        let components = Components::new_with_refreshed_list();

        let mut temperatures = Vec::new();
        let mut cpu_temps = Vec::new();
        let mut gpu_temp: Option<f32> = None;

        for component in &components {
            let label = component.label().to_string();
            let temp = component.temperature();
            let critical = component.critical();

            // Skip invalid readings (some sensors report 0 or negative)
            if temp <= 0.0 || temp > 150.0 {
                continue;
            }

            temperatures.push(TemperatureSensor {
                label: label.clone(),
                temp_celsius: temp,
                critical_celsius: critical,
            });

            // Categorize temperatures for summary fields
            let label_lower = label.to_lowercase();
            if label_lower.contains("cpu")
                || label_lower.contains("core")
                || label_lower.contains("package")
                || label_lower.contains("tctl")
                || label_lower.contains("tdie")
            {
                cpu_temps.push(temp);
            } else if label_lower.contains("gpu")
                || label_lower.contains("nvidia")
                || label_lower.contains("radeon")
                || label_lower.contains("amdgpu")
            {
                // Take the first GPU temperature found
                if gpu_temp.is_none() {
                    gpu_temp = Some(temp);
                }
            }
        }

        // Calculate average CPU temperature
        let cpu_temp = if cpu_temps.is_empty() {
            None
        } else {
            Some(cpu_temps.iter().sum::<f32>() / cpu_temps.len() as f32)
        };

        (cpu_temp, gpu_temp, temperatures)
    }

    /// Get public IP address (cached for 5 minutes)


The file C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\metrics\mod.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   325→            }
   326→        }
   327→    }
   328→
   329→    /// Get user idle time in seconds (Unix/macOS)
   330→    #[cfg(not(target_os = "windows"))]
   331→    fn get_user_idle_time(&self) -> Option<u64> {
   332→        // Unix: Check /dev/tty* or use platform-specific APIs
   333→        // For now, return None - can be enhanced with X11/Wayland idle detection
   334→        None
   335→    }
   336→
   337→    /// Collect temperature readings from all available sensors
   338→    fn collect_temperatures(&self) -> (Option<f32>, Option<f32>, Vec<TemperatureSensor>) {
   339→        let components = Components::new_with_refreshed_list();
   340→
   341→        let mut temperatures = Vec::new();
   342→        let mut cpu_temps = Vec::new();
   343→        let mut gpu_temp: Option<f32> = None;
   344→
   345→        for component in &components {
   346→            let label = component.label().to_string();
   347→            let temp = component.temperature();
   348→            let critical = component.critical();
   349→
   350→            // Skip invalid readings (some sensors report 0 or negative)
   351→            if temp <= 0.0 || temp > 150.0 {
   352→                continue;
   353→            }
   354→
   355→            temperatures.push(TemperatureSensor {
   356→                label: label.clone(),
   357→                temp_celsius: temp,
   358→                critical_celsius: critical,
   359→            });
   360→
   361→            // Categorize temperatures for summary fields
   362→            let label_lower = label.to_lowercase();
   363→            if label_lower.contains("cpu")
   364→                || label_lower.contains("core")
   365→                || label_lower.contains("package")
   366→                || label_lower.contains("tctl")
   367→                || label_lower.contains("tdie")
   368→            {
   369→                cpu_temps.push(temp);
   370→            } else if label_lower.contains("gpu")
   371→                || label_lower.contains("nvidia")
   372→                || label_lower.contains("radeon")
   373→                || label_lower.contains("amdgpu")
   374→            {
   375→                // Take the first GPU temperature found
   376→                if gpu_temp.is_none() {
   377→                    gpu_temp = Some(temp);
   378→                }
   379→            }
   380→        }
   381→
   382→        // Calculate average CPU temperature
   383→        let cpu_temp = if cpu_temps.is_empty() {
   384→            None
   385→        } else {
   386→            Some(cpu_temps.iter().sum::<f32>() / cpu_temps.len() as f32)
   387→        };
   388→
   389→        (cpu_temp, gpu_temp, temperatures)
   390→    }
   391→
   392→    /// Get public IP address (cached for 5 minutes)
   393→    async fn get_public_ip(&self) -> Option<String> {
   394→        use std::time::{Duration, Instant};
   395→
   396→        const REFRESH_INTERVAL: Duration = Duration::from_secs(300); // 5 minutes

    /// Get user idle time in seconds (Unix/macOS)
    #[cfg(not(target_os = "windows"))]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Unix: Check /dev/tty* or use platform-specific APIs
        // For now, return None - can be enhanced with X11/Wayland idle detection
        None
    }

    /// Collect temperature readings from all available sensors
    fn collect_temperatures(&self) -> (Option<f32>, Option<f32>, Vec<TemperatureSensor>) {
        let components = Components::new_with_refreshed_list();

        let mut temperatures = Vec::new();
        let mut cpu_temps = Vec::new();
        let mut gpu_temp: Option<f32> = None;

        for component in &components {
            let label = component.label().to_string();
            let temp = component.temperature();
            let critical = component.critical();

            // Skip invalid readings (some sensors report 0 or negative)
            if temp <= 0.0 || temp > 150.0 {
                continue;
            }

            temperatures.push(TemperatureSensor {
                label: label.clone(),
                temp_celsius: temp,
                critical_celsius: critical,
            });

            // Categorize temperatures for summary fields
            let label_lower = label.to_lowercase();
            if label_lower.contains("cpu")
                || label_lower.contains("core")
                || label_lower.contains("package")
                || label_lower.contains("tctl")
                || label_lower.contains("tdie")
            {
                cpu_temps.push(temp);
            } else if label_lower.contains("gpu")
                || label_lower.contains("nvidia")
                || label_lower.contains("radeon")
                || label_lower.contains("amdgpu")
            {
                // Take the first GPU temperature found
                if gpu_temp.is_none() {
                    gpu_temp = Some(temp);
                }
            }
        }

        // Calculate average CPU temperature
        let cpu_temp = if cpu_temps.is_empty() {
            None
        } else {
            Some(cpu_temps.iter().sum::<f32>() / cpu_temps.len() as f32)
        };

        (cpu_temp, gpu_temp, temperatures)
    }

    /// Get public IP address (cached for 5 minutes)

//! System metrics collection module
//!
//! Uses the `sysinfo` crate for cross-platform system metrics collection.
//! Collects CPU, memory, disk, and network statistics.
//! Uses `local-ip-address` for network interface enumeration.

use chrono::{DateTime, Utc};
use local_ip_address::list_afinet_netifas;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Mutex;
use sysinfo::{Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};

/// System metrics data structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMetrics {
    /// Timestamp when metrics were collected
    pub timestamp: DateTime<Utc>,

    /// CPU usage percentage (0-100)
    pub cpu_percent: f32,

    /// Memory usage percentage (0-100)
    pub memory_percent: f32,

    /// Memory used in bytes
    pub memory_used_bytes: u64,

    /// Total memory in bytes
    pub memory_total_bytes: u64,

    /// Disk usage percentage (0-100) - primary disk
    pub disk_percent: f32,

    /// Disk used in bytes - primary disk
    pub disk_used_bytes: u64,

    /// Total disk space in bytes - primary disk
    pub disk_total_bytes: u64,

    /// Network bytes received since last collection
    pub network_rx_bytes: u64,

    /// Network bytes transmitted since last collection
    pub network_tx_bytes: u64,

    /// Operating system type
    pub os_type: String,

    /// Operating system version
    pub os_version: String,

    /// System hostname
    pub hostname: String,

    /// System uptime in seconds
    #[serde(default)]
    pub uptime_seconds: u64,

    /// Boot time as Unix timestamp
    #[serde(default)]
    pub boot_time: i64,

    /// Logged in username (if available)
    #[serde(default)]
    pub logged_in_user: Option<String>,

    /// User idle time in seconds (time since last input)
    #[serde(default)]
    pub user_idle_seconds: Option<u64>,

    /// Public/WAN IP address (fetched periodically)
    #[serde(default)]
    pub public_ip: Option<String>,

    /// CPU temperature in Celsius (average across all CPU sensors)
    #[serde(default)]
    pub cpu_temp_celsius: Option<f32>,

    /// GPU temperature in Celsius (primary GPU if available)
    #[serde(default)]
    pub gpu_temp_celsius: Option<f32>,

    /// All temperature sensors with their labels and values
    #[serde(default)]
    pub temperatures: Vec<TemperatureSensor>,
}

/// Temperature sensor reading
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemperatureSensor {
    /// Sensor label/name (e.g., "CPU Package", "GPU", "nvme0")
    pub label: String,

    /// Temperature in Celsius
    pub temp_celsius: f32,

    /// Critical temperature threshold (if available)
    #[serde(default)]
    pub critical_celsius: Option<f32>,
}

/// Metrics collector using sysinfo
pub struct MetricsCollector {
    /// System info instance (needs to be refreshed for each collection)
    system: Mutex<System>,

    /// Previous network stats for delta calculation
    prev_network_rx: Mutex<u64>,
    prev_network_tx: Mutex<u64>,

    /// Cached public IP (refreshed less frequently)
    cached_public_ip: Mutex<Option<String>>,

    /// Last time public IP was fetched
    last_public_ip_fetch: Mutex<Option<std::time::Instant>>,
}

impl MetricsCollector {
    /// Create a new metrics collector
    pub fn new() -> Self {
        // Create system with minimal initial refresh
        let system = System::new_with_specifics(
            RefreshKind::new()
                .with_cpu(CpuRefreshKind::everything())
                .with_memory(MemoryRefreshKind::everything()),
        );

        Self {
            system: Mutex::new(system),
            prev_network_rx: Mutex::new(0),
            prev_network_tx: Mutex::new(0),
            cached_public_ip: Mutex::new(None),
            last_public_ip_fetch: Mutex::new(None),
        }
    }

    /// Collect current system metrics
    pub async fn collect(&self) -> SystemMetrics {
        // Collect CPU - need to do two refreshes with delay for accurate reading
        // We release the lock between operations to avoid holding MutexGuard across await
        {
            let mut system = self.system.lock().unwrap();
            system.refresh_cpu_all();
        }

        // Small delay for CPU measurement accuracy
        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;

        // Collect all synchronous metrics first, in a block that releases all locks
        let (
            cpu_percent,
            memory_percent,
            memory_used,
            memory_total,
            disk_percent,
            disk_used,
            disk_total,
            delta_rx,
            delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
        ) = {
            // Acquire system lock
            let mut system = self.system.lock().unwrap();
            system.refresh_cpu_all();
            system.refresh_memory();

            // Calculate CPU usage (average across all cores)
            let cpu_percent = system.global_cpu_usage();

            // Memory metrics
            let memory_used = system.used_memory();
            let memory_total = system.total_memory();
            let memory_percent = if memory_total > 0 {
                (memory_used as f32 / memory_total as f32) * 100.0
            } else {
                0.0
            };

            // Disk metrics (use first/primary disk)
            let disks = Disks::new_with_refreshed_list();
            let (disk_used, disk_total, disk_percent) = disks
                .iter()
                .next()
                .map(|d| {
                    let total = d.total_space();
                    let available = d.available_space();
                    let used = total.saturating_sub(available);
                    let percent = if total > 0 {
                        (used as f32 / total as f32) * 100.0
                    } else {
                        0.0
                    };
                    (used, total, percent)
                })
                .unwrap_or((0, 0, 0.0));

            // Network metrics (sum all interfaces)
            let networks = Networks::new_with_refreshed_list();
            let (total_rx, total_tx): (u64, u64) = networks
                .iter()
                .map(|(_, data)| (data.total_received(), data.total_transmitted()))
                .fold((0, 0), |(acc_rx, acc_tx), (rx, tx)| {
                    (acc_rx + rx, acc_tx + tx)
                });

            // Calculate delta from previous collection
            let (delta_rx, delta_tx) = {
                let mut prev_rx = self.prev_network_rx.lock().unwrap();
                let mut prev_tx = self.prev_network_tx.lock().unwrap();

                let delta_rx = total_rx.saturating_sub(*prev_rx);
                let delta_tx = total_tx.saturating_sub(*prev_tx);

                *prev_rx = total_rx;
                *prev_tx = total_tx;

                (delta_rx, delta_tx)
            };

            // Get OS info
            let os_type = std::env::consts::OS.to_string();
            let os_version = System::os_version().unwrap_or_else(|| "unknown".to_string());
            let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());

            // Get uptime and boot time
            let uptime_seconds = System::uptime();
            let boot_time = System::boot_time() as i64;

            // Get logged in user
            let logged_in_user = self.get_logged_in_user();

            // Get user idle time (platform-specific)
            let user_idle_seconds = self.get_user_idle_time();

            // Return all values - locks are dropped at end of this block
            (
                cpu_percent,
                memory_percent,
                memory_used,
                memory_total,
                disk_percent,
                disk_used,
                disk_total,
                delta_rx,
                delta_tx,
                os_type,
                os_version,
                hostname,
                uptime_seconds,
                boot_time,
                logged_in_user,
                user_idle_seconds,
            )
        };

        // All locks are now released - safe to do async work
        // Get public IP (cached, refreshed every 5 minutes)
        let public_ip = self.get_public_ip().await;

        SystemMetrics {
            timestamp: Utc::now(),
            cpu_percent,
            memory_percent,
            memory_used_bytes: memory_used,
            memory_total_bytes: memory_total,
            disk_percent,
            disk_used_bytes: disk_used,
            disk_total_bytes: disk_total,
            network_rx_bytes: delta_rx,
            network_tx_bytes: delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
            public_ip,
        }
    }

    /// Get the currently logged in user
    fn get_logged_in_user(&self) -> Option<String> {
        let users = Users::new_with_refreshed_list();
        // Return the first user found (typically the console user)
        users.iter().next().map(|u| u.name().to_string())
    }

    /// Get user idle time in seconds (time since last keyboard/mouse input)
    #[cfg(target_os = "windows")]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Windows: Use GetLastInputInfo API
        use std::mem;

        #[repr(C)]
        struct LASTINPUTINFO {
            cb_size: u32,
            dw_time: u32,
        }

        extern "system" {
            fn GetLastInputInfo(plii: *mut LASTINPUTINFO) -> i32;
            fn GetTickCount() -> u32;
        }

        unsafe {
            let mut lii = LASTINPUTINFO {
                cb_size: mem::size_of::<LASTINPUTINFO>() as u32,
                dw_time: 0,
            };

            if GetLastInputInfo(&mut lii) != 0 {
                let idle_ms = GetTickCount().wrapping_sub(lii.dw_time);
                Some((idle_ms / 1000) as u64)
            } else {
                None
            }
        }
    }

    /// Get user idle time in seconds (Unix/macOS)
    #[cfg(not(target_os = "windows"))]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Unix: Check /dev/tty* or use platform-specific APIs
        // For now, return None - can be enhanced with X11/Wayland idle detection
        None
    }

    /// Get public IP address (cached for 5 minutes)
    async fn get_public_ip(&self) -> Option<String> {
        use std::time::{Duration, Instant};

        const REFRESH_INTERVAL: Duration = Duration::from_secs(300); // 5 minutes

        // Check if we have a cached value that's still fresh
        {
            let last_fetch = self.last_public_ip_fetch.lock().unwrap();
            let cached_ip = self.cached_public_ip.lock().unwrap();

            if let Some(last) = *last_fetch {
                if last.elapsed() < REFRESH_INTERVAL {
                    return cached_ip.clone();
                }
            }
        }

        // Fetch new public IP
        let new_ip = self.fetch_public_ip().await;

        // Update cache
        {
            let mut last_fetch = self.last_public_ip_fetch.lock().unwrap();
            let mut cached_ip = self.cached_public_ip.lock().unwrap();
            *last_fetch = Some(Instant::now());
            *cached_ip = new_ip.clone();
        }

        new_ip
    }

    /// Fetch public IP from external service
    async fn fetch_public_ip(&self) -> Option<String> {
        // Try multiple services for reliability
        let services = [
            "https://api.ipify.org",
            "https://ifconfig.me/ip",
            "https://icanhazip.com",
        ];

        for service in &services {
            match reqwest::get(*service).await {
                Ok(resp) if resp.status().is_success() => {
                    if let Ok(ip) = resp.text().await {
                        let ip = ip.trim().to_string();
                        // Basic validation: should look like an IP
                        if ip.parse::<std::net::IpAddr>().is_ok() {
                            return Some(ip);
                        }
                    }
                }
                _ => continue,
            }
        }

        None
    }

    /// Get basic system info (for registration)
    pub fn get_system_info(&self) -> SystemInfo {
        let system = self.system.lock().unwrap();

        SystemInfo {
            os_type: std::env::consts::OS.to_string(),
            os_version: System::os_version().unwrap_or_else(|| "unknown".to_string()),
            hostname: System::host_name().unwrap_or_else(|| "unknown".to_string()),
            cpu_count: system.cpus().len() as u32,
            total_memory_bytes: system.total_memory(),
        }
    }
}

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

/// Basic system information (for agent registration)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemInfo {
    /// Operating system type (windows, linux, macos)
    pub os_type: String,

    /// Operating system version
    pub os_version: String,

    /// System hostname
    pub hostname: String,

    /// Number of CPU cores
    pub cpu_count: u32,

    /// Total memory in bytes
    pub total_memory_bytes: u64,
}

/// Network interface information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkInterface {
    /// Interface name (e.g., "eth0", "Wi-Fi", "Ethernet")
    pub name: String,

    /// MAC address (if available from sysinfo)
    pub mac_address: Option<String>,

    /// IPv4 addresses assigned to this interface
    pub ipv4_addresses: Vec<String>,

    /// IPv6 addresses assigned to this interface
    pub ipv6_addresses: Vec<String>,
}

/// Complete network state (sent on connect and on change)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkState {
    /// Timestamp when network state was collected
    pub timestamp: DateTime<Utc>,

    /// All network interfaces with their addresses
    pub interfaces: Vec<NetworkInterface>,

    /// Hash of the network state for quick change detection
    pub state_hash: String,
}

impl NetworkState {
    /// Collect current network state from the system
    pub fn collect() -> Self {
        let mut interface_map: HashMap<String, NetworkInterface> = HashMap::new();

        // Get IP addresses from local-ip-address crate
        if let Ok(netifas) = list_afinet_netifas() {
            for (name, ip) in netifas {
                let entry = interface_map.entry(name.clone()).or_insert_with(|| {
                    NetworkInterface {
                        name: name.clone(),
                        mac_address: None,
                        ipv4_addresses: Vec::new(),
                        ipv6_addresses: Vec::new(),
                    }
                });

                match ip {
                    IpAddr::V4(addr) => {
                        let addr_str = addr.to_string();
                        if !entry.ipv4_addresses.contains(&addr_str) {
                            entry.ipv4_addresses.push(addr_str);
                        }
                    }
                    IpAddr::V6(addr) => {
                        let addr_str = addr.to_string();
                        if !entry.ipv6_addresses.contains(&addr_str) {
                            entry.ipv6_addresses.push(addr_str);
                        }
                    }
                }
            }
        }

        // Get MAC addresses from sysinfo
        let networks = Networks::new_with_refreshed_list();
        for (name, data) in &networks {
            if let Some(entry) = interface_map.get_mut(name) {
                let mac = data.mac_address();
                let mac_str = format!(
                    "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
                    mac.0[0], mac.0[1], mac.0[2], mac.0[3], mac.0[4], mac.0[5]
                );
                // Don't store empty/null MACs
                if mac_str != "00:00:00:00:00:00" {
                    entry.mac_address = Some(mac_str);
                }
            }
        }

        // Convert to sorted vec for consistent ordering
        let mut interfaces: Vec<NetworkInterface> = interface_map.into_values().collect();
        interfaces.sort_by(|a, b| a.name.cmp(&b.name));

        // Filter out loopback and link-local only interfaces
        interfaces.retain(|iface| {
            // Keep if has any non-loopback IPv4
            let has_real_ipv4 = iface.ipv4_addresses.iter().any(|ip| {
                !ip.starts_with("127.") && !ip.starts_with("169.254.")
            });
            // Keep if has any non-link-local IPv6
            let has_real_ipv6 = iface.ipv6_addresses.iter().any(|ip| {
                !ip.starts_with("fe80:") && !ip.starts_with("::1")
            });
            has_real_ipv4 || has_real_ipv6
        });

        // Generate hash for change detection
        let state_hash = Self::compute_hash(&interfaces);

        NetworkState {
            timestamp: Utc::now(),
            interfaces,
            state_hash,
        }
    }

    /// Compute a simple hash of the network state for change detection
    fn compute_hash(interfaces: &[NetworkInterface]) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let mut hasher = DefaultHasher::new();
        for iface in interfaces {
            iface.name.hash(&mut hasher);
            iface.mac_address.hash(&mut hasher);
            for ip in &iface.ipv4_addresses {
                ip.hash(&mut hasher);
            }
            for ip in &iface.ipv6_addresses {
                ip.hash(&mut hasher);
            }
        }
        format!("{:016x}", hasher.finish())
    }

    /// Check if network state has changed compared to another state
    pub fn has_changed(&self, other: &NetworkState) -> bool {
        self.state_hash != other.state_hash
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_metrics_collection() {
        let collector = MetricsCollector::new();
        let metrics = collector.collect().await;

        // Basic sanity checks
        assert!(metrics.cpu_percent >= 0.0 && metrics.cpu_percent <= 100.0);
        assert!(metrics.memory_percent >= 0.0 && metrics.memory_percent <= 100.0);
        assert!(metrics.memory_total_bytes > 0);
        assert!(!metrics.os_type.is_empty());
        assert!(!metrics.hostname.is_empty());
    }

    #[test]
    fn test_system_info() {
        let collector = MetricsCollector::new();
        let info = collector.get_system_info();

        assert!(!info.os_type.is_empty());
        assert!(!info.hostname.is_empty());
        assert!(info.cpu_count > 0);
        assert!(info.total_memory_bytes > 0);
    }

    #[test]
    fn test_network_state_collection() {
        let state = NetworkState::collect();

        // Should have a valid timestamp
        assert!(state.timestamp <= Utc::now());

        // Should have a hash
        assert!(!state.state_hash.is_empty());
        assert_eq!(state.state_hash.len(), 16); // 64-bit hash as hex

        // Print for debugging
        println!("Network state collected:");
        for iface in &state.interfaces {
            println!("  {}: IPv4={:?}, IPv6={:?}, MAC={:?}",
                iface.name, iface.ipv4_addresses, iface.ipv6_addresses, iface.mac_address);
        }
    }

    #[test]
    fn test_network_state_change_detection() {
        let state1 = NetworkState::collect();
        let state2 = NetworkState::collect();

        // Same state should have same hash
        assert!(!state1.has_changed(&state2));

        // Create a modified state
        let mut modified = state1.clone();
        if let Some(iface) = modified.interfaces.first_mut() {
            iface.ipv4_addresses.push("10.99.99.99".to_string());
        }
        modified.state_hash = NetworkState::compute_hash(&modified.interfaces);

        // Modified state should be detected as changed
        assert!(state1.has_changed(&modified));
    }
}

+    /// Collect temperature readings from all available sensors

+    fn collect_temperatures(&self) -> (Option<f32>, Option<f32>, Vec<TemperatureSensor>) {

+        let components = Components::new_with_refreshed_list();

+        let mut temperatures = Vec::new();

+        let mut cpu_temps = Vec::new();

+        let mut gpu_temp: Option<f32> = None;

+            let label = component.label().to_string();

+            let temp = component.temperature();

+            let critical = component.critical();

+            // Skip invalid readings (some sensors report 0 or negative)

+            temperatures.push(TemperatureSensor {

+                critical_celsius: critical,

+            // Categorize temperatures for summary fields

+            let label_lower = label.to_lowercase();

+            if label_lower.contains("cpu")

+                || label_lower.contains("core")

+                || label_lower.contains("package")

+                || label_lower.contains("tctl")

+                || label_lower.contains("tdie")

+            } else if label_lower.contains("gpu")

+                || label_lower.contains("nvidia")

+                || label_lower.contains("radeon")

+                || label_lower.contains("amdgpu")

+                // Take the first GPU temperature found

+                if gpu_temp.is_none() {

+                    gpu_temp = Some(temp);

+        // Calculate average CPU temperature

+        let cpu_temp = if cpu_temps.is_empty() {

+            Some(cpu_temps.iter().sum::<f32>() / cpu_temps.len() as f32)

+        (cpu_temp, gpu_temp, temperatures)


Now I need to integrate the temperature collection into the collect() method. Let me find and update that section.


Now let me integrate temperature collection into the `collect()` method:


   230→            let os_version = System::os_version().unwrap_or_else(|| "unknown".to_string());
   231→            let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());
   232→
   233→            // Get uptime and boot time
   234→            let uptime_seconds = System::uptime();
   235→            let boot_time = System::boot_time() as i64;
   236→
   237→            // Get logged in user
   238→            let logged_in_user = self.get_logged_in_user();
   239→
   240→            // Get user idle time (platform-specific)
   241→            let user_idle_seconds = self.get_user_idle_time();
   242→
   243→            // Return all values - locks are dropped at end of this block
   244→            (
   245→                cpu_percent,
   246→                memory_percent,
   247→                memory_used,
   248→                memory_total,
   249→                disk_percent,
   250→                disk_used,
   251→                disk_total,
   252→                delta_rx,
   253→                delta_tx,
   254→                os_type,
   255→                os_version,
   256→                hostname,
   257→                uptime_seconds,
   258→                boot_time,
   259→                logged_in_user,
   260→                user_idle_seconds,
   261→            )
   262→        };
   263→
   264→        // All locks are now released - safe to do async work
   265→        // Get public IP (cached, refreshed every 5 minutes)
   266→        let public_ip = self.get_public_ip().await;
   267→
   268→        SystemMetrics {
   269→            timestamp: Utc::now(),
   270→            cpu_percent,
   271→            memory_percent,
   272→            memory_used_bytes: memory_used,
   273→            memory_total_bytes: memory_total,
   274→            disk_percent,
   275→            disk_used_bytes: disk_used,
   276→            disk_total_bytes: disk_total,
   277→            network_rx_bytes: delta_rx,
   278→            network_tx_bytes: delta_tx,
   279→            os_type,
   280→            os_version,
   281→            hostname,
   282→            uptime_seconds,
   283→            boot_time,
   284→            logged_in_user,
   285→            user_idle_seconds,
   286→            public_ip,
   287→        }
   288→    }
   289→
   290→    /// Get the currently logged in user
   291→    fn get_logged_in_user(&self) -> Option<String> {
   292→        let users = Users::new_with_refreshed_list();
   293→        // Return the first user found (typically the console user)
   294→        users.iter().next().map(|u| u.name().to_string())
   295→    }
   296→
   297→    /// Get user idle time in seconds (time since last keyboard/mouse input)
   298→    #[cfg(target_os = "windows")]
   299→    fn get_user_idle_time(&self) -> Option<u64> {
   300→        // Windows: Use GetLastInputInfo API
   301→        use std::mem;
   302→
   303→        #[repr(C)]
   304→        struct LASTINPUTINFO {
   305→            cb_size: u32,
   306→            dw_time: u32,
   307→        }
   308→
   309→        extern "system" {

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

            let os_version = System::os_version().unwrap_or_else(|| "unknown".to_string());
            let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());

            // Get uptime and boot time
            let uptime_seconds = System::uptime();
            let boot_time = System::boot_time() as i64;

            // Get logged in user
            let logged_in_user = self.get_logged_in_user();

            // Get user idle time (platform-specific)
            let user_idle_seconds = self.get_user_idle_time();

            // Return all values - locks are dropped at end of this block
            (
                cpu_percent,
                memory_percent,
                memory_used,
                memory_total,
                disk_percent,
                disk_used,
                disk_total,
                delta_rx,
                delta_tx,
                os_type,
                os_version,
                hostname,
                uptime_seconds,
                boot_time,
                logged_in_user,
                user_idle_seconds,
            )
        };

        // All locks are now released - safe to do async work
        // Get public IP (cached, refreshed every 5 minutes)
        let public_ip = self.get_public_ip().await;

        SystemMetrics {
            timestamp: Utc::now(),
            cpu_percent,
            memory_percent,
            memory_used_bytes: memory_used,
            memory_total_bytes: memory_total,
            disk_percent,
            disk_used_bytes: disk_used,
            disk_total_bytes: disk_total,
            network_rx_bytes: delta_rx,
            network_tx_bytes: delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
            public_ip,
        }
    }

    /// Get the currently logged in user
    fn get_logged_in_user(&self) -> Option<String> {
        let users = Users::new_with_refreshed_list();
        // Return the first user found (typically the console user)
        users.iter().next().map(|u| u.name().to_string())
    }

    /// Get user idle time in seconds (time since last keyboard/mouse input)
    #[cfg(target_os = "windows")]
    fn get_user_idle_time(&self) -> Option<u64> {
        // Windows: Use GetLastInputInfo API
        use std::mem;

        #[repr(C)]
        struct LASTINPUTINFO {
            cb_size: u32,
            dw_time: u32,
        }

        extern "system" {


Now I can see the collect() method. I need to add the temperature collection call and add the temperature fields to the returned SystemMetrics.


        // All locks are now released - safe to do async work
        // Get public IP (cached, refreshed every 5 minutes)
        let public_ip = self.get_public_ip().await;

        SystemMetrics {
            timestamp: Utc::now(),
            cpu_percent,
            memory_percent,
            memory_used_bytes: memory_used,
            memory_total_bytes: memory_total,
            disk_percent,
            disk_used_bytes: disk_used,
            disk_total_bytes: disk_total,
            network_rx_bytes: delta_rx,
            network_tx_bytes: delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
            public_ip,
        }
    }

        // All locks are now released - safe to do async work
        // Get public IP (cached, refreshed every 5 minutes)
        let public_ip = self.get_public_ip().await;

        // Collect temperature readings
        let (cpu_temp_celsius, gpu_temp_celsius, temperatures) = self.collect_temperatures();

        SystemMetrics {
            timestamp: Utc::now(),
            cpu_percent,
            memory_percent,
            memory_used_bytes: memory_used,
            memory_total_bytes: memory_total,
            disk_percent,
            disk_used_bytes: disk_used,
            disk_total_bytes: disk_total,
            network_rx_bytes: delta_rx,
            network_tx_bytes: delta_tx,
            os_type,
            os_version,
            hostname,
            uptime_seconds,
            boot_time,
            logged_in_user,
            user_idle_seconds,
            public_ip,
            cpu_temp_celsius,
            gpu_temp_celsius,
            temperatures,
        }
    }