use crate::{
Error, Result, safety::bypass::BypassLogEntry, safety::bypass::BypassManager,
safety::config::BypassConfig, safety::report::SafetyReport,
};
use console::style;
use std::path::PathBuf;
use tokio::fs;
pub async fn handle_report(
limit: usize,
include_audit: bool,
stage_filter: Option<String>,
) -> Result<()> {
println!("{}", style("📊 Safety Reports").bold());
println!("{}", "=".repeat(60));
println!();
let reports = load_recent_reports(limit).await?;
if reports.is_empty() {
println!("{}", style("No safety reports found.").dim());
println!();
println!("Reports are generated when safety checks run.");
println!("Try running: ferrous-forge safety check --stage=pre-commit");
} else {
let filtered_reports: Vec<_> = if let Some(ref stage) = stage_filter {
reports
.into_iter()
.filter(|r| r.stage.name().to_lowercase() == stage.to_lowercase())
.collect()
} else {
reports
};
println!("Showing last {} safety reports:\n", filtered_reports.len());
for (i, report) in filtered_reports.iter().enumerate() {
print_report_summary(i + 1, report);
}
}
if include_audit {
println!();
print_audit_section(limit).await?;
}
Ok(())
}
async fn load_recent_reports(limit: usize) -> Result<Vec<SafetyReport>> {
let reports_dir = match get_reports_dir() {
Ok(dir) => dir,
Err(_) => return Ok(Vec::new()),
};
if !reports_dir.exists() {
return Ok(Vec::new());
}
let mut entries = fs::read_dir(&reports_dir)
.await
.map_err(|e| Error::config(format!("Failed to read reports directory: {}", e)))?;
let mut reports = Vec::new();
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "json")
&& let Ok(contents) = fs::read_to_string(&path).await
&& let Ok(report) = serde_json::from_str::<SafetyReport>(&contents)
{
reports.push(report);
}
}
reports.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
reports.truncate(limit);
Ok(reports)
}
fn print_report_summary(index: usize, report: &SafetyReport) {
let status = if report.passed {
style("✅ PASSED").green()
} else {
style("❌ FAILED").red()
};
let stage_name = report.stage.display_name();
let timestamp = report.timestamp.format("%Y-%m-%d %H:%M:%S UTC");
println!("{}. {} - {}", index, status, style(stage_name).cyan());
println!(" Time: {}", style(timestamp).dim());
println!(" Duration: {:.2}s", report.total_duration.as_secs_f64());
println!(
" Checks: {} passed, {} failed",
style(report.checks.len() - report.failed_checks().len()).green(),
style(report.failed_checks().len()).red()
);
let failed = report.failed_checks();
if !failed.is_empty() {
println!(" {}:", style("Failed checks").red());
for check in failed {
println!(" • {}", style(check.check_type.display_name()).red());
}
}
println!();
}
async fn print_audit_section(limit: usize) -> Result<()> {
println!("{}", style("🔒 Bypass Audit Log").bold());
println!("{}", "=".repeat(60));
println!();
let config = BypassConfig::load_or_default().await?;
let manager = BypassManager::new(&config)?;
let entries = manager.get_audit_log(limit).await?;
if entries.is_empty() {
println!("{}", style("No bypass entries found.").dim());
} else {
println!("Showing last {} entries:\n", entries.len());
for (i, entry) in entries.iter().enumerate() {
print_audit_entry(i + 1, entry);
}
}
Ok(())
}
fn print_audit_entry(index: usize, entry: &BypassLogEntry) {
let status = if entry.successful {
style("✓ SUCCESS").green()
} else {
style("✗ FAILED").red()
};
println!("{}. {}", index, status);
println!(" Stage: {}", style(entry.stage.display_name()).cyan());
println!(" User: {}", style(&entry.user).yellow());
println!(
" Time: {}",
style(entry.timestamp.format("%Y-%m-%d %H:%M:%S UTC")).dim()
);
println!(" Reason: {}", entry.reason);
println!();
}
fn get_reports_dir() -> Result<PathBuf> {
let config_dir = crate::config::Config::config_dir_path()?;
Ok(config_dir.join("safety-reports"))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::safety::{CheckType, PipelineStage, report::CheckResult};
use std::time::Duration;
#[test]
fn test_print_report_summary_passed() {
let mut report = SafetyReport::new(PipelineStage::PreCommit);
report.passed = true;
report.total_duration = Duration::from_secs(5);
let check = CheckResult::new(CheckType::Format);
report.add_check(check);
print_report_summary(1, &report);
}
#[test]
fn test_print_report_summary_failed() {
let mut report = SafetyReport::new(PipelineStage::PreCommit);
let mut check = CheckResult::new(CheckType::Clippy);
check.add_error("Test error");
report.add_check(check);
print_report_summary(1, &report);
}
}