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