From c9bb2cbe648b27789f24ae89a2fabe5282c54149 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 6 Jun 2026 11:24:09 -0700 Subject: [PATCH] feat(heph-tui): show sync age in seconds under a minute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The background sync loop runs every 30s, so the last-sync age never crossed the 60s 'just now' threshold — the chip always read 'just now', which also masked the first missed sync (age 30-60s looked identical to a fresh one). Show seconds under a minute ('⟳ 26s') so the chip is a visible heartbeat and a stalled sync surfaces ~30s sooner. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/heph-tui/src/fmt.rs | 17 ++++++++++------- crates/heph-tui/src/ui.rs | 7 ++----- docs/changelog.d/+sync-age-seconds.feature.md | 1 + 3 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 docs/changelog.d/+sync-age-seconds.feature.md diff --git a/crates/heph-tui/src/fmt.rs b/crates/heph-tui/src/fmt.rs index 2383a7e..8c49ac9 100644 --- a/crates/heph-tui/src/fmt.rs +++ b/crates/heph-tui/src/fmt.rs @@ -30,13 +30,15 @@ pub fn now_ms() -> i64 { Local::now().timestamp_millis() } -/// A compact "how long ago" for the sync indicator: `just now` under a minute, -/// then `Nm` / `Nh` / `Nd`. Clamped at zero so a little clock skew never shows a -/// negative age. +/// A compact "how long ago" for the sync indicator: `Ns` under a minute, then +/// `Nm` / `Nh` / `Nd`. Second-granularity under a minute makes the chip a visible +/// heartbeat (the sync loop runs every 30s) and surfaces a missed beat as the age +/// climbing, rather than hiding under a flat "just now". Clamped at zero so a +/// little clock skew never shows a negative age. pub fn fmt_age(now_ms: i64, then_ms: i64) -> String { let secs = (now_ms - then_ms).max(0) / 1000; if secs < 60 { - "just now".into() + format!("{secs}s") } else if secs < 3_600 { format!("{}m", secs / 60) } else if secs < 86_400 { @@ -126,13 +128,14 @@ mod tests { #[test] fn age_is_compact_and_clamped() { let now = 1_000_000_000_000; - assert_eq!(fmt_age(now, now), "just now"); - assert_eq!(fmt_age(now, now - 30_000), "just now"); + assert_eq!(fmt_age(now, now), "0s"); + assert_eq!(fmt_age(now, now - 30_000), "30s"); + assert_eq!(fmt_age(now, now - 59_000), "59s"); assert_eq!(fmt_age(now, now - 5 * 60_000), "5m"); assert_eq!(fmt_age(now, now - 3 * 3_600_000), "3h"); assert_eq!(fmt_age(now, now - 2 * 86_400_000), "2d"); // Clock skew (then in the future) never shows a negative age. - assert_eq!(fmt_age(now, now + 10_000), "just now"); + assert_eq!(fmt_age(now, now + 10_000), "0s"); } #[test] diff --git a/crates/heph-tui/src/ui.rs b/crates/heph-tui/src/ui.rs index b457818..6e15453 100644 --- a/crates/heph-tui/src/ui.rs +++ b/crates/heph-tui/src/ui.rs @@ -661,10 +661,7 @@ mod tests { last_success_ms: Some(NOW), ..Default::default() }; - assert_eq!( - render(&spoke(h.clone(), 1), NOW), - "⟳ just now ⚠ 1 conflict" - ); - assert_eq!(render(&spoke(h, 3), NOW), "⟳ just now ⚠ 3 conflicts"); + assert_eq!(render(&spoke(h.clone(), 1), NOW), "⟳ 0s ⚠ 1 conflict"); + assert_eq!(render(&spoke(h, 3), NOW), "⟳ 0s ⚠ 3 conflicts"); } } diff --git a/docs/changelog.d/+sync-age-seconds.feature.md b/docs/changelog.d/+sync-age-seconds.feature.md new file mode 100644 index 0000000..cf453c2 --- /dev/null +++ b/docs/changelog.d/+sync-age-seconds.feature.md @@ -0,0 +1 @@ +heph-tui's sync indicator now shows the last-sync age in seconds under a minute (`⟳ 26s`) instead of a flat `just now`, so the chip reads as a live heartbeat and a missed sync (the loop runs every 30s) shows up as the age climbing.