feat(cli): render uptime in human units

The UPTIME column printed raw seconds, so a server up for three hours
read as 10549s. Format the two largest non-zero units instead, dropping
the smaller one when it is zero: 49s, 12m 4s, 2h 55m, 6d 3h.

Days is the largest unit and the output never exceeds eight characters,
so the ten-wide column still holds and RESTARTS stays put.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9BWMXKKNpid7XM6oFbyYq
This commit is contained in:
2026-08-10 13:31:14 +02:00
co-authored by Claude Opus 5
parent 3dcda14ba5
commit 29df988d9d
+79 -4
View File
@@ -7,10 +7,7 @@ pub fn list_table(rows: &[ServerSummary]) -> String {
for r in rows {
let pid = r.pid.map(|p| p.to_string()).unwrap_or_else(|| "-".into());
let up = r
.uptime_secs
.map(|s| format!("{}s", s))
.unwrap_or_else(|| "-".into());
let up = r.uptime_secs.map(uptime).unwrap_or_else(|| "-".into());
out.push_str(&format!(
"{:<20}{:<12}{:<8}{:<8}{:<10}{}\n",
@@ -26,6 +23,28 @@ pub fn list_table(rows: &[ServerSummary]) -> String {
out
}
fn uptime(secs: u64) -> String {
const MINUTE: u64 = 60;
const HOUR: u64 = 60 * MINUTE;
const DAY: u64 = 24 * HOUR;
let (major, minor, major_unit, minor_unit) = if secs >= DAY {
(secs / DAY, secs % DAY / HOUR, "d", "h")
} else if secs >= HOUR {
(secs / HOUR, secs % HOUR / MINUTE, "h", "m")
} else if secs >= MINUTE {
(secs / MINUTE, secs % MINUTE, "m", "s")
} else {
return format!("{secs}s");
};
if minor == 0 {
return format!("{major}{major_unit}");
}
format!("{major}{major_unit} {minor}{minor_unit}")
}
pub(crate) fn wait_line(info: &WaitInfo) -> String {
format!(
" wait-for: {}\n {}s elapsed, timeout {}s\n",
@@ -55,6 +74,62 @@ mod tests {
assert!(out.contains("waiting"), "got: {out}");
}
#[test]
fn uptime_under_a_minute_is_bare_seconds() {
assert_eq!(uptime(0), "0s");
assert_eq!(uptime(49), "49s");
assert_eq!(uptime(59), "59s");
}
#[test]
fn uptime_under_an_hour_is_minutes_and_seconds() {
assert_eq!(uptime(60), "1m");
assert_eq!(uptime(724), "12m 4s");
assert_eq!(uptime(3_599), "59m 59s");
}
#[test]
fn uptime_under_a_day_is_hours_and_minutes() {
assert_eq!(uptime(3_600), "1h");
assert_eq!(uptime(10_549), "2h 55m");
assert_eq!(uptime(86_399), "23h 59m");
}
#[test]
fn uptime_of_a_day_or_more_is_days_and_hours() {
assert_eq!(uptime(86_400), "1d");
assert_eq!(uptime(529_200), "6d 3h");
assert_eq!(uptime(2_707_200), "31d 8h");
}
#[test]
fn uptime_drops_a_trailing_zero_unit() {
assert_eq!(uptime(120), "2m");
assert_eq!(uptime(7_200), "2h");
assert_eq!(uptime(518_400), "6d");
}
#[test]
fn list_renders_uptime_in_human_units() {
let rows = vec![ServerSummary {
name: "vestige".to_string(),
state: ServerState::Running,
pid: Some(4821),
port: 3928,
uptime_secs: Some(10_549),
restart_count: 0,
last_exit: None,
}];
let out = list_table(&rows);
assert!(out.contains("2h 55m"), "got: {out}");
assert!(
!out.contains("10549"),
"raw seconds must not leak; got: {out}"
);
}
#[test]
fn status_renders_the_wait_condition_and_elapsed() {
let info = WaitInfo {