//! Diagnostics — task tracking, bottlenecks, and performance metrics. use std::collections::HashMap; use chrono::{DateTime, Utc}; use gyandoot_core::{KanbanResult, Task, TaskStatus}; use uuid::Uuid; /// A diagnostic snapshot of the Kanban system. #[derive(Debug, Clone)] pub struct KanbanDiagnostics { pub total_tasks: usize, pub backlog_count: usize, pub in_progress_count: usize, pub done_count: usize, pub blocked_count: usize, pub average_completion_time_ms: Option, pub bottlenecks: Vec, pub timestamp: DateTime, } /// Tracks task lifecycle and produces diagnostic information. pub struct Diagnostics { task_timings: HashMap, bottlenecks: Vec, } #[derive(Debug, Clone)] struct TaskTiming { created_at: DateTime, started_at: Option>, completed_at: Option>, status_changes: Vec<(DateTime, TaskStatus)>, } impl Diagnostics { /// Create a new diagnostics tracker. pub fn new() -> Self { Self { task_timings: HashMap::new(), bottlenecks: Vec::new(), } } /// Record a task creation. pub fn record_creation(&mut self, task_id: Uuid) { self.task_timings.insert( task_id, TaskTiming { created_at: Utc::now(), started_at: None, completed_at: None, status_changes: vec![(Utc::now(), TaskStatus::Backlog)], }, ); } /// Record a task status change. pub fn record_status_change(&mut self, task_id: &Uuid, new_status: TaskStatus) { if let Some(timing) = self.task_timings.get_mut(task_id) { timing.status_changes.push((Utc::now(), new_status.clone())); if new_status == TaskStatus::InProgress && timing.started_at.is_none() { timing.started_at = Some(Utc::now()); } if new_status == TaskStatus::Done { timing.completed_at = Some(Utc::now()); } } } /// Generate a diagnostics snapshot for a set of tasks. pub fn analyze(&self, tasks: &[Task]) -> KanbanResult { let total = tasks.len(); let backlog = tasks.iter().filter(|t| t.status == TaskStatus::Backlog).count(); let in_progress = tasks.iter().filter(|t| t.status == TaskStatus::InProgress).count(); let done = tasks.iter().filter(|t| t.status == TaskStatus::Done).count(); let blocked = tasks.iter().filter(|t| t.status == TaskStatus::Blocked).count(); // Calculate average completion time let completion_times: Vec = self .task_timings .values() .filter_map(|t| { let start = t.started_at?; let end = t.completed_at?; Some((end - start).num_milliseconds() as f64) }) .collect(); let avg_completion = if completion_times.is_empty() { None } else { Some(completion_times.iter().sum::() / completion_times.len() as f64) }; Ok(KanbanDiagnostics { total_tasks: total, backlog_count: backlog, in_progress_count: in_progress, done_count: done, blocked_count: blocked, average_completion_time_ms: avg_completion, bottlenecks: self.bottlenecks.clone(), timestamp: Utc::now(), }) } /// Add a bottleneck warning. pub fn report_bottleneck(&mut self, description: String) { self.bottlenecks.push(description); } } impl Default for Diagnostics { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_diagnostics_tracking() { let mut diagnostics = Diagnostics::new(); let task_id = Uuid::new_v4(); diagnostics.record_creation(task_id); diagnostics.record_status_change(&task_id, TaskStatus::InProgress); diagnostics.record_status_change(&task_id, TaskStatus::Done); let tasks = vec![Task { id: task_id, title: "Test".to_string(), description: String::new(), priority: gyandoot_core::Priority::Low, status: TaskStatus::Done, assigned_agent: None, created_at: Utc::now(), updated_at: Utc::now(), subtasks: Vec::new(), dependencies: Vec::new(), tags: Vec::new(), }]; let report = diagnostics.analyze(&tasks).unwrap(); assert_eq!(report.total_tasks, 1); assert_eq!(report.done_count, 1); } }