Compare commits

...

1 commit

Author SHA1 Message Date
c9bb2cbe64 feat(heph-tui): show sync age in seconds under a minute
All checks were successful
Build / validate (push) Successful in 6m28s
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) <noreply@anthropic.com>
2026-06-06 11:24:09 -07:00
3 changed files with 13 additions and 12 deletions

View file

@ -30,13 +30,15 @@ pub fn now_ms() -> i64 {
Local::now().timestamp_millis() Local::now().timestamp_millis()
} }
/// A compact "how long ago" for the sync indicator: `just now` under a minute, /// A compact "how long ago" for the sync indicator: `Ns` under a minute, then
/// then `Nm` / `Nh` / `Nd`. Clamped at zero so a little clock skew never shows a /// `Nm` / `Nh` / `Nd`. Second-granularity under a minute makes the chip a visible
/// negative age. /// 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 { pub fn fmt_age(now_ms: i64, then_ms: i64) -> String {
let secs = (now_ms - then_ms).max(0) / 1000; let secs = (now_ms - then_ms).max(0) / 1000;
if secs < 60 { if secs < 60 {
"just now".into() format!("{secs}s")
} else if secs < 3_600 { } else if secs < 3_600 {
format!("{}m", secs / 60) format!("{}m", secs / 60)
} else if secs < 86_400 { } else if secs < 86_400 {
@ -126,13 +128,14 @@ mod tests {
#[test] #[test]
fn age_is_compact_and_clamped() { fn age_is_compact_and_clamped() {
let now = 1_000_000_000_000; let now = 1_000_000_000_000;
assert_eq!(fmt_age(now, now), "just now"); assert_eq!(fmt_age(now, now), "0s");
assert_eq!(fmt_age(now, now - 30_000), "just now"); 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 - 5 * 60_000), "5m");
assert_eq!(fmt_age(now, now - 3 * 3_600_000), "3h"); assert_eq!(fmt_age(now, now - 3 * 3_600_000), "3h");
assert_eq!(fmt_age(now, now - 2 * 86_400_000), "2d"); assert_eq!(fmt_age(now, now - 2 * 86_400_000), "2d");
// Clock skew (then in the future) never shows a negative age. // 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] #[test]

View file

@ -661,10 +661,7 @@ mod tests {
last_success_ms: Some(NOW), last_success_ms: Some(NOW),
..Default::default() ..Default::default()
}; };
assert_eq!( assert_eq!(render(&spoke(h.clone(), 1), NOW), "⟳ 0s ⚠ 1 conflict");
render(&spoke(h.clone(), 1), NOW), assert_eq!(render(&spoke(h, 3), NOW), "⟳ 0s ⚠ 3 conflicts");
"⟳ just now ⚠ 1 conflict"
);
assert_eq!(render(&spoke(h, 3), NOW), "⟳ just now ⚠ 3 conflicts");
} }
} }

View file

@ -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.