flow_server/routes/
sessions.rs1use crate::{error::{AppError, AppResult}, helpers::format_system_time, state::{get_metadata, AppState}};
2use axum::{
3 extract::{Path, Query, State},
4 response::Json,
5};
6use flow_core::{SessionListItem, Task};
7use serde::Deserialize;
8use std::{fs, sync::Arc, time::SystemTime};
9
10#[derive(Debug, Deserialize)]
11pub struct SessionQuery {
12 limit: Option<String>,
13}
14
15pub async fn list_sessions(
17 State(state): State<Arc<AppState>>,
18 Query(query): Query<SessionQuery>,
19) -> AppResult<Json<Vec<SessionListItem>>> {
20 let limit_str = query.limit.unwrap_or_else(|| "20".to_string());
21 let limit: Option<usize> = if limit_str == "all" {
22 None
23 } else {
24 limit_str.parse().ok()
25 };
26
27 let metadata = get_metadata(&state).await;
28 let mut sessions = Vec::new();
29
30 if state.tasks_dir.exists() {
31 let Ok(entries) = fs::read_dir(&state.tasks_dir) else {
32 return Err(AppError::Internal("Failed to read tasks directory".into()));
33 };
34
35 for entry in entries.flatten() {
36 if !entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
37 continue;
38 }
39
40 let session_id = entry.file_name().to_string_lossy().to_string();
41 let session_path = entry.path();
42
43 let Ok(dir_stat) = fs::metadata(&session_path) else {
44 continue;
45 };
46
47 let Ok(task_entries) = fs::read_dir(&session_path) else {
48 continue;
49 };
50
51 let mut completed = 0usize;
52 let mut in_progress = 0usize;
53 let mut pending = 0usize;
54 let mut task_count = 0usize;
55 let mut newest_mtime: Option<SystemTime> = None;
56
57 for task_entry in task_entries.flatten() {
58 let fname = task_entry.file_name();
59 if !fname.to_string_lossy().ends_with(".json") {
60 continue;
61 }
62
63 task_count += 1;
64 let task_path = task_entry.path();
65
66 if let Ok(content) = fs::read_to_string(&task_path) {
67 if let Ok(task) = serde_json::from_str::<Task>(&content) {
68 match task.status.as_str() {
69 "completed" => completed += 1,
70 "in_progress" => in_progress += 1,
71 _ => pending += 1,
72 }
73 }
74 }
75
76 if let Ok(task_stat) = fs::metadata(&task_path) {
77 if let Ok(mtime) = task_stat.modified() {
78 newest_mtime = Some(newest_mtime.map_or(mtime, |prev| prev.max(mtime)));
79 }
80 }
81 }
82
83 let meta = metadata.get(&session_id);
84 let modified_at = newest_mtime
85 .or_else(|| dir_stat.modified().ok())
86 .map(|t| {
87 let duration = t
88 .duration_since(SystemTime::UNIX_EPOCH)
89 .unwrap_or_default();
90 format_system_time(duration)
91 })
92 .unwrap_or_default();
93
94 sessions.push(SessionListItem {
95 id: session_id.clone(),
96 name: meta.and_then(flow_core::SessionMeta::display_name),
97 slug: meta.and_then(|m| m.slug.clone()),
98 project: meta.and_then(|m| m.project_path.clone()),
99 description: meta.and_then(|m| m.description.clone()),
100 git_branch: meta.and_then(|m| m.git_branch.clone()),
101 task_count,
102 completed,
103 in_progress,
104 pending,
105 created_at: meta.and_then(|m| m.created.clone()),
106 modified_at,
107 });
108 }
109 }
110
111 sessions.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
113
114 if let Some(limit) = limit {
116 sessions.truncate(limit);
117 }
118
119 Ok(Json(sessions))
120}
121
122pub async fn get_session(
124 State(state): State<Arc<AppState>>,
125 Path(session_id): Path<String>,
126) -> AppResult<Json<Vec<Task>>> {
127 let session_path = state.tasks_dir.join(&session_id);
128
129 if !session_path.exists() {
130 return Err(AppError::NotFound("Session not found".into()));
131 }
132
133 let Ok(entries) = fs::read_dir(&session_path) else {
134 return Err(AppError::Internal("Failed to read session directory".into()));
135 };
136
137 let mut tasks: Vec<Task> = entries
138 .flatten()
139 .filter(|e| e.file_name().to_string_lossy().ends_with(".json"))
140 .filter_map(|e| {
141 fs::read_to_string(e.path())
142 .ok()
143 .and_then(|c| serde_json::from_str(&c).ok())
144 })
145 .collect();
146
147 tasks.sort_by(|a, b| {
149 a.id.parse::<u64>()
150 .unwrap_or(0)
151 .cmp(&b.id.parse::<u64>().unwrap_or(0))
152 });
153
154 Ok(Json(tasks))
155}