1use axum::{
2 extract::{Path, Query, State},
3 http::StatusCode,
4 response::{IntoResponse, Json},
5};
6use serde_json::json;
7
8use super::models::*;
9use super::server::AppState;
10use crate::{
11 events::EventManager, search::SearchManager, tasks::TaskManager, workspace::WorkspaceManager,
12};
13
14pub async fn list_tasks(
16 State(state): State<AppState>,
17 Query(query): Query<TaskListQuery>,
18) -> impl IntoResponse {
19 let db_pool = state.current_project.read().await.db_pool.clone();
20 let task_mgr = TaskManager::new(&db_pool);
21
22 let parent_filter = query.parent.as_deref().map(|p| {
24 if p == "null" {
25 None
26 } else {
27 p.parse::<i64>().ok()
28 }
29 });
30
31 match task_mgr
32 .find_tasks(query.status.as_deref(), parent_filter)
33 .await
34 {
35 Ok(tasks) => (StatusCode::OK, Json(ApiResponse { data: tasks })).into_response(),
36 Err(e) => (
37 StatusCode::INTERNAL_SERVER_ERROR,
38 Json(ApiError {
39 code: "DATABASE_ERROR".to_string(),
40 message: format!("Failed to list tasks: {}", e),
41 details: None,
42 }),
43 )
44 .into_response(),
45 }
46}
47
48pub async fn get_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
50 let db_pool = state.current_project.read().await.db_pool.clone();
51 let task_mgr = TaskManager::new(&db_pool);
52
53 match task_mgr.get_task(id).await {
54 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
55 Err(e) if e.to_string().contains("not found") => (
56 StatusCode::NOT_FOUND,
57 Json(ApiError {
58 code: "TASK_NOT_FOUND".to_string(),
59 message: format!("Task {} not found", id),
60 details: None,
61 }),
62 )
63 .into_response(),
64 Err(e) => (
65 StatusCode::INTERNAL_SERVER_ERROR,
66 Json(ApiError {
67 code: "DATABASE_ERROR".to_string(),
68 message: format!("Failed to get task: {}", e),
69 details: None,
70 }),
71 )
72 .into_response(),
73 }
74}
75
76pub async fn create_task(
78 State(state): State<AppState>,
79 Json(req): Json<CreateTaskRequest>,
80) -> impl IntoResponse {
81 let db_pool = state.current_project.read().await.db_pool.clone();
82 let task_mgr = TaskManager::new(&db_pool);
83
84 let result = task_mgr
86 .add_task(&req.name, req.spec.as_deref(), req.parent_id)
87 .await;
88
89 match result {
90 Ok(mut task) => {
91 if let Some(priority) = req.priority {
93 if let Ok(updated_task) = task_mgr
94 .update_task(task.id, None, None, None, None, None, Some(priority))
95 .await
96 {
97 task = updated_task;
98 }
99 }
101 (StatusCode::CREATED, Json(ApiResponse { data: task })).into_response()
102 },
103 Err(e) => (
104 StatusCode::BAD_REQUEST,
105 Json(ApiError {
106 code: "INVALID_REQUEST".to_string(),
107 message: format!("Failed to create task: {}", e),
108 details: None,
109 }),
110 )
111 .into_response(),
112 }
113}
114
115pub async fn update_task(
117 State(state): State<AppState>,
118 Path(id): Path<i64>,
119 Json(req): Json<UpdateTaskRequest>,
120) -> impl IntoResponse {
121 let db_pool = state.current_project.read().await.db_pool.clone();
122 let task_mgr = TaskManager::new(&db_pool);
123
124 match task_mgr.get_task(id).await {
126 Err(e) if e.to_string().contains("not found") => {
127 return (
128 StatusCode::NOT_FOUND,
129 Json(ApiError {
130 code: "TASK_NOT_FOUND".to_string(),
131 message: format!("Task {} not found", id),
132 details: None,
133 }),
134 )
135 .into_response()
136 },
137 Err(e) => {
138 return (
139 StatusCode::INTERNAL_SERVER_ERROR,
140 Json(ApiError {
141 code: "DATABASE_ERROR".to_string(),
142 message: format!("Database error: {}", e),
143 details: None,
144 }),
145 )
146 .into_response()
147 },
148 Ok(_) => {},
149 }
150
151 match task_mgr
154 .update_task(
155 id,
156 req.name.as_deref(),
157 req.spec.as_deref(),
158 None, req.status.as_deref(),
160 None, req.priority,
162 )
163 .await
164 {
165 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
166 Err(e) => (
167 StatusCode::BAD_REQUEST,
168 Json(ApiError {
169 code: "INVALID_REQUEST".to_string(),
170 message: format!("Failed to update task: {}", e),
171 details: None,
172 }),
173 )
174 .into_response(),
175 }
176}
177
178pub async fn delete_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
180 let db_pool = state.current_project.read().await.db_pool.clone();
181 let task_mgr = TaskManager::new(&db_pool);
182
183 match task_mgr.delete_task(id).await {
184 Ok(_) => (StatusCode::NO_CONTENT).into_response(),
185 Err(e) if e.to_string().contains("not found") => (
186 StatusCode::NOT_FOUND,
187 Json(ApiError {
188 code: "TASK_NOT_FOUND".to_string(),
189 message: format!("Task {} not found", id),
190 details: None,
191 }),
192 )
193 .into_response(),
194 Err(e) => (
195 StatusCode::BAD_REQUEST,
196 Json(ApiError {
197 code: "INVALID_REQUEST".to_string(),
198 message: format!("Failed to delete task: {}", e),
199 details: None,
200 }),
201 )
202 .into_response(),
203 }
204}
205
206pub async fn start_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
208 let db_pool = state.current_project.read().await.db_pool.clone();
209 let task_mgr = TaskManager::new(&db_pool);
210
211 match task_mgr.start_task(id, false).await {
212 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
213 Err(e) if e.to_string().contains("not found") => (
214 StatusCode::NOT_FOUND,
215 Json(ApiError {
216 code: "TASK_NOT_FOUND".to_string(),
217 message: format!("Task {} not found", id),
218 details: None,
219 }),
220 )
221 .into_response(),
222 Err(e) => (
223 StatusCode::BAD_REQUEST,
224 Json(ApiError {
225 code: "INVALID_REQUEST".to_string(),
226 message: format!("Failed to start task: {}", e),
227 details: None,
228 }),
229 )
230 .into_response(),
231 }
232}
233
234pub async fn done_task(State(state): State<AppState>) -> impl IntoResponse {
236 let db_pool = state.current_project.read().await.db_pool.clone();
237 let task_mgr = TaskManager::new(&db_pool);
238
239 match task_mgr.done_task().await {
240 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
241 Err(e) if e.to_string().contains("No current task") => (
242 StatusCode::BAD_REQUEST,
243 Json(ApiError {
244 code: "NO_CURRENT_TASK".to_string(),
245 message: "No current task to complete".to_string(),
246 details: None,
247 }),
248 )
249 .into_response(),
250 Err(e) => (
251 StatusCode::BAD_REQUEST,
252 Json(ApiError {
253 code: "INVALID_REQUEST".to_string(),
254 message: format!("Failed to complete task: {}", e),
255 details: None,
256 }),
257 )
258 .into_response(),
259 }
260}
261
262pub async fn spawn_subtask(
265 State(state): State<AppState>,
266 Path(_parent_id): Path<i64>, Json(req): Json<SpawnSubtaskRequest>,
268) -> impl IntoResponse {
269 let db_pool = state.current_project.read().await.db_pool.clone();
270 let task_mgr = TaskManager::new(&db_pool);
271
272 match task_mgr.spawn_subtask(&req.name, req.spec.as_deref()).await {
274 Ok(response) => (StatusCode::CREATED, Json(ApiResponse { data: response })).into_response(),
275 Err(e) if e.to_string().contains("No current task") => (
276 StatusCode::BAD_REQUEST,
277 Json(ApiError {
278 code: "NO_CURRENT_TASK".to_string(),
279 message: "No current task to spawn subtask from".to_string(),
280 details: None,
281 }),
282 )
283 .into_response(),
284 Err(e) => (
285 StatusCode::BAD_REQUEST,
286 Json(ApiError {
287 code: "INVALID_REQUEST".to_string(),
288 message: format!("Failed to spawn subtask: {}", e),
289 details: None,
290 }),
291 )
292 .into_response(),
293 }
294}
295
296pub async fn get_current_task(State(state): State<AppState>) -> impl IntoResponse {
298 let db_pool = state.current_project.read().await.db_pool.clone();
299 let workspace_mgr = WorkspaceManager::new(&db_pool);
300
301 match workspace_mgr.get_current_task().await {
302 Ok(response) => {
303 if response.task.is_some() {
304 (StatusCode::OK, Json(ApiResponse { data: response })).into_response()
305 } else {
306 (
307 StatusCode::OK,
308 Json(json!({
309 "data": null,
310 "message": "No current task"
311 })),
312 )
313 .into_response()
314 }
315 },
316 Err(e) => (
317 StatusCode::INTERNAL_SERVER_ERROR,
318 Json(ApiError {
319 code: "DATABASE_ERROR".to_string(),
320 message: format!("Failed to get current task: {}", e),
321 details: None,
322 }),
323 )
324 .into_response(),
325 }
326}
327
328pub async fn pick_next_task(State(state): State<AppState>) -> impl IntoResponse {
330 let db_pool = state.current_project.read().await.db_pool.clone();
331 let task_mgr = TaskManager::new(&db_pool);
332
333 match task_mgr.pick_next().await {
334 Ok(response) => (StatusCode::OK, Json(ApiResponse { data: response })).into_response(),
335 Err(e) => (
336 StatusCode::INTERNAL_SERVER_ERROR,
337 Json(ApiError {
338 code: "DATABASE_ERROR".to_string(),
339 message: format!("Failed to pick next task: {}", e),
340 details: None,
341 }),
342 )
343 .into_response(),
344 }
345}
346
347pub async fn list_events(
349 State(state): State<AppState>,
350 Path(task_id): Path<i64>,
351 Query(query): Query<EventListQuery>,
352) -> impl IntoResponse {
353 let db_pool = state.current_project.read().await.db_pool.clone();
354 let event_mgr = EventManager::new(&db_pool);
355
356 match event_mgr
358 .list_events(
359 Some(task_id),
360 query.limit.map(|l| l as i64),
361 query.event_type,
362 query.since,
363 )
364 .await
365 {
366 Ok(events) => (StatusCode::OK, Json(ApiResponse { data: events })).into_response(),
367 Err(e) => (
368 StatusCode::INTERNAL_SERVER_ERROR,
369 Json(ApiError {
370 code: "DATABASE_ERROR".to_string(),
371 message: format!("Failed to list events: {}", e),
372 details: None,
373 }),
374 )
375 .into_response(),
376 }
377}
378
379pub async fn create_event(
381 State(state): State<AppState>,
382 Path(task_id): Path<i64>,
383 Json(req): Json<CreateEventRequest>,
384) -> impl IntoResponse {
385 let db_pool = state.current_project.read().await.db_pool.clone();
386 let event_mgr = EventManager::new(&db_pool);
387
388 if !["decision", "blocker", "milestone", "note"].contains(&req.event_type.as_str()) {
390 return (
391 StatusCode::BAD_REQUEST,
392 Json(ApiError {
393 code: "INVALID_REQUEST".to_string(),
394 message: format!("Invalid event type: {}", req.event_type),
395 details: None,
396 }),
397 )
398 .into_response();
399 }
400
401 match event_mgr
403 .add_event(task_id, &req.event_type, &req.data)
404 .await
405 {
406 Ok(event) => (StatusCode::CREATED, Json(ApiResponse { data: event })).into_response(),
407 Err(e) => (
408 StatusCode::BAD_REQUEST,
409 Json(ApiError {
410 code: "INVALID_REQUEST".to_string(),
411 message: format!("Failed to create event: {}", e),
412 details: None,
413 }),
414 )
415 .into_response(),
416 }
417}
418
419pub async fn search(
421 State(state): State<AppState>,
422 Query(query): Query<SearchQuery>,
423) -> impl IntoResponse {
424 let db_pool = state.current_project.read().await.db_pool.clone();
425 let search_mgr = SearchManager::new(&db_pool);
426
427 match search_mgr
428 .unified_search(
429 &query.query,
430 query.include_tasks,
431 query.include_events,
432 query.limit.map(|l| l as i64),
433 )
434 .await
435 {
436 Ok(results) => (StatusCode::OK, Json(ApiResponse { data: results })).into_response(),
437 Err(e) => (
438 StatusCode::INTERNAL_SERVER_ERROR,
439 Json(ApiError {
440 code: "DATABASE_ERROR".to_string(),
441 message: format!("Search failed: {}", e),
442 details: None,
443 }),
444 )
445 .into_response(),
446 }
447}
448
449pub async fn list_projects(State(state): State<AppState>) -> impl IntoResponse {
451 let projects_info = {
453 let current_project = state.current_project.read().await;
454 state
455 .ws_state
456 .get_online_projects_with_current(
457 ¤t_project.project_name,
458 ¤t_project.project_path,
459 ¤t_project.db_path,
460 state.port,
461 )
462 .await
463 };
464
465 let port = state.port;
467 let pid = std::process::id();
468
469 let projects: Vec<serde_json::Value> = projects_info
470 .iter()
471 .map(|proj| {
472 json!({
473 "name": proj.name,
474 "path": proj.path,
475 "port": port,
476 "pid": pid,
477 "url": format!("http://127.0.0.1:{}", port),
478 "started_at": chrono::Utc::now().to_rfc3339(),
479 "mcp_connected": proj.mcp_connected,
480 "is_online": proj.is_online, "mcp_agent": proj.agent,
482 "mcp_last_seen": if proj.mcp_connected {
483 Some(chrono::Utc::now().to_rfc3339())
484 } else {
485 None::<String>
486 },
487 })
488 })
489 .collect();
490
491 (StatusCode::OK, Json(ApiResponse { data: projects })).into_response()
492}
493
494pub async fn switch_project(
496 State(state): State<AppState>,
497 Json(req): Json<SwitchProjectRequest>,
498) -> impl IntoResponse {
499 use super::server::ProjectContext;
500 use sqlx::SqlitePool;
501 use std::path::PathBuf;
502
503 let project_path = PathBuf::from(&req.project_path);
505
506 if !project_path.exists() {
507 return (
508 StatusCode::NOT_FOUND,
509 Json(ApiError {
510 code: "PROJECT_NOT_FOUND".to_string(),
511 message: format!("Project path does not exist: {}", project_path.display()),
512 details: None,
513 }),
514 )
515 .into_response();
516 }
517
518 let db_path = project_path.join(".intent-engine").join("project.db");
520
521 if !db_path.exists() {
522 return (
523 StatusCode::NOT_FOUND,
524 Json(ApiError {
525 code: "DATABASE_NOT_FOUND".to_string(),
526 message: format!(
527 "Database not found at {}. Is this an Intent-Engine project?",
528 db_path.display()
529 ),
530 details: None,
531 }),
532 )
533 .into_response();
534 }
535
536 let db_url = format!("sqlite://{}", db_path.display());
538 let new_db_pool = match SqlitePool::connect(&db_url).await {
539 Ok(pool) => pool,
540 Err(e) => {
541 return (
542 StatusCode::INTERNAL_SERVER_ERROR,
543 Json(ApiError {
544 code: "DATABASE_CONNECTION_ERROR".to_string(),
545 message: format!("Failed to connect to database: {}", e),
546 details: None,
547 }),
548 )
549 .into_response();
550 },
551 };
552
553 let project_name = project_path
555 .file_name()
556 .and_then(|n| n.to_str())
557 .unwrap_or("unknown")
558 .to_string();
559
560 let new_context = ProjectContext {
562 db_pool: new_db_pool,
563 project_name: project_name.clone(),
564 project_path: project_path.clone(),
565 db_path: db_path.clone(),
566 };
567
568 {
570 let mut current = state.current_project.write().await;
571 *current = new_context;
572 }
573
574 tracing::info!(
575 "Switched to project: {} at {}",
576 project_name,
577 project_path.display()
578 );
579
580 (
581 StatusCode::OK,
582 Json(ApiResponse {
583 data: json!({
584 "success": true,
585 "project_name": project_name,
586 "project_path": project_path.display().to_string(),
587 "database": db_path.display().to_string(),
588 }),
589 }),
590 )
591 .into_response()
592}