Now I have a complete picture of the codebase. Let me provide a comprehensive implementation plan for the stackable policy system.

---

# Stackable Policy System for GuruRMM - Implementation Plan

## Executive Summary

This plan designs a hierarchical policy system where policies can be assigned at Client, Site, and Agent levels, with a "last wins" merge strategy (Agent overrides Site overrides Client). The system will use the existing WebSocket `ConfigUpdate` message for policy delivery and evaluate thresholds on the server during metrics ingestion.

---

## 1. Database Schema

### 1.1 New Tables

#### `policies` Table
Stores policy definitions as JSON documents. Each policy is a reusable template that can be assigned to multiple targets.

```sql
-- Policy definitions (reusable policy templates)
CREATE TABLE policies (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    -- Policy content stored as JSONB for flexibility
    policy_data JSONB NOT NULL DEFAULT '{}',
    is_default BOOLEAN DEFAULT FALSE,  -- Mark as system default
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL
);

CREATE INDEX idx_policies_name ON policies(name);
CREATE INDEX idx_policies_default ON policies(is_default) WHERE is_default = TRUE;

-- Trigger for updated_at
CREATE TRIGGER update_policies_updated_at
    BEFORE UPDATE ON policies
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();
```

#### `policy_assignments` Table
Links policies to targets (clients, sites, or agents) with a priority system.

```sql
-- Policy assignments (links policies to targets)
CREATE TABLE policy_assignments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    policy_id UUID NOT NULL REFERENCES policies(id) ON DELETE CASCADE,
    -- Target: exactly one of these should be set
    client_id UUID REFERENCES clients(id) ON DELETE CASCADE,
    site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
    agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
    -- Priority within same level (higher = takes precedence)
    priority INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL,

    -- Ensure exactly one target is set
    CONSTRAINT policy_assignment_target_check CHECK (
        (client_id IS NOT NULL)::int +
        (site_id IS NOT NULL)::int +
        (agent_id IS NOT NULL)::int = 1
    )
);

CREATE INDEX idx_policy_assignments_policy ON policy_assignments(policy_id);
CREATE INDEX idx_policy_assignments_client ON policy_assignments(client_id) WHERE client_id IS NOT NULL;
CREATE INDEX idx_policy_assignments_site ON policy_assignments(site_id) WHERE site_id IS NOT NULL;
CREATE INDEX idx_policy_assignments_agent ON policy_assignments(agent_id) WHERE agent_id IS NOT NULL;
```

#### `alerts` Table
Stores triggered alerts for dashboard display.

```sql
-- Alerts triggered by threshold evaluation
CREATE TABLE alerts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    -- Alert categorization
    alert_type VARCHAR(50) NOT NULL,  -- 'cpu', 'memory', 'disk', 'temperature', 'service', 'custom'
    severity VARCHAR(20) NOT NULL DEFAULT 'warning',  -- 'info', 'warning', 'critical'
    -- Alert content
    title VARCHAR(255) NOT NULL,
    message TEXT,
    metric_value REAL,  -- The value that triggered the alert
    threshold_value REAL,  -- The threshold that was exceeded
    -- State management
    status VARCHAR(20) DEFAULT 'active',  -- 'active', 'acknowledged', 'resolved'
    acknowledged_at TIMESTAMPTZ,
    acknowledged_by UUID REFERENCES users(id) ON DELETE SET NULL,
    resolved_at TIMESTAMPTZ,
    -- Deduplication: hash of agent_id + alert_type + relevant identifiers
    dedup_key VARCHAR(255),
    -- Timestamps
    triggered_at TIMESTAMPTZ DEFAULT NOW(),
    last_seen_at TIMESTAMPTZ DEFAULT NOW(),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_alerts_agent ON alerts(agent_id);
CREATE INDEX idx_alerts_status ON alerts(status);
CREATE INDEX idx_alerts_type_status ON alerts(alert_type, status);
CREATE INDEX idx_alerts_triggered ON alerts(triggered_at DESC);
CREATE INDEX idx_alerts_dedup ON alerts(dedup_key) WHERE status = 'active';
```

