canic-cli 0.111.0

Operator CLI for Canic fleet setup, builds, evidence, catalog, backup, and restore workflows
Documentation
//! Module: fleet::progress::render
//!
//! Responsibility: render truthful human descriptions from typed host progress.
//! Does not own: backend readiness, progress inference or deployment completion.
//! Boundary: Root counts stay distinct from Components and reviewed effects from time.

use crate::support::path_stamp::utc_timestamp_ns;
use canic_contracts::dto::component_provisioning::FleetComponentProvisioningPhase;
use canic_host::fleet_ensure::dto::{
    FleetComponentProgress, FleetComponentProgressState, FleetEnsureActionKind, FleetEnsurePhase,
    FleetEnsureProgress, FleetEnsureProgressState, FleetProvisioningProgress,
};
use std::{fmt::Write as _, time::Duration};

pub(in crate::fleet) fn plain(progress: &FleetEnsureProgress) -> String {
    let mut line = format!(
        "Fleet deployment: {}; reviewed effects {}/{}",
        job(progress),
        progress.applied_effects,
        progress.reviewed_effects
    );
    if let Some(action) = &progress.next_action {
        let _ = write!(
            line,
            "; next reviewed work: {} ({})",
            action_label(action.kind),
            safe_text(&action.target)
        );
    }
    if let FleetEnsureProgressState::AwaitingProgress {
        elapsed_seconds,
        provisioning,
    } = &progress.state
    {
        let _ = write!(line, "; {elapsed_seconds}s awaiting this effect here");
        if let Some(detail) = provisioning {
            let _ = write!(
                line,
                "; prepared Roots {}/{}; registered Roots {}/{}; active Roots {}/{}; Components in scope {}",
                detail.provisioned_root_count,
                detail.root_batch_count,
                detail.directory_confirmed_root_count,
                detail.directory_confirmation_root_count,
                detail.runtime_activated_root_count,
                detail.root_batch_count,
                detail.component_count
            );
            if let Some(retry) = detail.pending_root_failure {
                let _ = write!(line, "; {}", pending_retry(retry).join("; "));
            }
            for component in &detail.components {
                let _ = write!(
                    line,
                    "; {}; Root {}",
                    component_label(component),
                    component.root
                );
            }
        } else {
            line.push_str("; readiness detail unavailable");
        }
    }
    if let FleetEnsureProgressState::ReviewRequired {
        review: Some(review),
        ..
    } = &progress.state
    {
        let _ = write!(line, "; {review}");
    }
    line
}

pub(super) fn milestone(
    progress: &FleetEnsureProgress,
    age: Duration,
    unchanged: Duration,
) -> String {
    let mut line = plain(progress);
    if age.as_secs() >= 30 {
        let _ = write!(
            line,
            "; observation: {}s old (stale); last change: {}s ago",
            age.as_secs(),
            unchanged.as_secs()
        );
    }
    line
}

pub(super) fn panel(
    progress: &FleetEnsureProgress,
    age: Duration,
    unchanged: Duration,
) -> Vec<String> {
    let stale = age.as_secs() >= 30;
    let marker = if stale {
        "!"
    } else if matches!(
        progress.state,
        FleetEnsureProgressState::AwaitingProgress { .. }
    ) {
        ["|", "/", "-", "\\"][(age.as_millis() / 250 % 4) as usize]
    } else {
        " "
    };
    let mut lines = vec![format!("{marker} {}", job(progress))];
    if let Some(action) = &progress.next_action
        && !matches!(
            progress.state,
            FleetEnsureProgressState::AwaitingProgress {
                provisioning: Some(_),
                ..
            }
        )
    {
        lines.push(format!(
            "Current work: {} ({})",
            action_label(action.kind),
            safe_text(&action.target)
        ));
    }
    if let FleetEnsureProgressState::AwaitingProgress {
        elapsed_seconds,
        provisioning,
    } = &progress.state
    {
        lines.push(String::new());
        if let Some(detail) = provisioning {
            lines.push(format!("{:<32} State   Roots", "Stage"));
            lines.extend(stages(detail));
            lines.push("Final checks                     Pending".into());
            if let Some(retry) = detail.pending_root_failure {
                lines.extend(pending_retry(retry));
            }
            lines.push(format!(
                "Waiting: {elapsed_seconds}s; Components in scope: {}",
                detail.component_count
            ));
            let mut components = detail.components.iter().collect::<Vec<_>>();
            components.sort_by_key(|component| !component.current);
            for component in components.iter().take(4) {
                lines.push(component_label(component));
                lines.push(format!("  Root {}", component.root));
            }
            if components.len() > 4 {
                lines.push(format!(
                    "{} more components in --json output and timing receipt",
                    components.len() - 4
                ));
            }
        } else {
            lines.push("Readiness detail unavailable".into());
            lines.push(format!("Waiting: {elapsed_seconds}s (last reported)"));
        }
    }
    lines.push(format!(
        "Reviewed work: {}/{} effects",
        progress.applied_effects, progress.reviewed_effects
    ));
    lines.push(format!(
        "Last change: {}s ago; observation: {}s old{}",
        unchanged.as_secs(),
        age.as_secs(),
        if stale { " (stale)" } else { "" }
    ));
    lines
}

