coven/sync/
status.rs

1/// Sync status derived from device heads.
2///
3/// After each pull, the caller has the full list of `DeviceHead`s. This
4/// module provides a type to summarize that into a human-readable status
5/// for the UI: when we last synced, and what other devices are doing.
6use super::storage::DeviceHead;
7
8/// Activity summary for a single remote device.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DeviceActivity {
11    pub device_id: String,
12    pub last_seq: u64,
13    /// RFC 3339 timestamp of the device's last sync. None if the head
14    /// was written before timestamps were added.
15    pub last_sync: Option<String>,
16}
17
18/// Sync status derived from the heads fetched during a pull.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct SyncStatus {
21    /// When this device last synced (RFC 3339). None if never synced.
22    pub last_sync_time: Option<String>,
23    /// Activity of other devices.
24    pub other_devices: Vec<DeviceActivity>,
25}
26
27/// Build a `SyncStatus` from a list of device heads.
28///
29/// `our_device_id` identifies the local device so its head can be
30/// separated from the "other devices" list.
31/// `local_sync_time` is the RFC 3339 timestamp of when *we* last
32/// completed a sync cycle (tracked locally, not from the heads).
33pub fn build_sync_status(
34    heads: &[DeviceHead],
35    our_device_id: &str,
36    local_sync_time: Option<&str>,
37) -> SyncStatus {
38    let mut other_devices = Vec::new();
39
40    for head in heads {
41        if head.device_id == our_device_id {
42            continue;
43        }
44
45        other_devices.push(DeviceActivity {
46            device_id: head.device_id.clone(),
47            last_seq: head.seq,
48            last_sync: head.last_sync.clone(),
49        });
50    }
51
52    SyncStatus {
53        last_sync_time: local_sync_time.map(|s| s.to_string()),
54        other_devices,
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn build_status_with_no_heads() {
64        let status = build_sync_status(&[], "dev-1", None);
65        assert_eq!(status.last_sync_time, None);
66        assert!(status.other_devices.is_empty());
67    }
68
69    #[test]
70    fn build_status_excludes_own_device() {
71        let heads = vec![
72            DeviceHead {
73                device_id: "dev-1".into(),
74                seq: 5,
75                snapshot_seq: None,
76                last_sync: Some("2026-02-10T12:00:00Z".into()),
77            },
78            DeviceHead {
79                device_id: "dev-2".into(),
80                seq: 3,
81                snapshot_seq: None,
82                last_sync: Some("2026-02-10T11:55:00Z".into()),
83            },
84        ];
85
86        let status = build_sync_status(&heads, "dev-1", Some("2026-02-10T12:00:00Z"));
87        assert_eq!(
88            status.last_sync_time,
89            Some("2026-02-10T12:00:00Z".to_string())
90        );
91        assert_eq!(status.other_devices.len(), 1);
92        assert_eq!(status.other_devices[0].device_id, "dev-2");
93        assert_eq!(status.other_devices[0].last_seq, 3);
94    }
95
96    #[test]
97    fn build_status_with_no_timestamps() {
98        let heads = vec![DeviceHead {
99            device_id: "dev-2".into(),
100            seq: 10,
101            snapshot_seq: None,
102            last_sync: None,
103        }];
104
105        let status = build_sync_status(&heads, "dev-1", None);
106        assert_eq!(status.last_sync_time, None);
107        assert_eq!(status.other_devices.len(), 1);
108        assert_eq!(status.other_devices[0].last_sync, None);
109    }
110}