#### `alert_thresholds_state` Table
Tracks threshold violation state for "X% for Y minutes" logic.

```sql
-- Threshold violation state tracking (for duration-based alerts)
CREATE TABLE alert_threshold_state (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    threshold_key VARCHAR(100) NOT NULL,  -- e.g., 'cpu_warning', 'memory_critical'
    -- State tracking
    violation_started_at TIMESTAMPTZ,
    consecutive_violations INTEGER DEFAULT 0,
    last_value REAL,
    last_checked_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(agent_id, threshold_key)
);

CREATE INDEX idx_threshold_state_agent ON alert_threshold_state(agent_id);
```

---

## 2. Policy Data Structure

### 2.1 Rust Structs (Server-side)

```rust
// server/src/db/policies.rs

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

/// Complete policy structure stored as JSONB
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PolicyData {
    /// Metrics collection settings
    #[serde(default)]
    pub metrics: Option<MetricsPolicy>,

    /// Alert thresholds
    #[serde(default)]
    pub thresholds: Option<ThresholdsPolicy>,

    /// System tray settings
    #[serde(default)]
    pub tray: Option<TrayPolicy>,

    /// Auto-update settings
    #[serde(default)]
    pub updates: Option<UpdatesPolicy>,

    /// Watchdog configuration
    #[serde(default)]
    pub watchdog: Option<WatchdogPolicy>,
}

/// Metrics collection policy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsPolicy {
    /// Collection interval in seconds (10-3600)
    pub interval_seconds: Option<u64>,
    /// Which metrics to collect
    pub collect_cpu: Option<bool>,
    pub collect_memory: Option<bool>,
    pub collect_disk: Option<bool>,
    pub collect_network: Option<bool>,
    pub collect_temperatures: Option<bool>,
    /// Extended metrics
    pub collect_user_info: Option<bool>,
    pub collect_public_ip: Option<bool>,
}

/// Alert thresholds policy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThresholdsPolicy {
    /// CPU thresholds
    pub cpu: Option<ResourceThreshold>,
    /// Memory thresholds
    pub memory: Option<ResourceThreshold>,
    /// Disk thresholds
    pub disk: Option<ResourceThreshold>,
    /// Temperature thresholds (Celsius)
    pub cpu_temp: Option<TemperatureThreshold>,
    pub gpu_temp: Option<TemperatureThreshold>,
}

/// Threshold for a resource (CPU/Memory/Disk)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceThreshold {
    /// Warning threshold percentage
    pub warning_percent: Option<f32>,
    /// Critical threshold percentage
    pub critical_percent: Option<f32>,
    /// Duration in seconds before triggering (0 = immediate)
    pub duration_seconds: Option<u64>,
}

/// Temperature threshold
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemperatureThreshold {
    pub warning_celsius: Option<f32>,
    pub critical_celsius: Option<f32>,
}

/// Tray application policy (matches existing TrayPolicy)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrayPolicy {
    pub enabled: Option<bool>,
    pub show_status: Option<bool>,
    pub show_info: Option<bool>,
    pub allow_force_checkin: Option<bool>,
    pub allow_view_logs: Option<bool>,
    pub allow_open_dashboard: Option<bool>,
    pub allow_stop_agent: Option<bool>,
    pub dashboard_url: Option<String>,
    pub tooltip_text: Option<String>,
    pub custom_icon: Option<String>,
}

/// Auto-update policy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdatesPolicy {
    /// Enable automatic updates
    pub auto_update: Option<bool>,
    /// Maintenance window (24h format, e.g., "02:00-06:00")
    pub maintenance_window: Option<String>,
    /// Channels: stable, beta, dev
    pub channel: Option<String>,
    /// Defer updates for N hours after release
    pub defer_hours: Option<u32>,
}

/// Watchdog policy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchdogPolicy {
    pub enabled: Option<bool>,
    pub check_interval_seconds: Option<u64>,
    pub services: Option<Vec<ServiceWatchPolicy>>,
    pub processes: Option<Vec<ProcessWatchPolicy>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceWatchPolicy {
    pub name: String,
    pub action: String,  // "alert", "restart", "ignore"
    pub max_restarts: Option<u32>,
    pub restart_cooldown_seconds: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessWatchPolicy {
    pub name: String,
    pub action: String,
    pub start_command: Option<String>,
}
```

