Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger-ui

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");
    }
}