use colored::Colorize;
use comfy_table::{ContentArrangement, Table};
use super::models::{
AdminStorageHealthBlock, AgentRow, ApprovalsSummary, BudgetRow, DeploymentOverview, RuntimeHealth, StatusSnapshot,
};
use crate::output::OutputFormat;
pub fn format_deployment_overview(overview: &DeploymentOverview) -> String {
let separator = "─────────────────────────────────────";
let mut out = String::new();
out.push_str("Agent Assembly Status\n");
out.push_str(separator);
out.push('\n');
if overview.health == "unreachable" {
out.push_str(&format!(" Gateway: {}\n", overview.gateway_url));
out.push_str(&format!(" Health: {}\n", "✗ unreachable".red()));
} else {
let storage_detail = overview
.storage_path
.as_deref()
.or(overview.database_url_redacted.as_deref());
let storage_line = match storage_detail {
Some(detail) => format!("{} ({detail})", overview.storage_backend),
None => overview.storage_backend.clone(),
};
out.push_str(&format!(" Mode: {}\n", overview.mode));
out.push_str(&format!(" Gateway: {}\n", overview.gateway_url));
out.push_str(&format!(" Storage: {storage_line}\n"));
out.push_str(&format!(" Version: {}\n", overview.version));
out.push_str(&format!(" Uptime: {}\n", format_duration(overview.uptime_secs)));
out.push_str(&format!(" Health: {}\n", "✓ ok".green()));
}
out.push_str(separator);
out.push('\n');
out
}
pub fn render_deployment_overview(overview: &DeploymentOverview) {
println!("{}", format_deployment_overview(overview));
}
pub fn format_storage_health(block: &AdminStorageHealthBlock) -> String {
let mut out = String::new();
out.push_str("STORAGE\n");
out.push_str("───────\n");
out.push_str(&format!(" Backend: {}\n", block.backend));
if let Some(path) = block.path.as_deref() {
out.push_str(&format!(" Path: {path}\n"));
}
if let Some(url) = block.database_url.as_deref() {
out.push_str(&format!(" DB: {url}\n"));
}
let indicator = match block.health.as_str() {
"ok" => "✓ ok".green().to_string(),
"degraded" => "⚠ degraded".yellow().to_string(),
_ => "✗ unavailable".red().to_string(),
};
out.push_str(&format!(" DB Health: {} ({}ms)\n", indicator, block.latency_ms));
out.push_str(&format!(
" Rows: audit_events: {} hot\n",
format_count(block.row_counts.audit_events_hot)
));
out.push_str(&format!(
" agents: {} | policies: {}\n",
format_count(block.row_counts.agents),
format_count(block.row_counts.policy_versions)
));
if let Some(ts) = block.timescaledb.as_ref() {
let ts_indicator = "✓ active".green();
out.push_str(&format!(
" TimescaleDB: {} ({}/{} chunks compressed, {:.1}× ratio)\n",
ts_indicator, ts.compressed_chunks, ts.total_chunks, ts.compression_ratio,
));
}
out
}
pub fn render_storage_health(block: &AdminStorageHealthBlock) {
println!("{}", format_storage_health(block));
}
fn format_count(n: u64) -> String {
let s = n.to_string();
let bytes = s.as_bytes();
let mut out = String::with_capacity(bytes.len() + bytes.len() / 3);
for (i, b) in bytes.iter().enumerate() {
if i > 0 && (bytes.len() - i) % 3 == 0 {
out.push(',');
}
out.push(*b as char);
}
out
}
pub fn render_runtime_health(health: &RuntimeHealth) {
println!("RUNTIME HEALTH");
println!("──────────────");
let indicator = if health.reachable { "✓" } else { "✗" };
println!(" API: {indicator} {}", health.status);
println!(" Uptime: {}", format_duration(health.uptime_secs));
println!(" Connections: {}", health.active_connections);
println!(" Lag: {} ms", health.pipeline_lag_ms);
println!();
}
fn format_duration(secs: u64) -> String {
let hours = secs / 3600;
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
if hours > 0 {
format!("{hours}h {minutes}m {seconds}s")
} else if minutes > 0 {
format!("{minutes}m {seconds}s")
} else {
format!("{seconds}s")
}
}
pub fn render_agents_table(agents: &[AgentRow]) {
println!("ACTIVE AGENTS");
println!("─────────────");
if agents.is_empty() {
println!(" (no agents registered)");
println!();
return;
}
let mut table = Table::new();
table.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(vec![
"AGENT_ID",
"NAME",
"STATUS",
"FRAMEWORK",
"SESSIONS",
"LAST_EVENT",
"VIOLATIONS_TODAY",
"LAYER",
]);
for agent in agents {
let status_icon = match agent.status.as_str() {
"Running" => "●",
"Idle" => "○",
"Suspended" => "⚠",
_ => "?",
};
table.add_row(vec![
&agent.id,
&agent.name,
&format!("{status_icon} {}", agent.status),
&agent.framework,
&agent.sessions.to_string(),
&agent.last_event,
&agent.violations_today.to_string(),
&agent.layer,
]);
}
println!("{table}");
println!();
}
pub fn render_approvals_summary(summary: &ApprovalsSummary) {
println!("PENDING APPROVALS");
println!("─────────────────");
println!(" Count: {}", summary.pending_count);
if let Some(ref age) = summary.oldest_pending_age {
println!(" Oldest: {age} ago");
}
println!();
}
pub fn format_bar_chart(percentage: u32) -> String {
let pct = percentage.min(100);
let filled = (pct as usize * 20) / 100;
let empty = 20 - filled;
format!("{}{} {:>3}%", "█".repeat(filled), "░".repeat(empty), pct,)
}
fn colorize_bar(bar: &str, percentage: u32) -> String {
if percentage > 80 {
bar.red().to_string()
} else if percentage >= 50 {
bar.yellow().to_string()
} else {
bar.green().to_string()
}
}
fn render_budget_line(label: &str, spend: &str, limit: Option<&str>) {
match limit {
Some(lim) => {
let spend_f: f64 = spend.parse().unwrap_or(0.0);
let limit_f: f64 = lim.parse().unwrap_or(1.0);
let pct = if limit_f > 0.0 {
((spend_f / limit_f) * 100.0).round() as u32
} else {
0
};
let bar = format_bar_chart(pct);
let colored_bar = colorize_bar(&bar, pct);
println!(" {label}: ${spend} / ${lim} {colored_bar}");
}
None => {
println!(" {label}: ${spend} (no limit set)");
}
}
}
pub fn render_budget_table(budget: &BudgetRow) {
println!("BUDGET STATUS");
println!("─────────────");
render_budget_line(
"Daily spend ",
&budget.daily_spend_usd,
budget.daily_limit_usd.as_deref(),
);
if let Some(ref monthly) = budget.monthly_spend_usd {
render_budget_line("Monthly spend", monthly, budget.monthly_limit_usd.as_deref());
}
println!(" Date: {}", budget.date);
if budget.per_agent.is_empty() {
println!(" (no per-agent data)");
} else {
println!();
println!("PER-AGENT SPEND (today)");
println!("───────────────────────");
let mut table = Table::new();
table.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(vec!["AGENT_ID", "DAILY_SPEND"]);
let mut sorted = budget.per_agent.clone();
sorted.sort_by(|a, b| {
let a_val: f64 = a.daily_spend_usd.parse().unwrap_or(0.0);
let b_val: f64 = b.daily_spend_usd.parse().unwrap_or(0.0);
b_val.partial_cmp(&a_val).unwrap_or(std::cmp::Ordering::Equal)
});
for entry in &sorted {
table.add_row(vec![&entry.agent_id, &format!("${}", entry.daily_spend_usd)]);
}
println!("{table}");
}
println!();
}
pub fn render_status_json(snapshot: &StatusSnapshot) {
match serde_json::to_string_pretty(snapshot) {
Ok(json) => println!("{json}"),
Err(e) => eprintln!("error serializing status to JSON: {e}"),
}
}
pub fn render_all(snapshot: &StatusSnapshot, format: OutputFormat) {
match format {
OutputFormat::Json => render_status_json(snapshot),
OutputFormat::Yaml => match serde_yaml::to_string(snapshot) {
Ok(yaml) => print!("{yaml}"),
Err(e) => eprintln!("error serializing status to YAML: {e}"),
},
OutputFormat::Table => {
render_deployment_overview(&snapshot.deployment);
if let Some(storage_health) = snapshot.storage_health.as_ref() {
render_storage_health(storage_health);
}
render_runtime_health(&snapshot.runtime);
render_agents_table(&snapshot.agents);
render_approvals_summary(&snapshot.approvals);
render_budget_table(&snapshot.budget);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' && chars.peek() == Some(&'[') {
chars.next();
for tail in chars.by_ref() {
if tail == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
}
fn local_sqlite_overview() -> DeploymentOverview {
DeploymentOverview {
mode: "local".to_string(),
gateway_url: "http://localhost:7391".to_string(),
storage_backend: "sqlite".to_string(),
storage_path: Some("~/.aasm/local.db".to_string()),
database_url_redacted: None,
version: "0.0.1".to_string(),
uptime_secs: 8133,
health: "ok".to_string(),
}
}
fn sqlite_storage_block() -> AdminStorageHealthBlock {
use super::super::models::AdminRowCountsBlock;
AdminStorageHealthBlock {
backend: "sqlite".into(),
path: Some("~/.aasm/local.db".into()),
database_url: None,
health: "ok".into(),
latency_ms: 1,
row_counts: AdminRowCountsBlock {
audit_events_hot: 47,
agents: 2,
policy_versions: 1,
},
timescaledb: None,
}
}
fn postgres_timescale_storage_block() -> AdminStorageHealthBlock {
use super::super::models::{AdminRowCountsBlock, AdminTimescaleDbBlock};
AdminStorageHealthBlock {
backend: "postgres".into(),
path: None,
database_url: Some("postgresql://aasm-user:***@db.internal:5432/aasm".into()),
health: "ok".into(),
latency_ms: 3,
row_counts: AdminRowCountsBlock {
audit_events_hot: 14_293,
agents: 8,
policy_versions: 3,
},
timescaledb: Some(AdminTimescaleDbBlock {
enabled: true,
total_chunks: 12,
compressed_chunks: 8,
compression_ratio: 11.4,
}),
}
}
#[test]
fn format_storage_health_renders_sqlite_block_without_timescaledb_line() {
let rendered = strip_ansi(&format_storage_health(&sqlite_storage_block()));
assert!(rendered.starts_with("STORAGE\n"));
assert!(rendered.contains(" Backend: sqlite\n"));
assert!(rendered.contains(" Path: ~/.aasm/local.db\n"));
assert!(rendered.contains(" DB Health: ✓ ok (1ms)\n"));
assert!(rendered.contains(" Rows: audit_events: 47 hot\n"));
assert!(rendered.contains(" agents: 2 | policies: 1\n"));
assert!(
!rendered.contains("TimescaleDB"),
"sqlite block must not render TimescaleDB line"
);
assert!(
!rendered.contains("DB:"),
"sqlite block must not render DB: (postgres-only) line"
);
}
#[test]
fn format_storage_health_renders_postgres_block_with_redacted_url_and_timescaledb() {
let rendered = strip_ansi(&format_storage_health(&postgres_timescale_storage_block()));
assert!(rendered.contains(" Backend: postgres\n"));
assert!(rendered.contains(" DB: postgresql://aasm-user:***@db.internal:5432/aasm\n"));
assert!(!rendered.contains("secret"));
assert!(rendered.contains(" DB Health: ✓ ok (3ms)\n"));
assert!(rendered.contains(" Rows: audit_events: 14,293 hot\n"));
assert!(rendered.contains(" TimescaleDB: ✓ active (8/12 chunks compressed, 11.4× ratio)\n"));
assert!(!rendered.contains("Path:"));
}
#[test]
fn format_storage_health_renders_unavailable_indicator() {
let mut block = sqlite_storage_block();
block.health = "unavailable".into();
block.latency_ms = 0;
let rendered = strip_ansi(&format_storage_health(&block));
assert!(rendered.contains(" DB Health: ✗ unavailable (0ms)\n"));
}
#[test]
fn format_count_inserts_thousands_separators() {
assert_eq!(format_count(0), "0");
assert_eq!(format_count(7), "7");
assert_eq!(format_count(1_000), "1,000");
assert_eq!(format_count(14_293), "14,293");
assert_eq!(format_count(1_234_567), "1,234,567");
}
#[test]
fn format_deployment_overview_shows_unreachable_indicator_when_health_is_unreachable() {
let overview = DeploymentOverview {
mode: "unknown".to_string(),
gateway_url: "http://localhost:7391".to_string(),
storage_backend: "unknown".to_string(),
storage_path: None,
database_url_redacted: None,
version: String::new(),
uptime_secs: 0,
health: "unreachable".to_string(),
};
let rendered = strip_ansi(&format_deployment_overview(&overview));
assert!(rendered.contains(" Gateway: http://localhost:7391\n"));
assert!(rendered.contains(" Health: ✗ unreachable\n"));
assert!(!rendered.contains("Mode:"));
assert!(!rendered.contains("Storage:"));
assert!(!rendered.contains("Version:"));
assert!(!rendered.contains("Uptime:"));
}
#[test]
fn format_deployment_overview_shows_redacted_db_url_for_remote_postgres() {
let overview = DeploymentOverview {
mode: "remote".to_string(),
gateway_url: "https://cp.company.internal:7391".to_string(),
storage_backend: "postgres".to_string(),
storage_path: None,
database_url_redacted: Some("postgresql://aasm:***@aasm-db:5432/aasm".to_string()),
version: "0.0.1".to_string(),
uptime_secs: 1_234_567,
health: "ok".to_string(),
};
let rendered = strip_ansi(&format_deployment_overview(&overview));
assert!(rendered.contains(" Mode: remote\n"));
assert!(rendered.contains(" Gateway: https://cp.company.internal:7391\n"));
assert!(rendered.contains(" Storage: postgres (postgresql://aasm:***@aasm-db:5432/aasm)\n"));
assert!(!rendered.contains("secret"));
}
#[test]
fn format_deployment_overview_renders_local_sqlite_header() {
let rendered = strip_ansi(&format_deployment_overview(&local_sqlite_overview()));
assert!(rendered.starts_with("Agent Assembly Status\n"));
assert!(rendered.contains(" Mode: local\n"));
assert!(rendered.contains(" Gateway: http://localhost:7391\n"));
assert!(rendered.contains(" Storage: sqlite (~/.aasm/local.db)\n"));
assert!(rendered.contains(" Version: 0.0.1\n"));
assert!(rendered.contains(" Uptime: 2h 15m 33s\n"));
assert!(rendered.contains(" Health: ✓ ok\n"));
}
#[test]
fn bar_chart_at_zero_percent() {
let bar = format_bar_chart(0);
assert_eq!(bar, "░░░░░░░░░░░░░░░░░░░░ 0%");
}
#[test]
fn bar_chart_at_fifty_percent() {
let bar = format_bar_chart(50);
assert_eq!(bar, "██████████░░░░░░░░░░ 50%");
}
#[test]
fn bar_chart_at_hundred_percent() {
let bar = format_bar_chart(100);
assert_eq!(bar, "████████████████████ 100%");
}
#[test]
fn bar_chart_clamps_above_hundred() {
let bar = format_bar_chart(150);
assert_eq!(bar, "████████████████████ 100%");
}
#[test]
fn colorize_bar_green_below_50() {
let bar = format_bar_chart(30);
let colored = colorize_bar(&bar, 30);
assert!(colored.contains("30%"));
}
#[test]
fn colorize_bar_yellow_at_50() {
let bar = format_bar_chart(50);
let colored = colorize_bar(&bar, 50);
assert!(colored.contains("50%"));
}
#[test]
fn colorize_bar_yellow_at_80() {
let bar = format_bar_chart(80);
let colored = colorize_bar(&bar, 80);
assert!(colored.contains("80%"));
}
#[test]
fn colorize_bar_red_above_80() {
let bar = format_bar_chart(95);
let colored = colorize_bar(&bar, 95);
assert!(colored.contains("95%"));
}
#[test]
fn per_agent_sorted_by_spend_descending() {
use super::super::models::AgentCostEntry;
let mut entries = [
AgentCostEntry {
agent_id: "low".to_string(),
daily_spend_usd: "1.00".to_string(),
},
AgentCostEntry {
agent_id: "high".to_string(),
daily_spend_usd: "10.00".to_string(),
},
AgentCostEntry {
agent_id: "mid".to_string(),
daily_spend_usd: "5.00".to_string(),
},
];
entries.sort_by(|a, b| {
let a_val: f64 = a.daily_spend_usd.parse().unwrap_or(0.0);
let b_val: f64 = b.daily_spend_usd.parse().unwrap_or(0.0);
b_val.partial_cmp(&a_val).unwrap_or(std::cmp::Ordering::Equal)
});
assert_eq!(entries[0].agent_id, "high");
assert_eq!(entries[1].agent_id, "mid");
assert_eq!(entries[2].agent_id, "low");
}
#[test]
fn render_status_json_contains_all_keys() {
use super::super::models::DeploymentOverview;
let snapshot = StatusSnapshot {
deployment: DeploymentOverview {
mode: "local".to_string(),
gateway_url: "http://localhost:7391".to_string(),
storage_backend: "sqlite".to_string(),
storage_path: None,
database_url_redacted: None,
version: "0.0.1".to_string(),
uptime_secs: 120,
health: "ok".to_string(),
},
runtime: RuntimeHealth {
reachable: true,
status: "ok".to_string(),
uptime_secs: 120,
active_connections: 3,
pipeline_lag_ms: 0,
},
agents: vec![],
approvals: ApprovalsSummary {
pending_count: 0,
oldest_pending_age: None,
},
budget: BudgetRow {
daily_spend_usd: "0.00".to_string(),
monthly_spend_usd: None,
daily_limit_usd: None,
monthly_limit_usd: None,
date: "2026-04-30".to_string(),
per_agent: vec![],
},
storage_health: None,
};
let json = serde_json::to_string_pretty(&snapshot).unwrap();
assert!(json.contains("\"deployment\""));
assert!(json.contains("\"gateway_url\""));
assert!(json.contains("\"storage_backend\""));
assert!(json.contains("\"runtime\""));
assert!(json.contains("\"agents\""));
assert!(json.contains("\"approvals\""));
assert!(json.contains("\"budget\""));
assert!(json.contains("\"uptime_secs\""));
assert!(json.contains("\"active_connections\""));
assert!(json.contains("\"pipeline_lag_ms\""));
}
#[test]
fn format_duration_seconds_only() {
assert_eq!(format_duration(45), "45s");
}
#[test]
fn format_duration_minutes_and_seconds() {
assert_eq!(format_duration(125), "2m 5s");
}
#[test]
fn format_duration_hours_minutes_seconds() {
assert_eq!(format_duration(3661), "1h 1m 1s");
}
#[test]
fn format_duration_zero() {
assert_eq!(format_duration(0), "0s");
}
}