### 2.2 Database Record Structs

```rust
/// Policy record from database
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Policy {
    pub id: Uuid,
    pub name: String,
    pub description: Option<String>,
    pub policy_data: sqlx::types::Json<PolicyData>,
    pub is_default: bool,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
    pub created_by: Option<Uuid>,
}

/// Policy assignment record
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct PolicyAssignment {
    pub id: Uuid,
    pub policy_id: Uuid,
    pub client_id: Option<Uuid>,
    pub site_id: Option<Uuid>,
    pub agent_id: Option<Uuid>,
    pub priority: i32,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub created_by: Option<Uuid>,
}

/// Assignment level for merge ordering
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AssignmentLevel {
    Default = 0,
    Client = 1,
    Site = 2,
    Agent = 3,
}
```

---

## 3. Policy Merge Logic

### 3.1 Merge Strategy

The merge follows a "deep merge with last wins" strategy:

1. Start with system defaults
2. Apply Client-level policies (sorted by priority)
3. Apply Site-level policies (sorted by priority)
4. Apply Agent-level policies (sorted by priority)

For each field:
- `None` values are skipped (no override)
- `Some` values replace previous values
- Arrays (services/processes) are concatenated, with later entries overriding same-name entries

### 3.2 Implementation

```rust
// server/src/policy/merge.rs

impl PolicyData {
    /// Create default policy with sensible defaults
    pub fn system_defaults() -> Self {
        Self {
            metrics: Some(MetricsPolicy {
                interval_seconds: Some(60),
                collect_cpu: Some(true),
                collect_memory: Some(true),
                collect_disk: Some(true),
                collect_network: Some(true),
                collect_temperatures: Some(true),
                collect_user_info: Some(true),
                collect_public_ip: Some(true),
            }),
            thresholds: Some(ThresholdsPolicy {
                cpu: Some(ResourceThreshold {
                    warning_percent: Some(80.0),
                    critical_percent: Some(95.0),
                    duration_seconds: Some(300),  // 5 minutes
                }),
                memory: Some(ResourceThreshold {
                    warning_percent: Some(85.0),
                    critical_percent: Some(95.0),
                    duration_seconds: Some(300),
                }),
                disk: Some(ResourceThreshold {
                    warning_percent: Some(85.0),
                    critical_percent: Some(95.0),
                    duration_seconds: Some(0),  // Immediate
                }),
                cpu_temp: Some(TemperatureThreshold {
                    warning_celsius: Some(80.0),
                    critical_celsius: Some(95.0),
                }),
                gpu_temp: Some(TemperatureThreshold {
                    warning_celsius: Some(85.0),
                    critical_celsius: Some(100.0),
                }),
            }),
            tray: Some(TrayPolicy {
                enabled: Some(true),
                show_status: Some(true),
                show_info: Some(true),
                allow_force_checkin: Some(true),
                allow_view_logs: Some(false),
                allow_open_dashboard: Some(true),
                allow_stop_agent: Some(false),
                dashboard_url: None,
                tooltip_text: None,
                custom_icon: None,
            }),
            updates: Some(UpdatesPolicy {
                auto_update: Some(true),
                maintenance_window: None,
                channel: Some("stable".to_string()),
                defer_hours: Some(24),
            }),
            watchdog: Some(WatchdogPolicy {
                enabled: Some(false),
                check_interval_seconds: Some(30),
                services: Some(vec![]),
                processes: Some(vec![]),
            }),
        }
    }

    /// Merge another policy into this one (other takes precedence)
    pub fn merge_from(&mut self, other: &PolicyData) {
        if let Some(ref metrics) = other.metrics {
            self.metrics = Some(self.metrics.take().unwrap_or_default().merge(metrics));
        }
        if let Some(ref thresholds) = other.thresholds {
            self.thresholds = Some(self.thresholds.take().unwrap_or_default().merge(thresholds));
        }
        if let Some(ref tray) = other.tray {
            self.tray = Some(self.tray.take().unwrap_or_default().merge(tray));
        }
        if let Some(ref updates) = other.updates {
            self.updates = Some(self.updates.take().unwrap_or_default().merge(updates));
        }
        if let Some(ref watchdog) = other.watchdog {
            self.watchdog = Some(self.watchdog.take().unwrap_or_default().merge(watchdog));
        }
    }
}

// Example merge trait implementation for a sub-policy
impl MetricsPolicy {
    pub fn merge(&self, other: &Self) -> Self {
        Self {
            interval_seconds: other.interval_seconds.or(self.interval_seconds),
            collect_cpu: other.collect_cpu.or(self.collect_cpu),
            collect_memory: other.collect_memory.or(self.collect_memory),
            collect_disk: other.collect_disk.or(self.collect_disk),
            collect_network: other.collect_network.or(self.collect_network),
            collect_temperatures: other.collect_temperatures.or(self.collect_temperatures),
            collect_user_info: other.collect_user_info.or(self.collect_user_info),
            collect_public_ip: other.collect_public_ip.or(self.collect_public_ip),
        }
    }
}
```

