Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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);
    }
}