nornir 0.5.0

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
//! πŸš‡ **Metro tab** β€” the dedicated viz pane that renders nornir's button β†’ gRPC β†’
//! warehouse coverage CHAINS through the reusable facett component
//! [`facett_graphview::MetroView`] (a [`facett_core::Facet`], imported here as
//! [`facett_jobview::Facet`] β€” the SAME `facett-core` trait both crates re-export).
//!
//! This pane owns the `MetroView` and pushes a fresh [`facett_graphview::MetroMap`]
//! into it each refresh (the push model). The map is produced by the PURE
//! [`super::metro_feed::build_metro_map`] over the live workspace's warehouse facts
//! + coverage; the app feeds it from the πŸ› Architecture tab's loaded data via
//! [`super::arch_tab::ArchTabState::facett_metro_map`] (one loader, no second scan).
//!
//! [`state_json`](Facet::state_json) surfaces the whole drawn map (lines, per-station
//! {label, kind, lit}, per-line + overall green) so the headless test matrix asserts
//! exactly what the line lit up WITHOUT a pixel β€” LAW 6.

use facett_graphview::{MetroMap, MetroView};
use facett_jobview::Facet; // == facett_core::Facet (both crates re-export the 0.1 trait)

use super::facett_theme::Theme;

/// πŸš‡ The Metro tab pane state β€” a thin host around the facett [`MetroView`].
pub struct MetroTabState {
    view: MetroView,
    /// Cached so a workspace switch re-feeds (the app pushes a fresh map on change).
    fed: bool,
    /// Lines in the last-fed map β€” 0 β‡’ the warehouse has no coverage chains yet
    /// (the LAW-2 empty board), so the shared `repo_pane` empty-state shows the
    /// "⏳ in route for populate…" / "not scanned yet" reason instead of a blank.
    line_count: usize,
    /// The active workspace (app pushes it via `set_workspace_name`) β€” the key the
    /// shared `repo_pane` jobs lookup needs to know if a populate is in route.
    workspace: String,
}

impl Default for MetroTabState {
    fn default() -> Self {
        Self {
            view: MetroView::new("πŸš‡ Metro"),
            fed: false,
            line_count: 0,
            workspace: String::new(),
        }
    }
}

impl MetroTabState {
    /// **Discovery-contract `local()`** β€” the no-arg ctor the test matrix enumerates.
    /// Starts empty; the app feeds the real workspace map via [`Self::set_map`]
    /// (mirrors `facett-jobview`'s `JobList::default` host on the 🧬 Nornir tab).
    pub fn local() -> Self {
        Self::default()
    }

    /// **Discovery-contract `remote()`** β€” same surface; the thin/server-fed variant.
    pub fn remote() -> Self {
        Self::default()
    }

    /// Replace the drawn map (the push model β€” called each refresh from the app's
    /// live workspace data). Marks the pane fed so a "no data yet" hint clears, and
    /// records the line count so an EMPTY map (no coverage chains) shows the shared
    /// LAW-2 empty-state instead of a blank board.
    pub fn set_map(&mut self, map: MetroMap) {
        self.line_count = map.lines.len();
        self.view.set_map(map);
        self.fed = true;
    }

    /// The active workspace, so the shared `repo_pane` empty-state can tell whether
    /// a populate is in route. The app pushes it each refresh alongside `set_map`.
    pub fn set_workspace_name(&mut self, workspace: String) {
        self.workspace = workspace;
    }

    /// Whether a real map has been fed yet (else the pane shows a load hint).
    pub fn is_fed(&self) -> bool {
        self.fed
    }

    /// The pane's LAW-2 empty-state: a fed-but-line-less map is the warehouse
    /// having no coverage chains yet β€” "⏳ in route for populate…" when a job is
    /// active for this workspace, else "not scanned yet". `Populated` once lines
    /// exist (or before the first feed, where the load hint already shows).
    fn empty_state(&self) -> super::repo_pane::EmptyState {
        let empty = self.fed && self.line_count == 0;
        super::repo_pane::classify_empty(empty, &self.workspace, "")
    }

    /// The active facett palette (C8) β€” the Facet reads the ambient egui theme, so
    /// this is a no-op seam kept for parity with the other tabs' `set_palette`.
    pub fn set_palette(&mut self, _t: Theme) {}

    /// Draw the pane: a header + the facett [`MetroView`] (scoped to the stable
    /// `Metro` pane id so the AccessKit `id_salt` matches the surfaces registry).
    pub fn draw(&mut self, ui: &mut egui::Ui) {
        ui.push_id("Metro", |ui| {
            ui.horizontal(|ui| {
                ui.heading("πŸš‡ Metro");
                ui.label(
                    egui::RichText::new(
                        "button β†’ emitters β†’ gRPC β†’ warehouse table, lit by the latest coverage run",
                    )
                    .weak(),
                );
            });
            if !self.fed {
                ui.label(
                    egui::RichText::new(
                        "loading the workspace's coverage chains… (open πŸ› Architecture once to warm the warehouse facts)",
                    )
                    .weak(),
                );
            } else if self.line_count == 0 {
                // LAW 2: fed but no chains β€” the warehouse has no coverage data yet.
                // Name WHY (in route for populate / not scanned) instead of an empty
                // board, then still let the (empty) Facet draw its frame below.
                let col = ui.visuals().text_color();
                super::repo_pane::render_empty(ui, true, &self.workspace, "", col);
            }
            ui.separator();
            Facet::ui(&mut self.view, ui);
        });
    }

    /// The drawn map as DATA (LAW 6) β€” delegates to the facett `MetroView`'s
    /// `state_json` (lines, per-station {label, kind, lit}, per-line + overall green).
    pub fn state_json(&self) -> serde_json::Value {
        let mut v = Facet::state_json(&self.view);
        if let Some(obj) = v.as_object_mut() {
            obj.insert("fed".into(), serde_json::Value::Bool(self.fed));
            // LAW 2 empty-state tag (populated / in_route / not_scanned) so a
            // headless drive asserts a fed-but-empty metro names its reason.
            obj.insert(
                "empty_state".into(),
                serde_json::Value::String(self.empty_state().id().to_string()),
            );
        }
        v
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use facett_graphview::{MetroLine, MetroStation, StationKind};

    /// inject-assert: feeding a real green line lights state_json up (green=true) and
    /// the pane reports fed.
    #[test]
    fn fed_map_surfaces_in_state_json() {
        let mut tab = MetroTabState::local();
        assert!(!tab.is_fed());
        assert_eq!(tab.state_json()["fed"], serde_json::json!(false));

        let line = MetroLine::new(
            "b→Svc.Verb",
            "b",
            vec![
                MetroStation::new("ui::b", "b", StationKind::Start, true),
                MetroStation::new("svc::emit", "emit", StationKind::Emitter, true),
                MetroStation::new("grpc::Svc.Verb", "Svc.Verb", StationKind::Grpc, true),
                MetroStation::new("wh::t", "t", StationKind::Terminus, true),
            ],
        );
        tab.set_map(MetroMap::new(vec![line]));
        assert!(tab.is_fed());
        let js = tab.state_json();
        assert_eq!(js["fed"], serde_json::json!(true));
        assert_eq!(js["lines"], serde_json::json!(1));
        assert_eq!(js["green"], serde_json::json!(true));
    }
}