### 3.3 Effective Policy Calculation

```rust
// server/src/policy/effective.rs

/// Calculate effective policy for an agent
pub async fn get_effective_policy(
    pool: &PgPool,
    agent_id: Uuid,
) -> Result<PolicyData, sqlx::Error> {
    // Get agent's site and client
    let agent_context = sqlx::query_as::<_, AgentPolicyContext>(
        r#"
        SELECT
            a.id as agent_id,
            a.site_id,
            s.client_id
        FROM agents a
        LEFT JOIN sites s ON a.site_id = s.id
        WHERE a.id = $1
        "#,
    )
    .bind(agent_id)
    .fetch_one(pool)
    .await?;

    // Start with system defaults
    let mut effective = PolicyData::system_defaults();

    // Get default policy if exists
    if let Some(default_policy) = get_default_policy(pool).await? {
        effective.merge_from(&default_policy.policy_data.0);
    }

    // Apply client-level policies
    if let Some(client_id) = agent_context.client_id {
        let client_policies = get_policies_for_target(pool, "client", client_id).await?;
        for policy in client_policies {
            effective.merge_from(&policy.policy_data.0);
        }
    }

    // Apply site-level policies
    if let Some(site_id) = agent_context.site_id {
        let site_policies = get_policies_for_target(pool, "site", site_id).await?;
        for policy in site_policies {
            effective.merge_from(&policy.policy_data.0);
        }
    }

    // Apply agent-level policies
    let agent_policies = get_policies_for_target(pool, "agent", agent_id).await?;
    for policy in agent_policies {
        effective.merge_from(&policy.policy_data.0);
    }

    Ok(effective)
}

async fn get_policies_for_target(
    pool: &PgPool,
    target_type: &str,
    target_id: Uuid,
) -> Result<Vec<Policy>, sqlx::Error> {
    let query = match target_type {
        "client" => r#"
            SELECT p.* FROM policies p
            JOIN policy_assignments pa ON p.id = pa.policy_id
            WHERE pa.client_id = $1
            ORDER BY pa.priority ASC
        "#,
        "site" => r#"
            SELECT p.* FROM policies p
            JOIN policy_assignments pa ON p.id = pa.policy_id
            WHERE pa.site_id = $1
            ORDER BY pa.priority ASC
        "#,
        "agent" => r#"
            SELECT p.* FROM policies p
            JOIN policy_assignments pa ON p.id = pa.policy_id
            WHERE pa.agent_id = $1
            ORDER BY pa.priority ASC
        "#,
        _ => return Err(sqlx::Error::Protocol("Invalid target type".into())),
    };

    sqlx::query_as::<_, Policy>(query)
        .bind(target_id)
        .fetch_all(pool)
        .await
}
```

