flow_server/routes/
tasks.rs1use 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
15pub 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
68pub 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 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
107pub 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 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}