estate/holger-live/src/waits.rs
//! **A first-boot wait, said by name, once a minute** (test3-holger-rs 2026-10-22: the main
//! stood 40+ minutes at `TICK stage=pairing` and nothing anyone could read said what it waited
//! for). Every wait of a Holger box's first boot — main, twin and front — says, in its boot log
//! (which the installer's log door, DEV-NARRATE and the ONE box's Why line read):
//!
//! ```text
//! WAITING <who> waits for <what> since 2026-10-22T18:12:05Z (12 min so far)
//! ```
//!
//! once when it starts and once a minute after ([`Wait::due`]), and once when it ends
//! ([`Wait::done`]). `<what>` is a description, never a value: a wait line names no secret,
//! and is worded so that no log filter withholds it (no `token`, `password`, `secret`,
//! `pubkey` in it — the redaction of a box built before nordisk-pairing 0.5.26 drops such a
//! line whole).
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
/// The word every wait line begins with (the ONE box's Why line looks for it).
pub const MARKER: &str = "WAITING";
/// How often a wait that goes on is said again.
pub const SAY_EVERY: Duration = Duration::from_secs(60);
/// Pure: `unix_s` as `YYYY-MM-DDTHH:MM:SSZ` (UTC).
pub fn utc(unix_s: u64) -> String {
let (days, rem) = (unix_s / 86_400, unix_s % 86_400);
// Howard Hinnant's civil_from_days.
let z = days as i64 + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = yoe + era * 400 + i64::from(m <= 2);
format!(
"{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
rem / 3_600,
rem % 3_600 / 60,
rem % 60
)
}
/// Pure: a duration for a person: `40 s`, `12 min`, `2 h 5 min`.
pub fn waited(secs: u64) -> String {
match secs {
s if s < 60 => format!("{s} s"),
s if s < 3_600 => format!("{} min", s / 60),
s => format!("{} h {} min", s / 3_600, s % 3_600 / 60),
}
}
/// Pure: **one wait line**.
pub fn line(who: &str, what: &str, since_unix_s: u64, waited_s: u64) -> String {
format!(
"{MARKER} {who} waits for {what} since {} ({} so far)",
utc(since_unix_s),
waited(waited_s)
)
}
/// Pure: **the line said when a wait ends**.
pub fn done_line(who: &str, what: &str, waited_s: u64) -> String {
format!(
"{MARKER} over: {who} waited {} for {what}",
waited(waited_s)
)
}
fn now_unix() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
/// **One wait**: who waits, for what, since when, and when it was last said.
#[derive(Debug, Clone)]
pub struct Wait {
who: String,
what: String,
since_unix_s: u64,
t0: Instant,
said: Option<Instant>,
}
impl Wait {
/// A wait that starts now.
pub fn new(who: &str, what: &str) -> Wait {
Wait {
who: who.into(),
what: what.into(),
since_unix_s: now_unix(),
t0: Instant::now(),
said: None,
}
}
/// A wait that started at `since_unix_s` / `t0` (the tests' clock).
pub fn at(who: &str, what: &str, since_unix_s: u64, t0: Instant) -> Wait {
Wait {
who: who.into(),
what: what.into(),
since_unix_s,
t0,
said: None,
}
}
/// What is waited for, with its start: `<what> since <utc>` (a pair status's words).
pub fn words(&self) -> String {
format!("{} since {}", self.what, utc(self.since_unix_s))
}
/// [`Self::due_at`] now.
pub fn due(&mut self) -> Option<String> {
self.due_at(Instant::now())
}
/// **The line to say at `now`**, or `None`: the first ask, then once per [`SAY_EVERY`].
pub fn due_at(&mut self, now: Instant) -> Option<String> {
if self
.said
.is_some_and(|s| now.saturating_duration_since(s) < SAY_EVERY)
{
return None;
}
self.said = Some(now);
Some(line(
&self.who,
&self.what,
self.since_unix_s,
now.saturating_duration_since(self.t0).as_secs(),
))
}
/// The line to say when the wait is over.
pub fn done(&self) -> String {
done_line(&self.who, &self.what, self.t0.elapsed().as_secs())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utc_is_the_calendar() {
assert_eq!(utc(0), "1970-01-01T00:00:00Z");
assert_eq!(utc(1_792_692_725), "2026-10-22T18:12:05Z");
assert_eq!(utc(951_782_400), "2000-02-29T00:00:00Z");
}
/// **A wait is said by name when it starts and once a minute after, with its start**
/// (test3-holger-rs 2026-10-22), and never in words a log filter withholds.
#[test]
fn a_wait_is_said_once_a_minute_by_name_with_its_start() {
let t0 = Instant::now();
let mut w = Wait::at(
"main",
"the front's offered console key, to derive the console login from",
1_792_692_725,
t0,
);
let first = w.due_at(t0).expect("said at once");
assert_eq!(first, "WAITING main waits for the front's offered console key, to derive the console login from since 2026-10-22T18:12:05Z (0 s so far)");
assert_eq!(
w.due_at(t0 + Duration::from_secs(30)),
None,
"not twice a minute"
);
let again = w
.due_at(t0 + Duration::from_secs(61))
.expect("once a minute");
assert!(
again.ends_with("since 2026-10-22T18:12:05Z (1 min so far)"),
"{again}"
);
assert_eq!(w.due_at(t0 + Duration::from_secs(100)), None);
assert!(w
.due_at(t0 + Duration::from_secs(40 * 60 + 5))
.unwrap()
.contains("(40 min so far)"));
assert_eq!(w.words(), "the front's offered console key, to derive the console login from since 2026-10-22T18:12:05Z");
for l in [first, again, w.done()] {
assert_eq!(
nordisk_pairing::redact::secret_class(&l),
None,
"no log filter withholds a wait line: {l}"
);
}
}
}