use crate::tui::health::types::{PanelData, PanelState};
pub fn format_relative_time(ts: Option<&str>) -> String {
let Some(s) = ts else {
return "never".to_string();
};
let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(s) else {
return "never".to_string();
};
let now = chrono::Utc::now();
let delta = now.signed_duration_since(parsed.with_timezone(&chrono::Utc));
let secs = delta.num_seconds();
if secs < 60 {
return "just now".to_string();
}
let mins = secs / 60;
if mins < 60 {
return format!("{mins}m ago");
}
let hours = mins / 60;
if hours < 24 {
return format!("{hours}h ago");
}
let days = hours / 24;
format!("{days}d ago")
}
pub fn format_uptime(secs: u64) -> String {
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
}
pub fn format_bytes(bytes: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
let b = bytes as f64;
if b >= GB {
format!("{:.1}GB", b / GB)
} else if b >= MB {
format!("{:.1}MB", b / MB)
} else if b >= KB {
format!("{:.1}KB", b / KB)
} else {
format!("{bytes}B")
}
}
pub fn format_rss(mb: u64) -> String {
if mb >= 1024 {
format!("{:.1}GB", mb as f64 / 1024.0)
} else {
format!("{mb}MB")
}
}
pub fn format_count(n: u64) -> String {
if n >= 10_000 {
format!("{:.1}k", n as f64 / 1000.0)
} else {
n.to_string()
}
}
pub fn format_with_commas(n: u64) -> String {
let s = n.to_string();
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len() + s.len() / 3);
for (i, b) in bytes.iter().enumerate() {
if i > 0 && (bytes.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(*b as char);
}
out
}
pub fn ascii_bar(ratio: f64, width: usize) -> String {
let r = ratio.clamp(0.0, 1.0);
let filled = (r * width as f64).round() as usize;
let filled = filled.min(width);
let mut s = String::with_capacity(width * 3);
for _ in 0..filled {
s.push('█');
}
for _ in filled..width {
s.push('░');
}
s
}
pub fn search_panel_lines(state: &PanelState, base_url: &str) -> Vec<String> {
panel_lines(state, base_url, "SEARCH", |data| {
vec![
format!(
"Indexes: {} Chunks: {}",
data.count_a,
format_count(data.count_b)
),
format!("Disk: {}", format_bytes(data.disk_bytes)),
]
})
}
pub fn memory_panel_lines(state: &PanelState, base_url: &str) -> Vec<String> {
panel_lines(state, base_url, "MEMORY", |data| {
vec![
format!(
"Palaces: {} Vectors: {}",
data.count_a,
format_count(data.count_b)
),
format!(
"Drawers: {} KG: {}",
data.count_c,
format_count(data.count_d)
),
]
})
}
fn panel_lines(
state: &PanelState,
base_url: &str,
name: &str,
counts: impl Fn(&PanelData) -> Vec<String>,
) -> Vec<String> {
match state {
PanelState::Connecting => vec![format!("{name} [○] connecting to {base_url}…")],
PanelState::Offline { last_error } => vec![
format!("{name} [○] OFFLINE"),
format!("unreachable at {base_url}"),
format!("last error: {last_error}"),
"retrying every 5s…".to_string(),
String::new(),
"[S]start [X]stop".to_string(),
],
PanelState::Online(data) => {
let version = if data.version.is_empty() {
"?".to_string()
} else {
format!("v{}", data.version)
};
let mut lines = vec![
format!("{name} [●] {version}"),
format!(
"RSS: {} CPU: {:.0}% Uptime: {}",
format_rss(data.rss_mb),
data.cpu_pct,
format_uptime(data.uptime_secs),
),
];
lines.extend(counts(data));
lines.push(String::new());
lines.push("[S]start [X]stop".to_string());
lines
}
}
}