use std::collections::HashSet;
use std::sync::Arc;
use axum::{
Json,
extract::{Path as AxumPath, State},
http::StatusCode,
response::IntoResponse,
};
use chrono::{DateTime, Utc};
use serde::Serialize;
use tracing::warn;
use crate::daemon::state::DaemonState;
use crate::deliverable::{
Deliverable, DeliverableId, DeliverableStatus, Milestone, MilestoneStatus,
};
use crate::project::{Project, ProjectStoreError, fleet_by_project};
use crate::session_manager::{ManagedSessionState, SessionRecord};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SessionStateCounts {
pub provisioning: usize,
pub active: usize,
pub stopped: usize,
pub errored: usize,
pub decommissioned: usize,
pub total: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProjectConfigFlags {
pub gh_user_set: bool,
pub github_binding_set: bool,
pub gh_account_set: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DeliverableStatusCounts {
pub proposed: usize,
pub in_progress: usize,
pub blocked: usize,
pub complete: usize,
pub delivered: usize,
pub shipped: usize,
pub total: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MilestoneStatusCounts {
pub proposed: usize,
pub in_progress: usize,
pub complete: usize,
pub shipped: usize,
pub total: usize,
pub dangling_deliverable_refs: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProjectStatusResponse {
pub project_name: String,
pub repo_url: String,
pub sessions: SessionStateCounts,
pub last_activity_at: Option<DateTime<Utc>>,
pub config: ProjectConfigFlags,
pub deliverables: DeliverableStatusCounts,
pub milestones: MilestoneStatusCounts,
}
pub fn aggregate_project_status(
project: &Project,
all_sessions: &[SessionRecord],
all_deliverables: &[Deliverable],
all_milestones: &[Milestone],
) -> ProjectStatusResponse {
let bound: Vec<SessionRecord> = fleet_by_project(all_sessions, std::slice::from_ref(project))
.into_iter()
.next()
.map(|fleet| fleet.sessions)
.unwrap_or_default();
let mut counts = SessionStateCounts {
provisioning: 0,
active: 0,
stopped: 0,
errored: 0,
decommissioned: 0,
total: 0,
};
let mut last_activity_at: Option<DateTime<Utc>> = None;
for session in &bound {
match session.state {
ManagedSessionState::Provisioning => counts.provisioning += 1,
ManagedSessionState::Active => counts.active += 1,
ManagedSessionState::Stopped => counts.stopped += 1,
ManagedSessionState::Errored => counts.errored += 1,
ManagedSessionState::Decommissioned | ManagedSessionState::Deleted => {
counts.decommissioned += 1
}
}
counts.total += 1;
if let Some(ts) = session.last_activity_at {
last_activity_at = Some(match last_activity_at {
Some(current) => current.max(ts),
None => ts,
});
}
}
let project_deliverables: Vec<&Deliverable> = all_deliverables
.iter()
.filter(|d| d.project_name == project.name)
.collect();
let project_milestones: Vec<&Milestone> = all_milestones
.iter()
.filter(|m| m.project_name == project.name)
.collect();
let project_deliverable_ids: HashSet<DeliverableId> =
project_deliverables.iter().map(|d| d.id).collect();
ProjectStatusResponse {
project_name: project.name.clone(),
repo_url: project.repo_url.clone(),
sessions: counts,
last_activity_at,
config: ProjectConfigFlags {
gh_user_set: project.gh_user.is_some(),
github_binding_set: project.github.is_some(),
gh_account_set: project.gh_account.is_some(),
},
deliverables: count_deliverable_statuses(&project_deliverables),
milestones: count_milestone_statuses(&project_milestones, &project_deliverable_ids),
}
}
fn count_deliverable_statuses(deliverables: &[&Deliverable]) -> DeliverableStatusCounts {
let mut counts = DeliverableStatusCounts {
proposed: 0,
in_progress: 0,
blocked: 0,
complete: 0,
delivered: 0,
shipped: 0,
total: 0,
};
for d in deliverables {
match d.status {
DeliverableStatus::Proposed => counts.proposed += 1,
DeliverableStatus::InProgress => counts.in_progress += 1,
DeliverableStatus::Blocked => counts.blocked += 1,
DeliverableStatus::Complete => counts.complete += 1,
DeliverableStatus::Delivered => counts.delivered += 1,
DeliverableStatus::Shipped => counts.shipped += 1,
}
counts.total += 1;
}
counts
}
fn count_milestone_statuses(
milestones: &[&Milestone],
project_deliverable_ids: &HashSet<DeliverableId>,
) -> MilestoneStatusCounts {
let mut counts = MilestoneStatusCounts {
proposed: 0,
in_progress: 0,
complete: 0,
shipped: 0,
total: 0,
dangling_deliverable_refs: 0,
};
for m in milestones {
match m.status {
MilestoneStatus::Proposed => counts.proposed += 1,
MilestoneStatus::InProgress => counts.in_progress += 1,
MilestoneStatus::Complete => counts.complete += 1,
MilestoneStatus::Shipped => counts.shipped += 1,
}
counts.total += 1;
if m.deliverables
.iter()
.any(|id| !project_deliverable_ids.contains(id))
{
counts.dangling_deliverable_refs += 1;
}
}
counts
}
pub async fn project_status_route(
State(state): State<Arc<DaemonState>>,
AxumPath(name): AxumPath<String>,
) -> impl IntoResponse {
let registry = state.project_registry().await;
let project = match registry.get(&name).await {
Ok(project) => project,
Err(ProjectStoreError::NotFound(_)) => {
return (StatusCode::NOT_FOUND, format!("project {name} not found")).into_response();
}
Err(e) => {
warn!(
error = %e,
project = %name,
"project_status_route: project registry read failed"
);
return (
StatusCode::INTERNAL_SERVER_ERROR,
"project registry read failed".to_string(),
)
.into_response();
}
};
let mgr = state.session_manager().await;
let sessions = mgr.list().await;
let deliverable_mgr = state.deliverable_manager().await;
let deliverables = match deliverable_mgr.all_deliverables().await {
Ok(d) => d,
Err(e) => {
warn!(
error = %e,
project = %name,
"project_status_route: deliverable store read failed"
);
return (
StatusCode::INTERNAL_SERVER_ERROR,
"deliverable store read failed".to_string(),
)
.into_response();
}
};
let milestones = match deliverable_mgr.all_milestones().await {
Ok(m) => m,
Err(e) => {
warn!(
error = %e,
project = %name,
"project_status_route: milestone store read failed"
);
return (
StatusCode::INTERNAL_SERVER_ERROR,
"milestone store read failed".to_string(),
)
.into_response();
}
};
Json(aggregate_project_status(
&project,
&sessions,
&deliverables,
&milestones,
))
.into_response()
}
#[cfg(test)]
mod tests;