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