Skip to main content

flow_server/routes/
tasks.rs

1use crate::{
2    error::{AppError, AppResult},
3    state::{get_metadata, AppState},
4};
5use axum::{
6    extract::{Path, State},
7    response::Json,
8};
9use flow_core::{Task, TaskWithSession};
10use serde::Deserialize;
11use std::{fs, sync::Arc};
12
13#[derive(Debug, Deserialize)]
14pub struct NoteRequest {
15    note: String,
16}
17
18/// GET /api/tasks/all — All tasks across all sessions
19pub async fn get_all_tasks(
20    State(state): State<Arc<AppState>>,
21) -> AppResult<Json<Vec<TaskWithSession>>> {
22    if !state.tasks_dir.exists() {
23        return Ok(Json(vec![]));
24    }
25
26    let metadata = get_metadata(&state).await;
27
28    let Ok(session_dirs) = fs::read_dir(&state.tasks_dir) else {
29        return Ok(Json(vec![]));
30    };
31
32    let mut all_tasks = Vec::new();
33
34    for session_entry in session_dirs.flatten() {
35        if !session_entry
36            .file_type()
37            .map(|ft| ft.is_dir())
38            .unwrap_or(false)
39        {
40            continue;
41        }
42
43        let session_id = session_entry.file_name().to_string_lossy().to_string();
44        let meta = metadata.get(&session_id);
45
46        let Ok(task_files) = fs::read_dir(session_entry.path()) else {
47            continue;
48        };
49
50        for task_entry in task_files.flatten() {
51            if !task_entry.file_name().to_string_lossy().ends_with(".json") {
52                continue;
53            }
54
55            if let Ok(content) = fs::read_to_string(task_entry.path()) {
56                if let Ok(task) = serde_json::from_str::<Task>(&content) {
57                    all_tasks.push(TaskWithSession {
58                        task,
59                        session_id: session_id.clone(),
60                        session_name: meta.and_then(flow_core::SessionMeta::display_name),
61                        project: meta.and_then(|m| m.project_path.clone()),
62                    });
63                }
64            }
65        }
66    }
67
68    Ok(Json(all_tasks))
69}
70
71/// POST `/api/tasks/:session_id/:task_id/note` — Add note to a task
72pub async fn add_note(
73    State(state): State<Arc<AppState>>,
74    Path((session_id, task_id)): Path<(String, String)>,
75    Json(body): Json<NoteRequest>,
76) -> AppResult<Json<serde_json::Value>> {
77    let note = body.note.trim();
78    if note.is_empty() {
79        return Err(AppError::BadRequest("Note cannot be empty".into()));
80    }
81
82    let task_path = state
83        .tasks_dir
84        .join(&session_id)
85        .join(format!("{task_id}.json"));
86
87    if !task_path.exists() {
88        return Err(AppError::NotFound("Task not found".into()));
89    }
90
91    let content = fs::read_to_string(&task_path)
92        .map_err(|e| AppError::Internal(format!("Failed to read task: {e}")))?;
93
94    let mut task: serde_json::Value = serde_json::from_str(&content)
95        .map_err(|e| AppError::Internal(format!("Failed to parse task: {e}")))?;
96
97    // Append note to description
98    let current_desc = task
99        .get("description")
100        .and_then(|v| v.as_str())
101        .unwrap_or("");
102    let note_block = format!("\n\n---\n\n#### [Note added by user]\n\n{note}");
103    task["description"] = serde_json::Value::String(format!("{current_desc}{note_block}"));
104
105    let output = serde_json::to_string_pretty(&task)
106        .map_err(|e| AppError::Internal(format!("Failed to serialize task: {e}")))?;
107    fs::write(&task_path, output)
108        .map_err(|e| AppError::Internal(format!("Failed to write task: {e}")))?;
109
110    Ok(Json(serde_json::json!({ "success": true, "task": task })))
111}
112
113/// DELETE `/api/tasks/:session_id/:task_id` — Delete a task
114pub async fn delete_task(
115    State(state): State<Arc<AppState>>,
116    Path((session_id, task_id)): Path<(String, String)>,
117) -> AppResult<Json<serde_json::Value>> {
118    let session_path = state.tasks_dir.join(&session_id);
119    let task_path = session_path.join(format!("{task_id}.json"));
120
121    if !task_path.exists() {
122        return Err(AppError::NotFound("Task not found".into()));
123    }
124
125    // Check if this task blocks other tasks
126    if let Ok(entries) = fs::read_dir(&session_path) {
127        for entry in entries.flatten() {
128            if !entry.file_name().to_string_lossy().ends_with(".json") {
129                continue;
130            }
131            if let Ok(content) = fs::read_to_string(entry.path()) {
132                if let Ok(other_task) = serde_json::from_str::<Task>(&content) {
133                    if other_task.blocked_by.contains(&task_id) {
134                        return Err(AppError::BadRequest(format!(
135                            "Cannot delete task that blocks other tasks (blocked: {})",
136                            other_task.id
137                        )));
138                    }
139                }
140            }
141        }
142    }
143
144    fs::remove_file(&task_path)
145        .map_err(|e| AppError::Internal(format!("Failed to delete task: {e}")))?;
146
147    Ok(Json(
148        serde_json::json!({ "success": true, "taskId": task_id }),
149    ))
150}