crates/monetize-ui-web/src/narrate.rs
//! **"Logs" on a narrated site** (lane N2, 2026-10-20): the DEV-NARRATE boot logs the ONE box
//! keeps for a test site that says `narrate = true`, each machine's tail. The cells the page
//! shows, tested on the host; [`crate::pages::sites::SiteLogs`] only lays them out. Admin
//! only: the console's server refuses anyone else (`/api/narrate/…`), and the page offers
//! the button to an admin alone.
use serde::Deserialize;
/// The console's route head: `{NARRATE_API}{estate}` (JSON), `{NARRATE_API}{estate}/{vm}` (text).
pub const NARRATE_API: &str = "/api/narrate/";
/// How often an open Logs panel reads again.
pub const REREAD_MS: u32 = 5_000;
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct VmLogJson {
pub vm: String,
pub text: String,
pub bytes: u64,
pub updated_unix_ms: i64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct NarrateJson {
pub estate: String,
pub narrated: bool,
pub words: String,
pub vms: Vec<VmLogJson>,
}
/// `/api/narrate/<estate>`.
pub fn logs_path(estate: &str) -> String {
format!("{NARRATE_API}{}", estate.trim())
}
/// `/api/narrate/<estate>/<vm>`: one machine's text, to open or save.
pub fn text_path(estate: &str, vm: &str) -> String {
format!("{NARRATE_API}{}/{}", estate.trim(), vm.trim())
}
/// The button's words, and whether it is offered: an admin's alone.
pub fn logs_button(admin: bool, open: bool) -> Option<&'static str> {
admin.then_some(if open { "Hide logs" } else { "Logs" })
}
/// One machine's heading: its role, what is kept, and an empty log said in words.
pub fn vm_head(v: &VmLogJson) -> String {
if v.bytes == 0 {
format!("{} — nothing heard yet", v.vm)
} else {
format!("{} — {} KiB kept", v.vm, v.bytes.div_ceil(1024))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_logs_cells_read_as_the_page_shows_them() {
assert_eq!(
logs_path(" test3-holger-rs "),
"/api/narrate/test3-holger-rs"
);
assert_eq!(
text_path("test3-holger-rs", "main"),
"/api/narrate/test3-holger-rs/main"
);
assert_eq!(
logs_button(false, false),
None,
"a viewer is offered no logs"
);
assert_eq!(
(logs_button(true, false), logs_button(true, true)),
(Some("Logs"), Some("Hide logs"))
);
assert_eq!(
vm_head(&VmLogJson {
vm: "front".into(),
..Default::default()
}),
"front — nothing heard yet"
);
assert_eq!(
vm_head(&VmLogJson {
vm: "main".into(),
bytes: 2049,
..Default::default()
}),
"main — 3 KiB kept"
);
}
}