src/sync.rs
//! **The Sync view — its rows**, pure and host-tested; `pages/admin.rs`
//! renders them in the Admin → Sync pane.
//!
//! The rows are holger's, as one `AdminSnapshot` carries them: the twins
//! enrolled here (`twin.twins`, holger's `GrowService.Twins`) and every paired
//! downstream per repository (`downstreams`, holger's `GrowService.Downstreams`).
//! The state word — `ok`, `behind`, `stalled` or `silent` — is the SERVER's
//! (holger's `follow_point` rule); this module judges nothing. It only says the
//! server's numbers in words: the lag in entries, the last sync as a time ago
//! from the snapshot's own clock, and today's pulls.
#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
#[cfg(not(target_arch = "wasm32"))]
use crate::admin_queue::pb;
#[cfg(target_arch = "wasm32")]
use crate::grpc::pb;
use crate::admin::human;
use crate::util::Tone;
/// One line of the Sync view.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SyncRow {
/// `twin` | `downstream`.
pub kind: &'static str,
pub name: String,
pub repository: String,
pub lag: String,
/// The server's word, as it came.
pub state: String,
pub tone: Tone,
pub last_synced: String,
pub pulls_today: String,
}
/// Pure: the badge tone of the server's state word (a word it does not know
/// is shown as it came, untoned).
pub fn state_tone(state: &str) -> Tone {
match state {
"ok" => Tone::Good,
"behind" => Tone::Warn,
"stalled" | "silent" => Tone::Bad,
_ => Tone::Neutral,
}
}
/// Pure: `at_ms` as a time ago, read against the snapshot's `taken_ms`.
pub fn ago(taken_ms: i64, at_ms: i64) -> String {
if at_ms <= 0 {
return "never".into();
}
match (taken_ms - at_ms).max(0) / 1_000 {
s if s < 120 => format!("{s} s ago"),
s if s < 7_200 => format!("{} min ago", s / 60),
s if s < 172_800 => format!("{} h ago", s / 3_600),
s => format!("{} days ago", s / 86_400),
}
}
/// Pure: every twin and every downstream repository, in the server's order.
pub fn rows(s: &pb::AdminSnapshotReply) -> Vec<SyncRow> {
let taken = s.taken_unix_ms;
let twins = s.twin.iter().flat_map(|t| &t.twins).map(|t| SyncRow {
kind: "twin",
name: format!("{} {}", t.principal, t.address).trim().to_string(),
repository: "every repository".into(),
lag: format!("{} entries", t.lag_entries),
state: t.state.clone(),
tone: state_tone(&t.state),
last_synced: ago(taken, t.last_seen_unix_ms),
pulls_today: "—".into(),
});
let downstreams = s.downstreams.iter().flat_map(|d| {
let pulls = format!("{} ({})", d.requests_today, human(d.bytes_today));
let line = |repository: String, lag: u64, state: &str, at: i64| SyncRow {
kind: "downstream",
name: d.name.clone(),
repository,
lag: format!("{lag} entries"),
state: state.to_string(),
tone: state_tone(state),
last_synced: ago(taken, at),
pulls_today: pulls.clone(),
};
if d.repositories.is_empty() {
return vec![line("—".into(), d.lag_entries, &d.state, d.last_seen_unix_ms)];
}
d.repositories.iter().map(|r| line(r.repository.clone(), r.lag_entries, &r.state, r.last_complete_unix_ms)).collect()
});
twins.chain(downstreams).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn snapshot() -> pb::AdminSnapshotReply {
let taken = 1_800_000_000_000;
pb::AdminSnapshotReply {
taken_unix_ms: taken,
twin: Some(pb::AdminTwin {
twins: vec![
pb::AdminTwinRow { principal: "vm3".into(), address: "10.0.0.3:8443".into(), state: "ok".into(), last_seen_unix_ms: taken - 4_000, lag_entries: 0, ..Default::default() },
pb::AdminTwinRow { principal: "vm4".into(), address: "10.0.0.4:8443".into(), state: "behind".into(), last_seen_unix_ms: taken - 600_000, lag_entries: 12, ..Default::default() },
],
..Default::default()
}),
downstreams: vec![
pb::AdminDownstreamRow {
name: "dc2".into(),
state: "stalled".into(),
requests_today: 41,
bytes_today: 3_000_000,
repositories: vec![pb::AdminDownstreamRepo { repository: "crates".into(), state: "stalled".into(), lag_entries: 7, last_complete_unix_ms: taken - 7_200_000, ..Default::default() }],
..Default::default()
},
pb::AdminDownstreamRow {
name: "dc3".into(),
state: "silent".into(),
repositories: vec![pb::AdminDownstreamRepo { repository: "pypi".into(), state: "silent".into(), lag_entries: 99, last_complete_unix_ms: 0, ..Default::default() }],
..Default::default()
},
// A pairing that lists no repository still has its line.
pb::AdminDownstreamRow { name: "dc4".into(), state: "silent".into(), ..Default::default() },
],
..Default::default()
}
}
/// ★ **The view renders the four states**, each as the server said it,
/// with the lag, the last sync as a time ago and today's pulls.
#[test]
fn the_sync_view_renders_ok_behind_stalled_and_silent() {
let r = rows(&snapshot());
fn line(r: &SyncRow) -> (&'static str, &str, &str, &str, &str, Tone, &str, &str) {
(r.kind, r.name.as_str(), r.repository.as_str(), r.lag.as_str(), r.state.as_str(), r.tone, r.last_synced.as_str(), r.pulls_today.as_str())
}
assert_eq!(
r.iter().map(line).collect::<Vec<_>>(),
vec![
("twin", "vm3 10.0.0.3:8443", "every repository", "0 entries", "ok", Tone::Good, "4 s ago", "—"),
("twin", "vm4 10.0.0.4:8443", "every repository", "12 entries", "behind", Tone::Warn, "10 min ago", "—"),
("downstream", "dc2", "crates", "7 entries", "stalled", Tone::Bad, "2 h ago", "41 (3.0 MB)"),
("downstream", "dc3", "pypi", "99 entries", "silent", Tone::Bad, "never", "0 (0 B)"),
("downstream", "dc4", "—", "0 entries", "silent", Tone::Bad, "never", "0 (0 B)"),
]
);
}
#[test]
fn a_word_the_console_does_not_know_is_shown_untoned_and_times_read_in_words() {
assert_eq!((state_tone("ok"), state_tone("behind"), state_tone("stalled"), state_tone("silent")), (Tone::Good, Tone::Warn, Tone::Bad, Tone::Bad));
assert_eq!(state_tone("resyncing"), Tone::Neutral);
assert_eq!(ago(10_000, 0), "never");
assert_eq!(ago(10_000, 12_000), "0 s ago", "a clock a moment ahead is now, not the future");
assert_eq!(ago(259_200_000, 1), "2 days ago");
}
}