flow_server/routes/
tasks.rs1use 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
18pub 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
71pub 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 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
113pub 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 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}