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() {
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);
}
}
}
}
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);
}
}
}
}
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,
})
}