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