Skip to main content

flow_server/routes/
sessions.rs

1use 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
15/// GET /api/sessions — List all sessions with task summaries
16pub 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    // Sort by most recently modified
112    sessions.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
113
114    // Apply limit
115    if let Some(limit) = limit {
116        sessions.truncate(limit);
117    }
118
119    Ok(Json(sessions))
120}
121
122/// GET `/api/sessions/:session_id` — Get tasks for a session
123pub 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    // Sort by numeric ID
148    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}