//! Rendering for `sm121 report`: the Markdown table (stdout mode) and the //! embedded HTML page (serve mode), over the collected [`ReportRow`]s. use super::provenance; use super::{PAGE_TEMPLATE, PerfCell, ReportEntry, ReportRow}; pub(crate) fn latest_score(r: &ReportRow) -> Option<&ReportEntry> { r.reports .iter() .rev() .find(|e| e.pct.is_some() || e.points.is_some()) } pub(crate) fn latest_perf(r: &ReportRow) -> Option<(&PerfCell, Option<&PerfCell>)> { r.reports.iter().rev().find_map(|e| { let c1 = e.perf.iter().find(|p| p.d == 0 && p.c == 1); c1.map(|c1| (c1, e.perf.iter().find(|p| p.d == 0 && p.c == 2))) }) } /// Normalized score, whole numbers without decimals (81, not 81.0). pub(crate) fn fmt_pct(pct: f64) -> String { if (pct - pct.trunc()).abs() < 1e-9 { format!("{}", pct as i64) } else { format!("{pct:.1}") } } pub(crate) fn fmt_tps(c: Option<&PerfCell>) -> String { c.map(|c| format!("{:.1}", c.tg)) .unwrap_or_else(|| "—".into()) } pub(crate) fn markdown(rows: &[ReportRow]) -> String { let mut s = format!( "# {}\n\n", fluent_i18n::t!("report-title", { "count" => rows.len() }) ); s.push_str(&format!( "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", t("report-col-model"), t("report-col-quant"), t("report-col-recipe"), t("report-col-prov"), t("report-col-engine"), t("report-col-nodes"), t("report-col-reports"), t("report-col-last"), t("report-col-up"), t("report-col-launches"), t("report-col-points"), t("report-col-score"), t("report-col-tps1"), t("report-col-tps2"), )); s.push_str("|---|---|---|---|---|---|---|---|---|---|---|---|\n"); for r in rows { let last = r.reports.last(); let latest = latest_score(r); let perf = latest_perf(r); s.push_str(&format!( "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", r.model, r.quant, r.name, provenance::cell(r), r.engine, r.nodes .map(|n| format!("{n}×{}", r.tp)) .unwrap_or_else(|| format!("1×{}", r.tp)), r.reports.len(), last.map(|e| match &e.time { Some(t) => format!("{} {t}", e.date), None => e.date.clone(), }) .unwrap_or("—".into()), r.up.map(|u| format!("● {}", crate::stamp::fmt_ts(u))) .unwrap_or_else(|| "—".into()), r.runs, latest .and_then(|e| e.points.clone()) .unwrap_or_else(|| "—".into()), latest .and_then(|e| e.pct) .map(fmt_pct) .unwrap_or_else(|| "—".into()), fmt_tps(perf.map(|(a, _)| a)), fmt_tps(perf.and_then(|(_, b)| b)), )); } s } pub(crate) fn t(key: &str) -> String { fluent_i18n::t!(key) } /// The interactive page: template with `{{DATA}}` replaced by the JSON /// rows (`<` escaped so `` in data can't break out). pub(crate) fn html_page(rows: &[ReportRow]) -> String { let json = serde_json::to_string(rows).unwrap_or_else(|_| "[]".into()); PAGE_TEMPLATE.replace("{{DATA}}", &json.replace('<', "\\u003c")) } /// Same page mounted under a URL prefix — the dashboard nests it at /// `/recipes`, so the page's own absolute paths (its SSE stream and the /// `/r//` viewer links) have to move with it. pub(crate) fn html_page_at(rows: &[ReportRow], prefix: &str) -> String { let page = html_page(rows); if prefix.is_empty() { return page; } page.replace("/events", &format!("{prefix}/events")) .replace("/r/", &format!("{prefix}/r/")) // marks the page as hosted (dashboard window): no own branding, and // the live chip only warns on a broken stream .replacen("", "", 1) }