---

## 4. API Endpoints

### 4.1 Policy CRUD Routes

Add to `server/src/api/mod.rs`:

```rust
// Policies
.route("/policies", get(policies::list_policies))
.route("/policies", post(policies::create_policy))
.route("/policies/:id", get(policies::get_policy))
.route("/policies/:id", put(policies::update_policy))
.route("/policies/:id", delete(policies::delete_policy))
// Policy assignments
.route("/policies/:id/assignments", get(policies::list_assignments))
.route("/policies/:id/assign", post(policies::assign_policy))
.route("/policy-assignments/:id", delete(policies::remove_assignment))
// Effective policy for an agent
.route("/agents/:id/effective-policy", get(policies::get_effective_policy))
// Deploy policy to connected agents
.route("/policies/:id/deploy", post(policies::deploy_policy))
// Alerts
.route("/alerts", get(alerts::list_alerts))
.route("/alerts/:id", get(alerts::get_alert))
.route("/alerts/:id/acknowledge", post(alerts::acknowledge_alert))
.route("/alerts/:id/resolve", post(alerts::resolve_alert))
.route("/alerts/summary", get(alerts::get_summary))
```

### 4.2 API Implementation (Key Endpoints)

```rust
// server/src/api/policies.rs

/// Assign a policy to a target
#[derive(Debug, Deserialize)]
pub struct AssignPolicyRequest {
    pub target_type: String,  // "client", "site", or "agent"
    pub target_id: Uuid,
    pub priority: Option<i32>,
}

pub async fn assign_policy(
    State(state): State<AppState>,
    Path(policy_id): Path<Uuid>,
    _auth: AuthUser,
    Json(request): Json<AssignPolicyRequest>,
) -> Result<Json<PolicyAssignment>, ApiError> {
    let assignment = db::create_policy_assignment(
        &state.db,
        policy_id,
        &request.target_type,
        request.target_id,
        request.priority.unwrap_or(0),
    ).await?;

    // Trigger policy push to affected agents
    push_policy_to_affected_agents(&state, &request.target_type, request.target_id).await;

    Ok(Json(assignment))
}

/// Get effective policy for an agent
pub async fn get_effective_policy(
    State(state): State<AppState>,
    Path(agent_id): Path<Uuid>,
    _auth: AuthUser,
) -> Result<Json<PolicyData>, ApiError> {
    let policy = policy::get_effective_policy(&state.db, agent_id).await?;
    Ok(Json(policy))
}

/// Deploy policy to all affected connected agents
async fn push_policy_to_affected_agents(
    state: &AppState,
    target_type: &str,
    target_id: Uuid,
) {
    // Get all affected agent IDs
    let agent_ids = match target_type {
        "client" => db::get_agent_ids_by_client(&state.db, target_id).await,
        "site" => db::get_agent_ids_by_site(&state.db, target_id).await,
        "agent" => Ok(vec![target_id]),
        _ => return,
    };

    if let Ok(agent_ids) = agent_ids {
        let agents = state.agents.read().await;
        for agent_id in agent_ids {
            if agents.is_connected(&agent_id) {
                // Calculate and send effective policy
                if let Ok(policy) = policy::get_effective_policy(&state.db, agent_id).await {
                    let config_msg = ServerMessage::ConfigUpdate(serde_json::to_value(&policy).unwrap());
                    let _ = agents.send_to(&agent_id, config_msg).await;
                }
            }
        }
    }
}
```

