Skip to main content

flow_server/routes/
tasks.rs

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