crates/monetize-ui-web/src/code_red.rs
//! **CODE RED on every page** (owner 2026-10-18): one red blinking banner under the top bar —
//! site · box · what is down · the error · the next action — for every thing an automatic path
//! left down and no operator has acknowledged yet. Monetize keeps them and mails them; this
//! only shows them and sends the operator's Ack.
use serde::{Deserialize, Serialize};
pub const CODE_RED_API: &str = "/api/code-red";
/// How often every page reads CODE RED again.
pub const REREAD_MS: u32 = 10_000;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct CodeRedJson {
pub key: String,
pub site: String,
pub r#box: String,
pub what: String,
pub error: String,
pub next: String,
pub raised_at_ms: i64,
pub acked_by: String,
pub acked_at_ms: i64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct CodeRedsJson {
pub items: Vec<CodeRedJson>,
}
/// Pure: **the banner's lines** — `(key, the line, the next action)` for every one not yet
/// acknowledged: `CODE RED · <site> · <box> — <what>: <error>`.
pub fn banner_lines(r: &CodeRedsJson) -> Vec<(String, String, String)> {
r.items
.iter()
.filter(|c| c.acked_at_ms == 0)
.map(|c| {
let mut head = vec!["CODE RED".to_string()];
for part in [&c.site, &c.r#box] {
if !part.trim().is_empty() {
head.push(part.trim().to_string());
}
}
let what = if c.error.trim().is_empty() {
c.what.trim().to_string()
} else {
format!("{}: {}", c.what.trim(), c.error.trim())
};
(
c.key.clone(),
format!("{} — {what}", head.join(" · ")),
c.next.trim().to_string(),
)
})
.collect()
}
/// The Ack's path for one.
pub fn ack_path(key: &str) -> String {
format!("{CODE_RED_API}/{}/ack", crate::fmt::percent_encode(key))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_banner_says_site_box_what_error_and_next_and_hides_the_seen() {
let r = CodeRedsJson {
items: vec![
CodeRedJson {
key: "update:gunnarrs:twin".into(),
site: "gunnar.rs".into(),
r#box: "twin".into(),
what: "the update stopped at the twin".into(),
error: "HTTP 404 STORAGE_NOT_FOUND".into(),
next: "start the twin, then press Update again".into(),
..Default::default()
},
CodeRedJson {
key: "seen".into(),
site: "test2.gunnar.rs".into(),
acked_at_ms: 5,
..Default::default()
},
],
};
let l = banner_lines(&r);
assert_eq!(l.len(), 1, "the acknowledged one is off the banner");
assert_eq!(l[0].1, "CODE RED · gunnar.rs · twin — the update stopped at the twin: HTTP 404 STORAGE_NOT_FOUND");
assert_eq!(l[0].2, "start the twin, then press Update again");
assert_eq!(
ack_path("update:gunnarrs:twin"),
"/api/code-red/update%3Agunnarrs%3Atwin/ack"
);
assert!(banner_lines(&CodeRedsJson::default()).is_empty());
}
}