---

## 5. Alert Evaluation System

### 5.1 Threshold Evaluation on Metrics Ingestion

Modify `handle_agent_message` in `server/src/ws/mod.rs`:

```rust
AgentMessage::Metrics(metrics) => {
    debug!("Received metrics from agent {}: CPU={:.1}%", agent_id, metrics.cpu_percent);

    // Store metrics in database (existing code)
    let create_metrics = db::CreateMetrics { /* ... */ };
    db::insert_metrics(&state.db, create_metrics).await?;

    // NEW: Evaluate thresholds and create alerts
    evaluate_thresholds(&state.db, agent_id, &metrics).await;

    // Update last_seen (existing code)
    db::update_agent_status(&state.db, agent_id, "online").await?;
}
```

### 5.2 Threshold Evaluator

```rust
// server/src/alerts/evaluator.rs

pub async fn evaluate_thresholds(
    pool: &PgPool,
    agent_id: Uuid,
    metrics: &MetricsPayload,
) {
    // Get effective policy for this agent
    let policy = match policy::get_effective_policy(pool, agent_id).await {
        Ok(p) => p,
        Err(_) => return,  // Use defaults or skip
    };

    let thresholds = match policy.thresholds {
        Some(t) => t,
        None => return,
    };

    // Evaluate each threshold
    if let Some(ref cpu_threshold) = thresholds.cpu {
        evaluate_resource_threshold(
            pool,
            agent_id,
            "cpu",
            metrics.cpu_percent,
            cpu_threshold,
        ).await;
    }

    if let Some(ref memory_threshold) = thresholds.memory {
        evaluate_resource_threshold(
            pool,
            agent_id,
            "memory",
            metrics.memory_percent,
            memory_threshold,
        ).await;
    }

    if let Some(ref disk_threshold) = thresholds.disk {
        evaluate_resource_threshold(
            pool,
            agent_id,
            "disk",
            metrics.disk_percent,
            disk_threshold,
        ).await;
    }

    // Temperature thresholds
    if let (Some(cpu_temp), Some(ref threshold)) = (metrics.cpu_temp_celsius, &thresholds.cpu_temp) {
        evaluate_temperature_threshold(pool, agent_id, "cpu_temp", cpu_temp, threshold).await;
    }
}

async fn evaluate_resource_threshold(
    pool: &PgPool,
    agent_id: Uuid,
    resource: &str,
    current_value: f32,
    threshold: &ResourceThreshold,
) {
    let critical_exceeded = threshold.critical_percent
        .map(|t| current_value >= t)
        .unwrap_or(false);
    let warning_exceeded = threshold.warning_percent
        .map(|t| current_value >= t)
        .unwrap_or(false);

    let severity = if critical_exceeded {
        "critical"
    } else if warning_exceeded {
        "warning"
    } else {
        // Below threshold - check if we should resolve existing alert
        resolve_if_below_threshold(pool, agent_id, resource).await;
        return;
    };

    let threshold_key = format!("{}_{}", resource, severity);
    let duration_seconds = threshold.duration_seconds.unwrap_or(0);

    if duration_seconds == 0 {
        // Immediate alert
        create_or_update_alert(pool, agent_id, resource, severity, current_value,
            threshold.critical_percent.or(threshold.warning_percent).unwrap_or(0.0)).await;
    } else {
        // Duration-based alert
        check_duration_threshold(
            pool, agent_id, &threshold_key, current_value,
            duration_seconds, severity, resource,
            threshold.critical_percent.or(threshold.warning_percent).unwrap_or(0.0)
        ).await;
    }
}

async fn check_duration_threshold(
    pool: &PgPool,
    agent_id: Uuid,
    threshold_key: &str,
    current_value: f32,
    required_duration_seconds: u64,
    severity: &str,
    resource: &str,
    threshold_value: f32,
) {
    // Get or create threshold state
    let state = db::get_or_create_threshold_state(pool, agent_id, threshold_key).await;

    if let Ok(mut state) = state {
        let now = chrono::Utc::now();

        if state.violation_started_at.is_some() {
            // We're in a violation period - check duration
            let violation_start = state.violation_started_at.unwrap();
            let elapsed = (now - violation_start).num_seconds() as u64;

            if elapsed >= required_duration_seconds {
                // Duration exceeded - create alert
                create_or_update_alert(pool, agent_id, resource, severity, current_value, threshold_value).await;
            }

            // Update state
            state.consecutive_violations += 1;
            state.last_value = current_value;
            state.last_checked_at = now;
        } else {
            // Start new violation period
            state.violation_started_at = Some(now);
            state.consecutive_violations = 1;
            state.last_value = current_value;
            state.last_checked_at = now;
        }

        let _ = db::update_threshold_state(pool, &state).await;
    }
}

async fn create_or_update_alert(
    pool: &PgPool,
    agent_id: Uuid,
    alert_type: &str,
    severity: &str,
    metric_value: f32,
    threshold_value: f32,
) {
    let dedup_key = format!("{}:{}:{}", agent_id, alert_type, severity);
    let title = format!("{} {} - {:.1}%",
        alert_type.to_uppercase(),
        severity.to_uppercase(),
        metric_value
    );
    let message = format!(
        "{} usage at {:.1}% exceeds {} threshold of {:.1}%",
        alert_type, metric_value, severity, threshold_value
    );

    // Try to find existing active alert with same dedup_key
    let existing = db::get_active_alert_by_dedup_key(pool, &dedup_key).await;

    match existing {
        Ok(Some(alert)) => {
            // Update existing alert
            let _ = db::update_alert_last_seen(pool, alert.id, metric_value).await;
        }
        _ => {
            // Create new alert
            let _ = db::create_alert(pool, db::CreateAlert {
                agent_id,
                alert_type: alert_type.to_string(),
                severity: severity.to_string(),
                title,
                message: Some(message),
                metric_value: Some(metric_value),
                threshold_value: Some(threshold_value),
                dedup_key: Some(dedup_key),
            }).await;
        }
    }
}
```

