holger-ui 0.1.4

Operator/admin UI for holger over the HolgerObject core API — egui via facett, embedded (LocalHolger, direct core calls) or remote (RemoteHolger gRPC).
//! Operator-tool config + result types for the holger UI data layer (Tools#1–#5):
//! holger-ds delivery bench, migrate, airgap, and deploy (Skíðblaðnir). Each is a
//! plain serializable struct with a `state_json` so the robot asserts the pane as
//! DATA; the RUNNERS that populate them live on `UiData` in the parent. Split out of
//! `data`; `use super::*` inherits the parent's imports (Serialize / Value / …).

use super::*;

/// The result of one `holger-ds` artifact-delivery run, measured **over the
/// package protocol** (never in-process): file/crate counts plus the throughput
/// the gatling pool actually pulled off the wire. Surfaced in the Tools#1 pane and
/// `state_json` so the robot asserts a REAL benchmark ran (not a no-op).
#[derive(Debug, Clone, Serialize, Default, PartialEq)]
pub struct DsResult {
    /// `true` once a run completed and populated the throughput figures.
    pub ran: bool,
    pub scale: String,
    /// `"self-host"` or the external target base URL the run drove.
    pub target: String,
    pub crates: usize,
    pub files: usize,
    pub ops: u64,
    pub bytes: u64,
    pub mbs: f64,
    pub ops_per_sec: f64,
    /// One-line human status/result shown in the pane.
    pub status: String,
    pub error: Option<String>,
}

/// Tool #1 holger-ds configuration — the demo-plan scale slider + target. Drives
/// the artifact-delivery benchmark: `scale` (micro|small|large), and an optional
/// `target` base URL to measure an EXTERNAL holger/nexus/artifactory over the
/// real package protocol (empty ⇒ stand up a local holger gateway).
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct DsConfig {
    /// `"micro"` / `"small"` / `"large"`.
    pub scale: String,
    /// External target base URL, or empty to self-host.
    pub target: String,
    /// One-line status shown in the pane (last run / running notice).
    pub status: String,
    /// The last completed run's result, if any.
    pub result: Option<DsResult>,
}

impl Default for DsConfig {
    fn default() -> Self {
        Self {
            scale: "micro".to_string(),
            target: String::new(),
            status: String::new(),
            result: None,
        }
    }
}

impl DsConfig {
    pub fn state_json(&self) -> Value {
        serde_json::to_value(self).unwrap_or(Value::Null)
    }

    /// The equivalent `holger-server holger-ds` CLI invocation — the plan the
    /// Tools#1 pane shows (and a robot asserts).
    pub fn cli_command(&self) -> String {
        let mut s = format!("holger-server holger-ds --preset {}", self.scale);
        if !self.target.is_empty() {
            s.push_str(&format!(" --target {}", self.target));
        }
        s
    }
}

/// The result of one Migrate ("suck") run via the holger agent connector:
/// how many upstream repositories were dumped into the znippy archive (or a clear
/// error). Surfaced in the Tools#2 pane + `state_json` so the robot asserts the
/// connector was actually invoked (not a label-only stub).
#[derive(Debug, Clone, Serialize, Default, PartialEq, Eq)]
pub struct MigrateRun {
    /// `true` once the connector ran and produced an archive.
    pub ran: bool,
    pub source: String,
    pub url: String,
    pub output: String,
    /// Number of upstream repositories sucked into the archive.
    pub repositories: usize,
    pub status: String,
    pub error: Option<String>,
}

/// Tool #2 Migrate ("suck") configuration — pull a Nexus / Artifactory registry
/// into one or many znippy archives via the holger agent (the single home of the
/// sucking). `source` is `"nexus"` or `"artifactory"`; `url` the registry base;
/// `output` the `.znippy` path; `repository` an optional single-repo filter.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct MigrateConfig {
    /// `"nexus"` or `"artifactory"`.
    pub source: String,
    pub url: String,
    pub output: String,
    pub repository: String,
    /// One-line status shown in the pane (last run / running notice).
    pub status: String,
    /// The last completed run's result, if any.
    pub result: Option<MigrateRun>,
}

