node-app-build 6.4.3

Mini app developer CLI: scaffold, validate, package node-app-* Debian packages
//! Infrastructure health checks via docker inspect (no env-file needed).

use std::process::Command;

use super::ops::{postgres_container_name, rgs_container_name};
use super::InfraContext;

#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub enum InfraHealthStatus {
    Healthy,
    Running,
    Degraded,
    Down,
    External,
}

#[derive(Debug, Clone)]
pub struct ServiceHealth {
    pub name: String,
    pub status: InfraHealthStatus,
    pub detail: String,
}

/// Check health of postgres and rgs via docker inspect (no docker compose env needed).
pub fn check_all(ctx: &InfraContext) -> Vec<ServiceHealth> {
    vec![
        check_container(ctx, "postgres", &postgres_container_name(ctx)),
        check_rgs_container(ctx),
    ]
}

fn is_running(container: &str) -> bool {
    Command::new("docker")
        .args(["inspect", "--format", "{{.State.Running}}", container])
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "true")
        .unwrap_or(false)
}

fn container_health(container: &str) -> Option<String> {
    let out = Command::new("docker")
        .args(["inspect", "--format", "{{.State.Health.Status}}", container])
        .output()
        .ok()?;
    let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
    if s.is_empty() || s == "<no value>" { None } else { Some(s) }
}

fn check_container(ctx: &InfraContext, name: &str, container: &str) -> ServiceHealth {
    if !is_running(container) {
        return ServiceHealth {
            name: name.to_string(),
            status: InfraHealthStatus::Down,
            detail: format!("container {container} not running"),
        };
    }
    match container_health(container).as_deref() {
        Some("healthy") => {
            let detail = if name == "postgres" {
                db_summary_detail(ctx).unwrap_or_else(|| "healthy".to_string())
            } else {
                "healthy".to_string()
            };
            ServiceHealth { name: name.to_string(), status: InfraHealthStatus::Healthy, detail }
        }
        Some(h) => ServiceHealth {
            name: name.to_string(),
            status: InfraHealthStatus::Running,
            detail: format!("running ({h})"),
        },
        None => ServiceHealth {
            name: name.to_string(),
            status: InfraHealthStatus::Running,
            detail: "running".to_string(),
        },
    }
}

fn check_rgs_container(ctx: &InfraContext) -> ServiceHealth {
    let container = rgs_container_name(ctx);
    if !is_running(&container) {
        return ServiceHealth {
            name: "rgs".to_string(),
            status: InfraHealthStatus::Down,
            detail: format!("container {container} not running"),
        };
    }
    // Check if snapshot file exists in the container
    let has_snapshot = Command::new("docker")
        .args(["exec", &container, "test", "-s", "/res/symlinks/0.bin"])
        .status()
        .map(|s| s.success())
        .unwrap_or(false);

    if has_snapshot {
        let ts = super::snapshot::fetch_snapshot_summary(ctx, None)
            .ok()
            .map(|s| format!("snapshot: {}", s.timestamp_str))
            .unwrap_or_else(|| "snapshot ready".to_string());
        ServiceHealth { name: "rgs".to_string(), status: InfraHealthStatus::Healthy, detail: format!(":8011 · {ts}") }
    } else {
        ServiceHealth { name: "rgs".to_string(), status: InfraHealthStatus::Running, detail: ":8011 · syncing gossip…".to_string() }
    }
}

fn db_summary_detail(ctx: &InfraContext) -> Option<String> {
    let s = super::db::query_summary(ctx).ok()?;
    Some(format!(":5432 · {} nodes · {} channels · {} updates",
        s.node_announcements, s.channel_announcements, s.channel_updates))
}

pub fn print_status(ctx: &InfraContext) {
    let services = check_all(ctx);
    println!("{:<12} {:<14} Details", "Service", "Status");
    println!("{}", "".repeat(60));
    for svc in &services {
        let label = match svc.status {
            InfraHealthStatus::Healthy  => "✓ healthy",
            InfraHealthStatus::Running  => "◐ running",
            InfraHealthStatus::Degraded => "⚠ degraded",
            InfraHealthStatus::Down     => "✗ down",
            InfraHealthStatus::External => "⚡ external",
        };
        println!("{:<12} {:<14} {}", svc.name, label, svc.detail);
    }
}