sb-vault 0.1.0

S&B Vault // Zero-Trust Desktop Suite & Secret Engine
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SshSessionRecord {
    pub session_id: String,
    pub server_name: String,
    pub user: String,
    pub client_ip: Option<String>,
    pub started_at: String,
    pub duration_seconds: u64,
    pub auth_method: String,
    pub recording_file: Option<String>,
    pub status: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PipelineRunRecord {
    pub run_id: String,
    pub pipeline_or_recipe: String,
    pub is_pipeline: bool,
    pub env: String,
    pub user: String,
    pub started_at: String,
    pub ended_at: String,
    pub duration_ms: u64,
    pub status: String,
    pub stages_total: usize,
    pub stages_passed: usize,
    pub failed_step: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AuditOverview {
    pub ssh_sessions: Vec<SshSessionRecord>,
    pub pipeline_runs: Vec<PipelineRunRecord>,
    pub available_recordings: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AsciinemaFrame {
    pub time: f64,
    pub event_type: String,
    pub data: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AsciinemaCast {
    pub version: u32,
    pub width: u32,
    pub height: u32,
    pub title: Option<String>,
    pub duration: f64,
    pub frames: Vec<AsciinemaFrame>,
}

#[tauri::command]
pub fn get_audit_records() -> Result<AuditOverview, String> {
    let mut ssh_sessions = Vec::new();
    let mut pipeline_runs = Vec::new();
    let mut available_recordings = Vec::new();

    if let Some(home) = dirs::home_dir() {
        // 1. SSH Sessions
        let ssh_audit = home.join(".sb-ssh").join("audit").join("sessions.jsonl");
        if ssh_audit.exists() {
            if let Ok(content) = std::fs::read_to_string(&ssh_audit) {
                for line in content.lines().rev() {
                    if let Ok(rec) = serde_json::from_str::<SshSessionRecord>(line) {
                        ssh_sessions.push(rec);
                    }
                }
            }
        }

        // 2. Pipeline Runs
        let forge_audit = home.join(".sb-forge").join("audit").join("runs.jsonl");
        if forge_audit.exists() {
            if let Ok(content) = std::fs::read_to_string(&forge_audit) {
                for line in content.lines().rev() {
                    if let Ok(rec) = serde_json::from_str::<PipelineRunRecord>(line) {
                        pipeline_runs.push(rec);
                    }
                }
            }
        }

        // 3. Recordings (.cast)
        let recordings_dir = home.join(".sb-ssh").join("recordings");
        if recordings_dir.exists() {
            if let Ok(entries) = std::fs::read_dir(&recordings_dir) {
                for entry in entries.flatten() {
                    let path = entry.path();
                    if path.extension().and_then(|e| e.to_str()) == Some("cast") {
                        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                            available_recordings.push(name.to_string());
                        }
                    }
                }
            }
        }
    }

    Ok(AuditOverview {
        ssh_sessions,
        pipeline_runs,
        available_recordings,
    })
}

#[tauri::command]
pub fn load_session_recording(cast_filename: String) -> Result<AsciinemaCast, String> {
    let home = dirs::home_dir().ok_or_else(|| "Home-Verzeichnis nicht gefunden".to_string())?;
    let path = home.join(".sb-ssh").join("recordings").join(&cast_filename);

    if !path.exists() {
        return Err(format!("Aufnahme '{}' existiert nicht", cast_filename));
    }

    let content = std::fs::read_to_string(&path)
        .map_err(|e| format!("Datei konnte nicht gelesen werden: {}", e))?;

    let mut lines = content.lines();
    let header_line = lines.next().ok_or_else(|| "Leere .cast Datei".to_string())?;
    let header_json: serde_json::Value = serde_json::from_str(header_line)
        .map_err(|e| format!("Ungültiger Header: {}", e))?;

    let width = header_json.get("width").and_then(|w| w.as_u64()).unwrap_or(80) as u32;
    let height = header_json.get("height").and_then(|h| h.as_u64()).unwrap_or(24) as u32;
    let title = header_json.get("title").and_then(|t| t.as_str()).map(String::from);

    let mut frames = Vec::new();
    let mut duration = 0.0;

    for line in lines {
        if line.trim().is_empty() {
            continue;
        }
        if let Ok(val) = serde_json::from_str::<serde_json::Value>(line) {
            if let Some(arr) = val.as_array() {
                if arr.len() >= 3 {
                    let time = arr[0].as_f64().unwrap_or(0.0);
                    let event_type = arr[1].as_str().unwrap_or("o").to_string();
                    let data = arr[2].as_str().unwrap_or("").to_string();
                    if time > duration {
                        duration = time;
                    }
                    frames.push(AsciinemaFrame {
                        time,
                        event_type,
                        data,
                    });
                }
            }
        }
    }

    Ok(AsciinemaCast {
        version: 2,
        width,
        height,
        title,
        duration,
        frames,
    })
}