1use crate::backends::Status;
2use crate::cli::init_task_manager;
3use crate::config;
4use crate::store;
5use crate::store::TaskStatus;
6use crate::store::TaskStore;
7use crate::tmux::TmuxManager;
8use chrono::{DateTime, Duration, Utc};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12pub async fn dashboard(global: bool, project: Option<String>) -> anyhow::Result<()> {
14 if global || project.is_some() {
17 return dashboard_from_global_store(project.as_deref()).await;
18 }
19
20 let task_manager = match init_task_manager().await {
21 Ok(tm) => tm,
22 Err(e) => {
23 tracing::debug!("no project context available, falling back to global store: {e:#}");
24 return dashboard_from_global_store(None).await;
25 }
26 };
27
28 let statuses = [
29 Status::New,
30 Status::Routed,
31 Status::InProgress,
32 Status::InReview,
33 Status::Done,
34 Status::NeedsReview,
35 Status::Blocked,
36 ];
37
38 let all_external = task_manager.list_all_external_tasks().await?;
40 let all_internal = task_manager.list_all_internal().await.unwrap_or_default();
41
42 let mut counts: Vec<(Status, usize, usize)> = Vec::new(); let mut total = 0usize;
44 let mut done_tasks = Vec::new();
45 for s in &statuses {
46 let label = s.as_label();
47 let ext_filtered: Vec<_> = all_external
48 .iter()
49 .filter(|t| t.labels.contains(&label.to_string()))
50 .cloned()
51 .collect();
52 let ts = match s {
53 Status::New => TaskStatus::New,
54 Status::Routed => TaskStatus::Routed,
55 Status::InProgress => TaskStatus::InProgress,
56 Status::Done => TaskStatus::Done,
57 Status::Blocked => TaskStatus::Blocked,
58 Status::InReview => TaskStatus::InReview,
59 Status::NeedsReview => TaskStatus::NeedsReview,
60 };
61 let int_count = all_internal.iter().filter(|t| t.status == ts).count();
62 total += ext_filtered.len() + int_count;
63 counts.push((*s, ext_filtered.len(), int_count));
64 if *s == Status::Done {
65 done_tasks = ext_filtered;
66 }
67 }
68
69 println!("Tasks ({} total)", total);
70
71 for (s, ext, int) in &counts {
72 let count = ext + int;
73 if count > 0 {
74 println!(
75 " {:<12} {:>3}{}",
76 s.as_label().replace("status:", ""),
77 count,
78 if *int > 0 {
79 format!(" ({} internal)", int)
80 } else {
81 String::new()
82 }
83 );
84 }
85 }
86
87 let store: Option<Arc<TaskStore>> = crate::cli::init_store().await.ok().map(Arc::new);
88 let repo = config::get_current_repo().unwrap_or_default();
89
90 println!("\nActive Sessions");
91 let tmux = TmuxManager::new();
92 let sessions = tmux.list_sessions().await.unwrap_or_default();
93 for s in sessions.iter() {
94 let agent = store::opt_store_get_task(&store, &repo, &s.task_id)
95 .await
96 .and_then(|t| t.agent)
97 .unwrap_or_default();
98 println!(" {:<25} {:<8} #{}", s.name, agent, s.task_id);
99 }
100
101 println!("\nRecent (last 24h)");
102 let cutoff: DateTime<Utc> = Utc::now() - Duration::hours(24);
103 for r in done_tasks.iter().take(10) {
104 if let Ok(dt) = DateTime::parse_from_rfc3339(&r.updated_at) {
105 let dt_utc = dt.with_timezone(&Utc);
106 if dt_utc >= cutoff {
107 let agent = store::opt_store_get_task(&store, &repo, &r.id.0)
108 .await
109 .and_then(|t| t.agent)
110 .unwrap_or_default();
111 let elapsed = Utc::now() - dt_utc;
112 let mins = elapsed.num_minutes();
113 println!(
114 " ✅ #{:<4} {:<30} {:<8} done {:>4}m ago",
115 r.id.0, r.title, agent, mins
116 );
117 }
118 }
119 }
120
121 Ok(())
122}
123
124async fn dashboard_from_global_store(project: Option<&str>) -> anyhow::Result<()> {
126 let store = match crate::cli::init_store().await {
127 Ok(s) => Arc::new(s),
128 Err(_) => {
129 println!("Tasks (0 total)\n\nActive Sessions\n\nRecent (last 24h)");
130 return Ok(());
131 }
132 };
133
134 let mut active = store.list_all_active_global().await.unwrap_or_default();
136 let mut done = store
137 .list_all_by_status_global(TaskStatus::Done)
138 .await
139 .unwrap_or_default();
140
141 if let Some(p) = project {
143 active.retain(|t| matches_project_filter(&t.repo, p));
144 done.retain(|t| matches_project_filter(&t.repo, p));
145 }
146
147 let agent_by_ext_id: HashMap<String, String> = active
149 .iter()
150 .filter_map(|t| {
151 let ext_id = t.external_id.as_deref()?;
152 let agent = t.agent.as_deref()?;
153 Some((ext_id.to_string(), agent.to_string()))
154 })
155 .collect();
156
157 let status_order = [
159 TaskStatus::New,
160 TaskStatus::Routed,
161 TaskStatus::InProgress,
162 TaskStatus::InReview,
163 TaskStatus::NeedsReview,
164 TaskStatus::Blocked,
165 TaskStatus::Done,
166 ];
167
168 let done_count = done.len();
169 let total = active.len() + done_count;
170 println!("Tasks ({} total)", total);
171
172 for ts in &status_order {
173 let count = if *ts == TaskStatus::Done {
174 done_count
175 } else {
176 active.iter().filter(|t| t.status == *ts).count()
177 };
178 if count > 0 {
179 println!(" {:<12} {:>3}", ts.as_str().replace('_', ""), count);
180 }
181 }
182
183 println!("\nActive Sessions");
184 let tmux = TmuxManager::new();
185 let sessions = tmux.list_sessions().await.unwrap_or_default();
186 for s in sessions.iter() {
187 let agent = agent_by_ext_id
188 .get(&s.task_id)
189 .map(String::as_str)
190 .unwrap_or_default();
191 println!(" {:<25} {:<8} #{}", s.name, agent, s.task_id);
192 }
193
194 println!("\nRecent (last 24h)");
195 let cutoff: DateTime<Utc> = Utc::now() - Duration::hours(24);
196 for r in done.iter().take(10) {
197 if let Ok(dt) = DateTime::parse_from_rfc3339(&r.updated_at) {
198 let dt_utc = dt.with_timezone(&Utc);
199 if dt_utc >= cutoff {
200 let agent = r.agent.as_deref().unwrap_or_default();
201 let elapsed = Utc::now() - dt_utc;
202 let mins = elapsed.num_minutes();
203 let id_str = r.external_id.clone().unwrap_or_else(|| r.id.to_string());
204 println!(
205 " ✅ #{:<4} {:<30} {:<8} done {:>4}m ago",
206 id_str, r.title, agent, mins
207 );
208 }
209 }
210 }
211
212 Ok(())
213}
214
215fn matches_project_filter(repo: &str, project: &str) -> bool {
216 let repo_name = repo.rsplit('/').next().unwrap_or(repo);
217 repo == project || repo_name == project || repo.ends_with(&format!("/{project}"))
218}