fn component_label(component: &FleetComponentProgress) -> String {
    let state = match component.state {
        FleetComponentProgressState::Unknown => "unknown",
        FleetComponentProgressState::Reserved => "reserved",
        FleetComponentProgressState::Claimed => "claimed",
        FleetComponentProgressState::Installed => "installed",
        FleetComponentProgressState::Registered => "registered",
        FleetComponentProgressState::Published => "directory published",
        FleetComponentProgressState::RuntimePending => "awaiting activation",
        FleetComponentProgressState::Active => "active",
    };
    format!(
        "{}[{}]/{} ({}): {state}{}",
        safe_text(&component.deployment),
        component.placement,
        safe_text(&component.member_path.join("/")),
        safe_text(&component.component_spec),
        if component.current { " [current]" } else { "" }
    )
}

fn stages(detail: &FleetProvisioningProgress) -> [String; 3] {
    [
        stage(
            "Prepare application canisters",
            detail.provisioned_root_count,
            detail.root_batch_count,
            matches!(
                detail.phase,
                FleetComponentProvisioningPhase::RootsAccepted
                    | FleetComponentProvisioningPhase::ProvisioningRoots
            ),
        ),
        stage(
            "Register services",
            detail.directory_confirmed_root_count,
            detail.directory_confirmation_root_count,
            matches!(
                detail.phase,
                FleetComponentProvisioningPhase::ComponentsProvisioned
                    | FleetComponentProvisioningPhase::ServiceTopologyPublished
                    | FleetComponentProvisioningPhase::ConfirmingDirectories
            ),
        ),
        stage(
            "Start application services",
            detail.runtime_activated_root_count,
            detail.root_batch_count,
            matches!(
                detail.phase,
                FleetComponentProvisioningPhase::DirectoriesConfirmed
                    | FleetComponentProvisioningPhase::ActivatingRuntimes
            ),
        ),
    ]
}

fn stage(label: &str, done: u32, total: u32, waiting: bool) -> String {
    let state = if total == 0 || done > total {
        "Unknown"
    } else if done == total {
        "Done"
    } else if waiting {
        "Waiting"
    } else {
        "Pending"
    };
    format!("{label:<32} {state:<7} {done}/{total}")
}

const fn job(progress: &FleetEnsureProgress) -> &'static str {
    match &progress.state {
        FleetEnsureProgressState::Complete => "Deployment verified",
        FleetEnsureProgressState::FundingRequired => "Operator funding required",
        FleetEnsureProgressState::ReviewRequired { .. } => "Operator review required",
        FleetEnsureProgressState::PrerequisiteComplete => {
            "Prerequisites complete; deployment verification pending"
        }
        FleetEnsureProgressState::AwaitingProgress {
            provisioning: Some(detail),
            ..
        } => provisioning_job(detail.phase),
        FleetEnsureProgressState::AwaitingProgress { .. } => match progress.phase {
            FleetEnsurePhase::TerminalVerification => "Waiting for final verification",
            FleetEnsurePhase::PoolReadiness => "Waiting for pool readiness",
            _ => "Waiting for observed progress",
        },
        FleetEnsureProgressState::Advancing => match progress.phase {
            FleetEnsurePhase::Infrastructure => "Preparing infrastructure",
            FleetEnsurePhase::ImportReconciliation => "Reconciling pool canisters",
            FleetEnsurePhase::ControlPlane => "Preparing Fleet services and artifacts",
            FleetEnsurePhase::WorkloadProvisioning => "Preparing application services",
            FleetEnsurePhase::PoolReadiness => "Checking pool readiness",
            FleetEnsurePhase::TerminalVerification | FleetEnsurePhase::Complete => {
                "Verifying deployment"
            }
        },
    }
}

