#![allow(clippy::manual_is_multiple_of)]
use super::helpers_time::*;
use crate::core::types;
use crate::tripwire::eventlog;
use std::path::Path;
pub(crate) fn cmd_history(
state_dir: &Path,
machine_filter: Option<&str>,
limit: usize,
json: bool,
since: Option<&str>,
) -> Result<(), String> {
let mut all_events = load_machine_events(state_dir, machine_filter)?;
all_events.sort_by(|a, b| b.ts.cmp(&a.ts));
if let Some(since_str) = since {
let cutoff_str = compute_cutoff_iso8601(since_str)?;
all_events.retain(|e| e.ts.as_str() >= cutoff_str.as_str());
}
let apply_events: Vec<&types::TimestampedEvent> = all_events
.iter()
.filter(|e| {
matches!(
e.event,
types::ProvenanceEvent::ApplyStarted { .. }
| types::ProvenanceEvent::ApplyCompleted { .. }
)
})
.take(limit)
.collect();
if json {
output_history_json(&all_events, &apply_events, since, limit)?;
} else if apply_events.is_empty() {
println!("No apply history found. Run `forjar apply` first.");
} else {
print_apply_events(&apply_events);
}
Ok(())
}
fn machines_with_event_logs(state_dir: &Path, filter: Option<&str>) -> Vec<String> {
let Ok(entries) = std::fs::read_dir(state_dir) else {
return Vec::new();
};
entries
.flatten()
.filter(|e| e.path().is_dir())
.map(|e| e.file_name().to_string_lossy().to_string())
.filter(|name| filter.is_none_or(|f| name == f))
.filter(|name| eventlog::event_log_path(state_dir, name).exists())
.collect()
}
fn parse_event_log(path: &Path) -> Result<Vec<types::TimestampedEvent>, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
Ok(content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str::<types::TimestampedEvent>(l).ok())
.collect())
}
pub(crate) fn load_machine_events(
state_dir: &Path,
machine_filter: Option<&str>,
) -> Result<Vec<types::TimestampedEvent>, String> {
if !state_dir.exists() {
return Err(format!(
"cannot read state dir {}: not found",
state_dir.display()
));
}
let mut all = Vec::new();
for name in machines_with_event_logs(state_dir, machine_filter) {
all.extend(parse_event_log(&eventlog::event_log_path(
state_dir, &name,
))?);
}
Ok(all)
}
fn epoch_secs_to_iso8601(d: u64) -> String {
let secs_in_day = 86400u64;
let mut days = d / secs_in_day;
let rem = d % secs_in_day;
let hh = rem / 3600;
let mm = (rem % 3600) / 60;
let ss = rem % 60;
let mut y = 1970i64;
loop {
let dy = if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {
366
} else {
365
};
if days < dy {
break;
}
days -= dy;
y += 1;
}
let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
let mdays = [
31,
if leap { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut mo = 0usize;
while mo < 12 && days >= mdays[mo] {
days -= mdays[mo];
mo += 1;
}
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
y,
mo + 1,
days + 1,
hh,
mm,
ss
)
}
fn compute_cutoff_iso8601(since_str: &str) -> Result<String, String> {
let secs = parse_duration_secs(since_str)?;
let now = std::time::SystemTime::now();
let cutoff = now
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| format!("time error: {e}"))?
.as_secs()
.saturating_sub(secs);
Ok(epoch_secs_to_iso8601(cutoff))
}
fn output_history_json(
all_events: &[types::TimestampedEvent],
apply_events: &[&types::TimestampedEvent],
since: Option<&str>,
limit: usize,
) -> Result<(), String> {
let total_events = all_events.len();
let started = apply_events
.iter()
.filter(|e| matches!(e.event, types::ProvenanceEvent::ApplyStarted { .. }))
.count();
let completed = apply_events
.iter()
.filter(|e| matches!(e.event, types::ProvenanceEvent::ApplyCompleted { .. }))
.count();
let output = serde_json::json!({
"total_events": total_events,
"apply_started": started,
"apply_completed": completed,
"since": since,
"limit": limit,
"events": apply_events,
});
println!(
"{}",
serde_json::to_string_pretty(&output).map_err(|e| format!("JSON error: {e}"))?
);
Ok(())
}
fn print_apply_events(apply_events: &[&types::TimestampedEvent]) {
for event in apply_events {
match &event.event {
types::ProvenanceEvent::ApplyStarted {
machine, run_id, ..
} => {
println!("{} started {} ({})", event.ts, machine, run_id);
}
types::ProvenanceEvent::ApplyCompleted {
machine,
run_id,
resources_converged,
resources_unchanged,
resources_failed,
total_seconds,
} => {
println!(
"{} complete {} ({}) — {} converged, {} unchanged, {} failed ({:.1}s)",
event.ts,
machine,
run_id,
resources_converged,
resources_unchanged,
resources_failed,
total_seconds
);
}
_ => {}
}
}
}