use crate::backends::Status;
use crate::cli::init_task_manager;
use crate::config;
use crate::store;
use crate::store::TaskStatus;
use crate::store::TaskStore;
use crate::tmux::TmuxManager;
use chrono::{DateTime, Duration, Utc};
use std::collections::HashMap;
use std::sync::Arc;
pub async fn dashboard(global: bool, project: Option<String>) -> anyhow::Result<()> {
if global || project.is_some() {
return dashboard_from_global_store(project.as_deref()).await;
}
let task_manager = match init_task_manager().await {
Ok(tm) => tm,
Err(e) => {
tracing::debug!("no project context available, falling back to global store: {e:#}");
return dashboard_from_global_store(None).await;
}
};
let statuses = [
Status::New,
Status::Routed,
Status::InProgress,
Status::InReview,
Status::Done,
Status::NeedsReview,
Status::Blocked,
Status::Waiting,
];
let all_external = task_manager.list_all_external_tasks().await?;
let all_internal = task_manager.list_all_internal().await.unwrap_or_default();
let mut counts: Vec<(Status, usize, usize)> = Vec::new(); let mut total = 0usize;
let mut done_tasks = Vec::new();
for s in &statuses {
let label = s.as_label();
let ext_filtered: Vec<_> = all_external
.iter()
.filter(|t| t.labels.contains(&label.to_string()))
.cloned()
.collect();
let ts = match s {
Status::New => TaskStatus::New,
Status::Routed => TaskStatus::Routed,
Status::InProgress => TaskStatus::InProgress,
Status::Done => TaskStatus::Done,
Status::Blocked => TaskStatus::Blocked,
Status::InReview => TaskStatus::InReview,
Status::NeedsReview => TaskStatus::NeedsReview,
Status::Waiting => TaskStatus::Waiting,
};
let int_count = all_internal.iter().filter(|t| t.status == ts).count();
total += ext_filtered.len() + int_count;
counts.push((*s, ext_filtered.len(), int_count));
if *s == Status::Done {
done_tasks = ext_filtered;
}
}
println!("Tasks ({} total)", total);
for (s, ext, int) in &counts {
let count = ext + int;
if count > 0 {
println!(
" {:<12} {:>3}{}",
s.as_label().replace("status:", ""),
count,
if *int > 0 {
format!(" ({} internal)", int)
} else {
String::new()
}
);
}
}
let store: Option<Arc<TaskStore>> = crate::cli::init_store().await.ok().map(Arc::new);
let repo = config::get_current_repo().unwrap_or_default();
println!("\nActive Sessions");
let tmux = TmuxManager::new();
let sessions = tmux.list_sessions().await.unwrap_or_default();
for s in sessions.iter() {
let agent = store::opt_store_get_task(&store, &repo, &s.task_id)
.await
.and_then(|t| t.agent)
.unwrap_or_default();
println!(" {:<25} {:<8} #{}", s.name, agent, s.task_id);
}
println!("\nRecent (last 24h)");
let cutoff: DateTime<Utc> = Utc::now() - Duration::hours(24);
for r in done_tasks.iter().take(10) {
if let Ok(dt) = DateTime::parse_from_rfc3339(&r.updated_at) {
let dt_utc = dt.with_timezone(&Utc);
if dt_utc >= cutoff {
let agent = store::opt_store_get_task(&store, &repo, &r.id.0)
.await
.and_then(|t| t.agent)
.unwrap_or_default();
let elapsed = Utc::now() - dt_utc;
let mins = elapsed.num_minutes();
println!(
" ✅ #{:<4} {:<30} {:<8} done {:>4}m ago",
r.id.0, r.title, agent, mins
);
}
}
}
Ok(())
}
async fn dashboard_from_global_store(project: Option<&str>) -> anyhow::Result<()> {
let store = match crate::cli::init_store().await {
Ok(s) => Arc::new(s),
Err(_) => {
println!("Tasks (0 total)\n\nActive Sessions\n\nRecent (last 24h)");
return Ok(());
}
};
let mut active = store.list_all_active_global().await.unwrap_or_default();
let mut done = store
.list_all_by_status_global(TaskStatus::Done)
.await
.unwrap_or_default();
if let Some(p) = project {
active.retain(|t| matches_project_filter(&t.repo, p));
done.retain(|t| matches_project_filter(&t.repo, p));
}
let agent_by_ext_id: HashMap<String, String> = active
.iter()
.filter_map(|t| {
let ext_id = t.external_id.as_deref()?;
let agent = t.agent.as_deref()?;
Some((ext_id.to_string(), agent.to_string()))
})
.collect();
let status_order = [
TaskStatus::New,
TaskStatus::Routed,
TaskStatus::InProgress,
TaskStatus::InReview,
TaskStatus::NeedsReview,
TaskStatus::Blocked,
TaskStatus::Done,
];
let done_count = done.len();
let total = active.len() + done_count;
println!("Tasks ({} total)", total);
for ts in &status_order {
let count = if *ts == TaskStatus::Done {
done_count
} else {
active.iter().filter(|t| t.status == *ts).count()
};
if count > 0 {
println!(" {:<12} {:>3}", ts.as_str().replace('_', ""), count);
}
}
println!("\nActive Sessions");
let tmux = TmuxManager::new();
let sessions = tmux.list_sessions().await.unwrap_or_default();
for s in sessions.iter() {
let agent = agent_by_ext_id
.get(&s.task_id)
.map(String::as_str)
.unwrap_or_default();
println!(" {:<25} {:<8} #{}", s.name, agent, s.task_id);
}
println!("\nRecent (last 24h)");
let cutoff: DateTime<Utc> = Utc::now() - Duration::hours(24);
for r in done.iter().take(10) {
if let Ok(dt) = DateTime::parse_from_rfc3339(&r.updated_at) {
let dt_utc = dt.with_timezone(&Utc);
if dt_utc >= cutoff {
let agent = r.agent.as_deref().unwrap_or_default();
let elapsed = Utc::now() - dt_utc;
let mins = elapsed.num_minutes();
let id_str = r.external_id.clone().unwrap_or_else(|| r.id.to_string());
println!(
" ✅ #{:<4} {:<30} {:<8} done {:>4}m ago",
id_str, r.title, agent, mins
);
}
}
}
Ok(())
}
fn matches_project_filter(repo: &str, project: &str) -> bool {
let repo_name = repo.rsplit('/').next().unwrap_or(repo);
repo == project || repo_name == project || repo.ends_with(&format!("/{project}"))
}