Skip to main content

flow_server/routes/
sessions.rs

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