---

## 6. Agent-Side Policy Application

### 6.1 Enhanced ConfigUpdatePayload

Update `agent/src/transport/mod.rs`:

```rust
/// Configuration update payload - full effective policy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigUpdatePayload {
    /// Metrics collection settings
    #[serde(default)]
    pub metrics: Option<MetricsPolicyAgent>,

    /// Alert thresholds (for reference/display only - evaluated server-side)
    #[serde(default)]
    pub thresholds: Option<serde_json::Value>,

    /// System tray settings
    #[serde(default)]
    pub tray: Option<crate::ipc::TrayPolicy>,

    /// Auto-update settings
    #[serde(default)]
    pub updates: Option<UpdatesPolicyAgent>,

    /// Watchdog configuration
    #[serde(default)]
    pub watchdog: Option<WatchdogPolicyAgent>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsPolicyAgent {
    pub interval_seconds: Option<u64>,
    pub collect_cpu: Option<bool>,
    pub collect_memory: Option<bool>,
    pub collect_disk: Option<bool>,
    pub collect_network: Option<bool>,
    pub collect_temperatures: Option<bool>,
    pub collect_user_info: Option<bool>,
    pub collect_public_ip: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdatesPolicyAgent {
    pub auto_update: Option<bool>,
    pub maintenance_window: Option<String>,
    pub channel: Option<String>,
    pub defer_hours: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchdogPolicyAgent {
    pub enabled: Option<bool>,
    pub check_interval_seconds: Option<u64>,
    pub services: Option<Vec<serde_json::Value>>,
    pub processes: Option<Vec<serde_json::Value>>,
}
```

