crates/nornir-viz-web/src/lib.rs
//! **nornir-viz-web** — nornir's viz as a `wasm32` WEBSITE.
//!
//! The owner's ask: "prepare a wasm for nornir viz … website with nornir … nornir
//! viz --> server nornir." This crate is the **viz-only** slice that crosses to
//! the browser: it renders nornir's reusable, wasm-capable **facett Facets** — the
//! very components nornir's native viz embeds (the 🧬 jobview, 🚇 metro/graphview,
//! 🌐 graph3d, 🧬 helix, 🏢 warehousedeck, 📖 docview) — through eframe's **wgpu /
//! WebGPU** backend, with NO native-only stack (no winit-native, no file dialogs, no
//! threads, no warehouse/redb, no tonic gRPC).
//!
//! The backend used to be glow, which on wasm32 means **WebGL2** — the thing LAW #4 /
//! GFX_V2 Decision 0 dropped. It also mattered for what this page can DRAW: the facett
//! facets above that own a wgpu custom pass (graph3d, through facett-core graphcloud) never
//! ran that pass at all under a WebGL host — the page silently fell back to the CPU painter.
//!
//! # Why a separate crate (not the whole `nornir` lib to wasm)
//!
//! The `nornir` binary crate is a native kitchen sink (redb warehouse, tonic gRPC,
//! tokio, gix, candle/ort, rmcp) that cannot cross to `wasm32-unknown-unknown`, and
//! The owner was explicit: **only the viz**, not the whole crate. The viz's genuinely
//! portable content is its facett Facets (pure egui, CPU painters), so THIS crate
//! depends on exactly those and builds them for wasm. The native viz (`nornir viz`
//! → `nornir-ui`) is untouched and stays fully working.
//!
//! # Wasm-capable vs native-only (the honest gate)
//!
//! Rendered in wasm (this deck): jobview, metro/graphview, graph3d, helix,
//! warehousedeck, docview — all pure egui + CPU painters, no I/O.
//! **Native-only** (NOT here, they need a device/FS/threads the browser lacks):
//! the 🔨 Build tab (spawns real `cargo build` on a worker thread), the warehouse
//! (redb), the gRPC/remote panes (tonic), file dialogs, and the wgpu GPU
//! fast-paths (the CPU painters draw instead in wasm).
//!
//! The same headless `state_json` contract the native viz exposes is exposed here
//! (via [`NornirVizApp::state_json`] and, on wasm, the `window.__nornir_viz_state()`
//! JS hook in `main.rs`), so a browser drive can read what actually rendered.
use eframe::egui::{self, Color32};
use facett_core::FacetDeck;
use facett_docview::{DocPage, DocView};
use facett_graph3d::{Graph3D, Layout3D};
use facett_graphview::metro::MetroView;
use facett_helix::HelixView;
use facett_jobview::{JobEntry, JobList, JobStatus};
use facett_warehousedeck::WarehouseDeck;
/// The wasm-capable facett Facets this website renders, in tab order — the
/// discoverable "what crosses to the browser" legend (id + one-line what-it-is).
/// Mirrors the six components nornir's native viz embeds and drives headless in
/// `autonom::facett_probe`.
pub const WASM_CAPABLE: &[(&str, &str)] = &[
(
"jobview",
"🧬 hierarchical / streaming job tree — facett-jobview",
),
(
"metro",
"🚇 subway-line view of test-coverage chains — facett-graphview::MetroView",
),
(
"graph3d",
"🌐 3-D node cloud (CPU-painted, orbit/spin) — facett-graph3d",
),
(
"helix",
"🧬 TIME-HELIX plan/dep DAG winding along release time — facett-helix",
),
(
"warehousedeck",
"🏢 multi-pane lakehouse deck (CPU graph3d panes) — facett-warehousedeck",
),
(
"docview",
"📖 vector-SVG documentation viewer (pure-Rust resvg) — facett-docview",
),
];
/// Build the deck of nornir's wasm-capable facett Facets, seeded with realistic
/// demo data — no window, no I/O (so the headless self-check can construct it).
pub fn nornir_viz_deck() -> FacetDeck {
// 🧬 jobview — a workspace populate as a parent job + streaming child clones.
let mut jobs = JobList::new("jobview");
let child = |id: &str, name: &str, st: JobStatus| JobEntry {
target: name.into(),
parent_id: Some("populate".to_string()),
..JobEntry::new(id, "workspace_clone", st)
};
jobs.set_jobs(vec![
JobEntry {
target: "nordisk".into(),
detail: "populate nordisk — 5 members".into(),
..JobEntry::new("populate", "workspace_populate", JobStatus::Running)
},
child("clone-nornir", "nornir", JobStatus::Done),
child("clone-facett", "facett", JobStatus::Done),
child("clone-holger", "holger", JobStatus::Running),
child("clone-znippy", "znippy", JobStatus::Queued),
]);
// 🚇 metro — the built-in two-line coverage map (one green, one with an unlit
// emitter → red). `local()` self-titles "Metro".
let metro = MetroView::local();
// 🌐 graph3d — a small labelled node cloud on a sphere (CPU-painted, orbitable).
let nodes: Vec<(String, Color32)> = vec![
("nornir".into(), Color32::from_rgb(0x4e, 0x9a, 0xf1)),
("facett".into(), Color32::from_rgb(0x8b, 0xc3, 0x4a)),
("holger".into(), Color32::from_rgb(0xf1, 0xa7, 0x4e)),
("znippy".into(), Color32::from_rgb(0xe5, 0x73, 0x73)),
("skade".into(), Color32::from_rgb(0xba, 0x8b, 0xf1)),
];
let edges = vec![(0usize, 1usize), (0, 2), (2, 3), (0, 4)];
let graph3d = Graph3D::new(nodes, edges, Layout3D::Sphere);
// 🧬 helix — the built-in demo scene (crate coils winding along release time).
let helix = HelixView::demo();
// 🏢 warehousedeck — a wall of CPU-painted panes.
let warehousedeck = WarehouseDeck::new("warehousedeck").with_cols(2);
// 📖 docview — a tiny bundled vector-SVG manual page (no fonts, glyphs outlined).
let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" width="240" height="320">
<rect width="240" height="320" fill="#ffffff"/>
<rect x="20" y="20" width="200" height="34" fill="#4e9af1"/>
<rect x="20" y="74" width="150" height="10" fill="#333"/>
<rect x="20" y="94" width="180" height="10" fill="#666"/>
<rect x="20" y="114" width="120" height="10" fill="#666"/>
</svg>"##;
let pages: Vec<DocPage> = DocPage::from_svg_bytes(svg).into_iter().collect();
let toc = if pages.is_empty() {
Vec::new()
} else {
vec![("nornir viz — web".to_string(), 0usize)]
};
let docview = DocView::new("docview", pages, toc);
FacetDeck::new(vec![
Box::new(jobs),
Box::new(metro),
Box::new(graph3d),
Box::new(helix),
Box::new(warehousedeck),
Box::new(docview),
])
}
/// The eframe app: a [`FacetDeck`] of nornir's wasm-capable facett Facets. The deck
/// draws its own tab bar + the active facet and composes every facet's `state_json`
/// under its title, so the whole-app introspection contract is free.
pub struct NornirVizApp {
/// The deck of wasm-capable facett Facets.
pub deck: FacetDeck,
}
impl Default for NornirVizApp {
fn default() -> Self {
Self::new()
}
}
impl NornirVizApp {
/// Build the app (headless — no window). The self-check + the native/wasm
/// entry points all go through here.
pub fn new() -> Self {
Self {
deck: nornir_viz_deck(),
}
}
/// The whole-app observable state (every tab's `state_json` under its title) —
/// the same read-back contract the native viz exposes, so a browser drive can
/// assert what rendered.
pub fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"app": "nornir-viz-web",
"wasm_capable": WASM_CAPABLE.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
"active": self.deck.active_title(),
"tabs": self.deck.titles(),
"deck": self.deck.state_json(),
})
}
}
impl eframe::App for NornirVizApp {
// This eframe stack's `App` hands the app a `Ui` (the central area) directly —
// the same render path facett-demo uses (CLI/GUI parity), so what draws here is
// what ships.
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
ui.horizontal(|ui| {
ui.heading("nornir · viz");
ui.separator();
ui.weak("wasm-capable facett Facets (wgpu / WebGPU)");
});
ui.separator();
self.deck.ui(ui);
// On wasm, publish the whole-app state each frame so `window.__nornir_viz_state()`
// (the JS hook in main.rs) returns what actually rendered in the SHIPPED bundle.
#[cfg(target_arch = "wasm32")]
{
let s = serde_json::to_string(&self.state_json()).unwrap_or_default();
crate::wasm_state::publish(s);
}
}
}
/// Wasm-only: the process-global backing store for the `window.__nornir_viz_state()`
/// JS hook (there is no filesystem in the browser). The app publishes its whole
/// `state_json` here each frame; `main.rs`'s exported `__nornir_viz_state` reads it.
#[cfg(target_arch = "wasm32")]
pub mod wasm_state {
use std::sync::Mutex;
static LATEST: Mutex<String> = Mutex::new(String::new());
/// Publish the latest whole-app `state_json` string (called once per frame).
pub fn publish(json: String) {
if let Ok(mut g) = LATEST.lock() {
*g = json;
}
}
/// Read the latest published state (the JS hook's backing store).
pub fn latest() -> String {
LATEST.lock().map(|g| g.clone()).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deck_carries_the_six_wasm_capable_facets() {
let app = NornirVizApp::new();
let titles = app.deck.titles();
assert_eq!(titles.len(), 6, "one tab per wasm-capable facett Facet");
// The legend and the built deck agree on the count.
assert_eq!(WASM_CAPABLE.len(), 6);
}
#[test]
fn state_json_is_the_read_back_contract() {
let app = NornirVizApp::new();
let j = app.state_json();
assert_eq!(j["app"], "nornir-viz-web");
assert_eq!(j["tabs"].as_array().unwrap().len(), 6);
// The composed deck state carries every facet's own state_json under its title.
assert!(
j["deck"].is_object(),
"deck composes every facet's state_json"
);
assert_eq!(j["wasm_capable"].as_array().unwrap().len(), 6);
}
}