Fix relative timestamps stuck at "just now"
jiff's Timestamp::since returns a Span balanced to seconds, so
get_hours() and get_minutes() always read 0. Compute the diff from
duration_since().as_secs() and extract a pure relative_label helper for
boundary testing.

Assisted-by: Claude Opus 4.7 via Claude Code
change zxuusnzsosyvptnllmklvvzyzrntzwls
commit 15be0d1cbf99a4bd53b5759efc1c6a9b8e8a9e2f
author Alpha Chen <alpha@kejadlen.dev>
date
parent pytsnozz
diff --git a/src/quire/web/format.rs b/src/quire/web/format.rs
index e191eba..9405dcb 100644
--- a/src/quire/web/format.rs
+++ b/src/quire/web/format.rs
@@ -1,33 +1,40 @@
 //! Formatting helpers for the web view.
 
+use jiff::Timestamp;
+
 /// Relative time display (e.g. "3m ago", "2h ago", ISO if older than 24h).
 pub fn format_timestamp_relative(ms: i64) -> String {
-    use jiff::Timestamp;
-    match Timestamp::from_millisecond(ms) {
-        Ok(ts) => {
-            let now = Timestamp::now();
-            let span = now.since(ts).unwrap_or_else(|_| jiff::Span::new());
-            let hours = span.get_hours().abs();
-            let minutes = span.get_minutes().abs();
-            if hours < 1 {
-                if minutes < 1 {
-                    "just now".to_string()
-                } else {
-                    format!("{minutes}m ago")
-                }
-            } else if hours < 24 {
-                format!("{hours}h ago")
-            } else {
-                ts.to_string()
-            }
-        }
-        Err(_) => format!("{ms}ms"),
+    let Ok(ts) = Timestamp::from_millisecond(ms) else {
+        return format!("{ms}ms");
+    };
+    let diff_secs = Timestamp::now().duration_since(ts).as_secs().max(0);
+    match relative_label(diff_secs) {
+        Some(label) => label,
+        None => ts.to_string(),
+    }
+}
+
+/// Format a positive seconds-ago value as "Nm ago" / "Nh ago".
+///
+/// Returns `None` when the diff is at least 24 hours — caller renders
+/// an absolute timestamp instead.
+fn relative_label(secs: i64) -> Option<String> {
+    let mins = secs / 60;
+    let hours = mins / 60;
+    if hours >= 24 {
+        return None;
+    }
+    if mins == 0 {
+        return Some("just now".to_string());
+    }
+    if hours == 0 {
+        return Some(format!("{mins}m ago"));
     }
+    Some(format!("{hours}h ago"))
 }
 
 /// ISO timestamp for title attributes.
 pub fn format_timestamp_iso(ms: i64) -> String {
-    use jiff::Timestamp;
     Timestamp::from_millisecond(ms)
         .map(|ts| ts.to_string())
         .unwrap_or_else(|_| format!("{ms}ms"))
@@ -36,21 +43,18 @@ pub fn format_timestamp_iso(ms: i64) -> String {
 /// Duration between optional start/end millisecond timestamps.
 pub fn format_duration(start: Option<i64>, end: Option<i64>) -> String {
     match (start, end) {
-        (Some(s), Some(e)) => {
-            let ms = e - s;
-            if ms < 1000 {
-                format!("{ms}ms")
-            } else {
-                format!("{}s", ms / 1000)
-            }
-        }
+        (Some(s), Some(e)) => format_ms_duration(e - s),
         _ => "—".to_string(),
     }
 }
 
 /// Duration between exact start/end millisecond timestamps.
 pub fn format_duration_exact(start: i64, end: i64) -> String {
-    let ms = end - start;
+    format_ms_duration(end - start)
+}
+
+fn format_ms_duration(ms: i64) -> String {
+    let ms = ms.max(0);
     if ms < 1000 {
         format!("{ms}ms")
     } else {
@@ -76,4 +80,33 @@ mod tests {
     fn format_duration_dash_when_missing() {
         assert_eq!(format_duration(None, None), "—");
     }
+
+    #[test]
+    fn format_duration_clamps_negative_to_zero() {
+        assert_eq!(format_duration(Some(500), Some(0)), "0ms");
+    }
+
+    #[test]
+    fn relative_label_just_now_under_a_minute() {
+        assert_eq!(relative_label(0).as_deref(), Some("just now"));
+        assert_eq!(relative_label(59).as_deref(), Some("just now"));
+    }
+
+    #[test]
+    fn relative_label_minutes() {
+        assert_eq!(relative_label(60).as_deref(), Some("1m ago"));
+        assert_eq!(relative_label(59 * 60 + 59).as_deref(), Some("59m ago"));
+    }
+
+    #[test]
+    fn relative_label_hours() {
+        assert_eq!(relative_label(60 * 60).as_deref(), Some("1h ago"));
+        assert_eq!(relative_label(23 * 60 * 60).as_deref(), Some("23h ago"));
+    }
+
+    #[test]
+    fn relative_label_returns_none_past_a_day() {
+        assert_eq!(relative_label(24 * 60 * 60), None);
+        assert_eq!(relative_label(72 * 60 * 60), None);
+    }
 }