### 6.2 Agent Runtime Policy Application

In `agent/src/transport/websocket.rs`, update `handle_server_message`:

```rust
ServerMessage::ConfigUpdate(update) => {
    info!("Received config update");

    // Parse the update payload
    if let Ok(policy) = serde_json::from_value::<ConfigUpdatePayload>(update.clone()) {
        // Apply metrics settings
        if let Some(ref metrics) = policy.metrics {
            apply_metrics_policy(metrics).await;
        }

        // Apply tray policy via IPC
        if let Some(ref tray) = policy.tray {
            // Update tray policy in state and broadcast to connected tray apps
            if let Some(ipc_state) = get_ipc_state() {
                ipc_state.update_policy(tray.clone()).await;
            }
        }

        // Apply watchdog settings
        if let Some(ref watchdog) = policy.watchdog {
            apply_watchdog_policy(watchdog).await;
        }

        // Store update settings for auto-updater reference
        if let Some(ref updates) = policy.updates {
            store_update_policy(updates).await;
        }
    }
}
```

---

## 7. WebSocket ConfigUpdate Enhancement

### 7.1 Server-Side: Send Policy on Connection

In `server/src/ws/mod.rs`, after successful authentication:

```rust
// After: state.agents.write().await.add(result.agent_id, tx.clone());

// Send initial effective policy
if let Ok(policy) = policy::get_effective_policy(&state.db, result.agent_id).await {
    let config_msg = ServerMessage::ConfigUpdate(serde_json::to_value(&policy).unwrap());
    if let Err(e) = tx.send(config_msg).await {
        warn!("Failed to send initial policy: {}", e);
    }
}
```

### 7.2 Trigger Policy Push on Assignment Changes

The `push_policy_to_affected_agents` function (shown in section 4.2) handles this when policies are assigned, updated, or removed.

---

## 8. Implementation Sequence

### Phase 1: Database & Core Structures
1. Create migration file `006_policies.sql` with all tables
2. Add `server/src/db/policies.rs` with CRUD operations
3. Add `server/src/db/alerts.rs` with alert operations
4. Create `PolicyData` and related structs

### Phase 2: Policy Merge Logic
1. Create `server/src/policy/mod.rs` module
2. Implement `merge.rs` with merge logic
3. Implement `effective.rs` for effective policy calculation
4. Add unit tests for merge logic

### Phase 3: API Endpoints
1. Create `server/src/api/policies.rs`
2. Create `server/src/api/alerts.rs`
3. Add routes to `server/src/api/mod.rs`
4. Test CRUD operations via API

### Phase 4: Alert Evaluation
1. Create `server/src/alerts/mod.rs` module
2. Implement `evaluator.rs` with threshold logic
3. Integrate into metrics ingestion handler
4. Add threshold state tracking

### Phase 5: Agent Integration
1. Update `ConfigUpdatePayload` in agent
2. Implement policy application in agent runtime
3. Update tray policy handling
4. Send policy on agent connection

### Phase 6: Dashboard Integration
1. Policy management UI (create, edit, assign)
2. Alert dashboard with filtering
3. Effective policy viewer per agent
4. Policy deployment status

---

## 9. Critical Files for Implementation

### Critical Files for Implementation

- **`C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db\mod.rs`** - Add policies.rs and alerts.rs module exports
- **`C:\Users\MikeSwanson\claude-projects\gururmm\server\src\ws\mod.rs`** - Integrate threshold evaluation into metrics handler, send policy on connection
- **`C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\mod.rs`** - Add policy and alert API routes
- **`C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\transport\mod.rs`** - Enhance ConfigUpdatePayload structure
- **`C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\transport\websocket.rs`** - Implement policy application in handle_server_message