const fn provisioning_job(phase: FleetComponentProvisioningPhase) -> &'static str {
    match phase {
        FleetComponentProvisioningPhase::Planned
        | FleetComponentProvisioningPhase::AcceptingRoots => "Waiting for Roots to accept work",
        FleetComponentProvisioningPhase::RootsAccepted
        | FleetComponentProvisioningPhase::ProvisioningRoots => "Preparing application canisters",
        FleetComponentProvisioningPhase::ComponentsProvisioned
        | FleetComponentProvisioningPhase::ServiceTopologyPublished
        | FleetComponentProvisioningPhase::ConfirmingDirectories => "Registering services",
        FleetComponentProvisioningPhase::DirectoriesConfirmed
        | FleetComponentProvisioningPhase::ActivatingRuntimes => {
            "Waiting for application services to start"
        }
        FleetComponentProvisioningPhase::RuntimesActivated => {
            "Application services started; verification pending"
        }
    }
}

const fn action_label(kind: FleetEnsureActionKind) -> &'static str {
    match kind {
        FleetEnsureActionKind::ActivateRegistry => "Activate Fleet registry",
        FleetEnsureActionKind::ActivateRegistryMirror => "Activate registry mirror",
        FleetEnsureActionKind::AdoptStore => "Connect artifact Store",
        FleetEnsureActionKind::BootstrapStore => "Prepare artifact Store",
        FleetEnsureActionKind::Create => "Create canister",
        FleetEnsureActionKind::Delete => "Delete canister",
        FleetEnsureActionKind::DeleteSnapshot => "Clear reviewed snapshot",
        FleetEnsureActionKind::Fund => "Fund canister",
        FleetEnsureActionKind::FundEstate => "Fund Root estate",
        FleetEnsureActionKind::Install => "Install canister",
        FleetEnsureActionKind::JoinRoot => "Register Root",
        FleetEnsureActionKind::MaintainPoolReadiness => "Prepare ready pool canisters",
        FleetEnsureActionKind::ObservePoolReadiness => "Verify pool readiness",
        FleetEnsureActionKind::PrepareComponentRegistry => "Prepare service registry",
        FleetEnsureActionKind::PrepareStoreFixture => "Prepare artifact upload",
        FleetEnsureActionKind::Protocol => "Apply reviewed protocol action",
        FleetEnsureActionKind::ProvisionComponents => "Provision application services",
        FleetEnsureActionKind::PublishStoreChunk
        | FleetEnsureActionKind::PublishStoreFixtureChunk => "Upload artifact chunk",
        FleetEnsureActionKind::ReconcilePoolAsset => "Reconcile pool canister",
        FleetEnsureActionKind::SetControllers => "Set canister controllers",
        FleetEnsureActionKind::Start => "Start canister",
        FleetEnsureActionKind::Stop => "Stop canister",
        FleetEnsureActionKind::SynchronizeRegistry => "Synchronize registry",
        FleetEnsureActionKind::Transfer => "Transfer cycles",
        FleetEnsureActionKind::Uninstall => "Clear code and state",
    }
}

fn safe_text(value: &str) -> String {
    value
        .chars()
        .map(|ch| if ch.is_control() { '?' } else { ch })
        .collect()
}

fn pending_retry(
    retry: canic_contracts::dto::component_provisioning::FleetComponentProvisioningRootFailure,
) -> [String; 2] {
    if let Some(origin) = retry.origin {
        let deadline = origin.retry_at_ns.map_or_else(
            || "retry deadline unavailable".to_owned(),
            |value| format!("observed retry deadline: {}", utc_timestamp_ns(value)),
        );
        [
            format!(
                "Pending owner {}: {:?}, code {}, {:?}",
                origin.target, origin.stage, origin.diagnostic_code, origin.retry_category,
            ),
            deadline,
        ]
    } else {
        [
            format!(
                "Root {}: {:?}, code {}; originating owner unavailable",
                retry.fleet_subnet_root, retry.stage, retry.diagnostic_code
            ),
            "retry deadline unavailable".to_owned(),
        ]
    }
}