rustploy 0.1.6

Rustploy PaaS daemon — lightweight self-hosted deployment platform
//! `Command::DeployEngineStatus`: estado da tela Deploy Engine: fila (na ordem
//! da fila), o que está rodando, pausa e histórico recente, com nomes de
//! serviço/projeto.

use crate::bootstrap::state::AppState;
use chrono::Utc;
use shared::{ActiveDeployInfo, DeployEngineSummary, Response as RpResponse};
use std::collections::HashMap;

pub async fn handle(state: AppState) -> RpResponse {
    let db = &state.db;

    let active_deps = match crate::store::deployments::get_non_terminal(db).await {
        Ok(d) => d,
        Err(e) => return RpResponse::err("DatabaseError", e.to_string()),
    };

    let recent_deps = match crate::store::deployments::list_terminal_last_24h(db, 20).await {
        Ok(d) => d,
        Err(e) => return RpResponse::err("DatabaseError", e.to_string()),
    };

    let (total_24h, successful_24h, failed_24h) = crate::store::deployments::stats_last_24h(db)
        .await
        .unwrap_or_default();

    // Cache service+project name lookups.
    let mut svc_cache: HashMap<String, (String, String)> = HashMap::new();
    let mut proj_cache: HashMap<String, String> = HashMap::new();

    let resolve = |svc_cache: &mut HashMap<String, (String, String)>,
                   _proj_cache: &mut HashMap<String, String>,
                   service_id: &str|
     -> (String, String) {
        if let Some(cached) = svc_cache.get(service_id) {
            return cached.clone();
        }
        (service_id.to_string(), String::new())
    };
    let _ = resolve; // avoid unused warning — we resolve inline below

    async fn resolve_names(
        state: &AppState,
        service_id: &str,
        svc_cache: &mut HashMap<String, (String, String)>,
        proj_cache: &mut HashMap<String, String>,
    ) -> (String, String) {
        if let Some(cached) = svc_cache.get(service_id) {
            return cached.clone();
        }
        let (sname, pid) = match crate::store::services::get(&state.db, service_id).await {
            Ok(Some(s)) => (s.spec.name, s.spec.project_id),
            _ => (service_id.to_string(), String::new()),
        };
        let pname = if pid.is_empty() {
            String::new()
        } else if let Some(p) = proj_cache.get(&pid) {
            p.clone()
        } else {
            let name = match crate::store::projects::get(&state.db, &pid).await {
                Ok(Some(p)) => p.name,
                _ => pid.clone(),
            };
            proj_cache.insert(pid.clone(), name.clone());
            name
        };
        svc_cache.insert(service_id.to_string(), (sname.clone(), pname.clone()));
        (sname, pname)
    }

    let now = Utc::now();

    let mut active = Vec::with_capacity(active_deps.len());
    for dep in active_deps {
        let (service_name, project_name) =
            resolve_names(&state, &dep.service_id, &mut svc_cache, &mut proj_cache).await;

        let elapsed_secs = (now - dep.started_at).num_seconds().max(0) as u64;

        let current_state_secs = dep
            .states_log
            .last()
            .map(|t| (now - t.at).num_seconds().max(0) as u64)
            .unwrap_or(elapsed_secs);

        active.push(ActiveDeployInfo {
            deployment_id: dep.id,
            service_id: dep.service_id,
            service_name,
            project_name,
            state: dep.state,
            started_at: dep.started_at,
            elapsed_secs,
            current_state_secs,
            states: dep.states_log.clone(),
        });
    }

    let mut recent = Vec::with_capacity(recent_deps.len());
    for dep in recent_deps {
        let (service_name, project_name) =
            resolve_names(&state, &dep.service_id, &mut svc_cache, &mut proj_cache).await;

        let elapsed_secs = dep
            .finished_at
            .map(|f| (f - dep.started_at).num_seconds().max(0) as u64)
            .unwrap_or_else(|| (now - dep.started_at).num_seconds().max(0) as u64);

        let current_state_secs = (now - dep.started_at).num_seconds().max(0) as u64;

        recent.push(ActiveDeployInfo {
            deployment_id: dep.id,
            service_id: dep.service_id,
            service_name,
            project_name,
            state: dep.state,
            started_at: dep.started_at,
            elapsed_secs,
            current_state_secs,
            states: dep.states_log.clone(),
        });
    }

    // Separa os que estão só esperando na fila (Pending, presentes na
    // DeployQueue) dos que realmente estão em execução. `queued` sai na ordem da
    // fila (o primeiro é o próximo a rodar), não na ordem do banco.
    let (_running, queued_ids, paused) = state.deploy_queue.snapshot();
    let queued_set: std::collections::HashSet<&str> =
        queued_ids.iter().map(|s| s.as_str()).collect();

    let mut running_active = Vec::with_capacity(active.len());
    let mut queued_by_id: HashMap<String, ActiveDeployInfo> = HashMap::new();
    for info in active {
        if queued_set.contains(info.deployment_id.as_str()) {
            queued_by_id.insert(info.deployment_id.clone(), info);
        } else {
            running_active.push(info);
        }
    }
    let queued: Vec<ActiveDeployInfo> = queued_ids
        .iter()
        .filter_map(|id| queued_by_id.remove(id))
        .collect();

    RpResponse::DeployEngineStatus(Box::new(DeployEngineSummary {
        version: env!("CARGO_PKG_VERSION").to_string(),
        uptime_secs: state.started_at.elapsed().as_secs(),
        active: running_active,
        recent,
        total_24h,
        successful_24h,
        failed_24h,
        queued,
        paused,
    }))
}