impl Default for MigrateConfig {
    fn default() -> Self {
        Self {
            source: "nexus".to_string(),
            url: String::new(),
            output: "migrated.znippy".to_string(),
            repository: String::new(),
            status: String::new(),
            result: None,
        }
    }
}

impl MigrateConfig {
    pub fn state_json(&self) -> Value {
        serde_json::to_value(self).unwrap_or(Value::Null)
    }

    /// The equivalent `holger-agent` invocation — the migrate plan the Tools#2
    /// pane shows (and a robot asserts). Uses `--<source>-url` so it stays in
    /// lockstep with the agent CLI flags.
    pub fn cli_command(&self) -> String {
        let url_flag = if self.source == "artifactory" { "--artifactory-url" } else { "--nexus-url" };
        let url = if self.url.is_empty() { "<url>" } else { &self.url };
        let mut s = format!(
            "holger-agent --from {} {url_flag} {url} --to znippy --output {}",
            self.source, self.output
        );
        if !self.repository.is_empty() {
            s.push_str(&format!(" --repository {}", self.repository));
        }
        s
    }
}

/// The result of one Tool #5 Airgap populate run. A `--dry-run` produces the plan
/// only (no shell); a real run delegates to the `distribution/airgap/populate`
/// driver and captures its log + exit status. Surfaced in the Tools#5 pane +
/// `state_json` so the robot asserts a real plan/result (never a crash/no-op).
#[derive(Debug, Clone, Serialize, Default, PartialEq, Eq)]
pub struct AirgapRun {
    /// `true` once a plan/run completed (dry-run plan counts as ran).
    pub ran: bool,
    pub dry_run: bool,
    /// The pure-Rust plan text (always present once validated).
    pub plan: String,
    /// The driver's captured log (empty for a dry-run).
    pub log: String,
    pub exit_code: i32,
    pub success: bool,
    pub status: String,
    pub error: Option<String>,
}

/// The result of one Tool #4 Deploy (Skíðblaðnir) run. Reads the selected repo's
/// **sealed znippy bundle** over the core API (`archive_info`) — the real artifact
/// to deploy — and folds in an airgap-engine push-only deploy plan for the chosen
/// test target. Surfaced in the Tools#4 pane + `state_json` so the robot asserts a
/// real deploy ran (never a no-op / "being implemented" stub).
#[derive(Debug, Clone, Serialize, Default, PartialEq, Eq)]
pub struct DeployRun {
    /// `true` once a deploy plan was produced over a real sealed bundle.
    pub ran: bool,
    pub repository: String,
    /// `"holger"` / `"nexus"` / `"artifactory"` — the airgap target deployed to.
    pub target: String,
    /// File count of the sealed znippy bundle being deployed (from `archive_info`).
    pub file_count: u64,
    /// Total uncompressed bytes of the sealed bundle.
    pub bytes: u64,
    /// The backing archive path / repo name reported by the core.
    pub archive_path: String,
    /// The airgap-engine deploy plan (push-only carry of the sealed bundle).
    pub plan: String,
    pub status: String,
    pub error: Option<String>,
}

/// Tool #4 Deploy (Skíðblaðnir) configuration — pick the test target the selected
/// repo's sealed znippy bundle is deployed to. `target` reuses the airgap engine's
/// [`holger_airgap::Target`] so the deploy rides the documented airgap path.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct DeployConfig {
    /// The deploy target (reuses the airgap engine's targets).
    pub target: holger_airgap::Target,
    /// One-line status shown in the pane (last run / running notice).
    pub status: String,
    /// The last completed deploy's result, if any.
    pub result: Option<DeployRun>,
}

impl Default for DeployConfig {
    fn default() -> Self {
        Self {
            target: holger_airgap::Target::Holger,
            status: String::new(),
            result: None,
        }
    }
}

impl DeployConfig {
    pub fn state_json(&self) -> Value {
        serde_json::to_value(self).unwrap_or(Value::Null)
    }
}