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 db::models::TaskSortBy, events::EventManager, search::SearchManager, tasks::TaskManager,
12 workspace::WorkspaceManager,
13};
14
15pub async fn list_tasks(
17 State(state): State<AppState>,
18 Query(query): Query<TaskListQuery>,
19) -> impl IntoResponse {
20 let db_pool = match state.get_active_db_pool().await {
21 Ok(pool) => pool,
22 Err(e) => {
23 return (
24 StatusCode::INTERNAL_SERVER_ERROR,
25 Json(ApiError {
26 code: "DATABASE_ERROR".to_string(),
27 message: e,
28 details: None,
29 }),
30 )
31 .into_response()
32 },
33 };
34 let task_mgr = TaskManager::new(&db_pool);
35
36 let parent_filter = query.parent.as_deref().map(|p| {
38 if p == "null" {
39 None
40 } else {
41 p.parse::<i64>().ok()
42 }
43 });
44
45 let sort_by = match query.sort_by.as_deref() {
47 Some("id") => Some(TaskSortBy::Id),
48 Some("priority") => Some(TaskSortBy::Priority),
49 Some("time") => Some(TaskSortBy::Time),
50 Some("focus") => Some(TaskSortBy::FocusAware),
51 _ => Some(TaskSortBy::FocusAware), };
53
54 match task_mgr
55 .find_tasks(
56 query.status.as_deref(),
57 parent_filter,
58 sort_by,
59 query.limit,
60 query.offset,
61 )
62 .await
63 {
64 Ok(result) => (StatusCode::OK, Json(ApiResponse { data: result })).into_response(),
65 Err(e) => {
66 tracing::error!(error = %e, "Failed to fetch tasks");
67 (
68 StatusCode::INTERNAL_SERVER_ERROR,
69 Json(ApiError {
70 code: "DATABASE_ERROR".to_string(),
71 message: format!("Failed to list tasks: {}", e),
72 details: None,
73 }),
74 )
75 .into_response()
76 },
77 }
78}
79
80pub async fn get_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
82 let db_pool = match state.get_active_db_pool().await {
83 Ok(pool) => pool,
84 Err(e) => {
85 return (
86 StatusCode::INTERNAL_SERVER_ERROR,
87 Json(ApiError {
88 code: "DATABASE_ERROR".to_string(),
89 message: e,
90 details: None,
91 }),
92 )
93 .into_response()
94 },
95 };
96 let task_mgr = TaskManager::new(&db_pool);
97
98 match task_mgr.get_task(id).await {
99 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
100 Err(e) if e.to_string().contains("not found") => (
101 StatusCode::NOT_FOUND,
102 Json(ApiError {
103 code: "TASK_NOT_FOUND".to_string(),
104 message: format!("Task {} not found", id),
105 details: None,
106 }),
107 )
108 .into_response(),
109 Err(e) => (
110 StatusCode::INTERNAL_SERVER_ERROR,
111 Json(ApiError {
112 code: "DATABASE_ERROR".to_string(),
113 message: format!("Failed to get task: {}", e),
114 details: None,
115 }),
116 )
117 .into_response(),
118 }
119}
120
121pub async fn create_task(
123 State(state): State<AppState>,
124 Json(req): Json<CreateTaskRequest>,
125) -> impl IntoResponse {
126 let db_pool = match state.get_active_db_pool().await {
127 Ok(pool) => pool,
128 Err(e) => {
129 return (
130 StatusCode::INTERNAL_SERVER_ERROR,
131 Json(ApiError {
132 code: "DATABASE_ERROR".to_string(),
133 message: e,
134 details: None,
135 }),
136 )
137 .into_response()
138 },
139 };
140 let project_path = state
141 .get_active_project()
142 .await
143 .map(|p| p.path.to_string_lossy().to_string())
144 .unwrap_or_default();
145
146 let task_mgr = TaskManager::with_websocket(
147 &db_pool,
148 std::sync::Arc::new(state.ws_state.clone()),
149 project_path,
150 );
151
152 let result = task_mgr
156 .add_task(&req.name, req.spec.as_deref(), req.parent_id, None)
157 .await;
158
159 match result {
160 Ok(mut task) => {
161 if let Some(priority) = req.priority {
163 if let Ok(updated_task) = task_mgr
164 .update_task(task.id, None, None, None, None, None, Some(priority))
165 .await
166 {
167 task = updated_task;
168 }
169 }
171 (StatusCode::CREATED, Json(ApiResponse { data: task })).into_response()
172 },
173 Err(e) => (
174 StatusCode::BAD_REQUEST,
175 Json(ApiError {
176 code: "INVALID_REQUEST".to_string(),
177 message: format!("Failed to create task: {}", e),
178 details: None,
179 }),
180 )
181 .into_response(),
182 }
183}
184
185pub async fn update_task(
187 State(state): State<AppState>,
188 Path(id): Path<i64>,
189 Json(req): Json<UpdateTaskRequest>,
190) -> impl IntoResponse {
191 let db_pool = match state.get_active_db_pool().await {
192 Ok(pool) => pool,
193 Err(e) => {
194 return (
195 StatusCode::INTERNAL_SERVER_ERROR,
196 Json(ApiError {
197 code: "DATABASE_ERROR".to_string(),
198 message: e,
199 details: None,
200 }),
201 )
202 .into_response()
203 },
204 };
205 let project_path = state
206 .get_active_project()
207 .await
208 .map(|p| p.path.to_string_lossy().to_string())
209 .unwrap_or_default();
210
211 let task_mgr = TaskManager::with_websocket(
212 &db_pool,
213 std::sync::Arc::new(state.ws_state.clone()),
214 project_path,
215 );
216
217 match task_mgr.get_task(id).await {
219 Err(e) if e.to_string().contains("not found") => {
220 return (
221 StatusCode::NOT_FOUND,
222 Json(ApiError {
223 code: "TASK_NOT_FOUND".to_string(),
224 message: format!("Task {} not found", id),
225 details: None,
226 }),
227 )
228 .into_response()
229 },
230 Err(e) => {
231 return (
232 StatusCode::INTERNAL_SERVER_ERROR,
233 Json(ApiError {
234 code: "DATABASE_ERROR".to_string(),
235 message: format!("Database error: {}", e),
236 details: None,
237 }),
238 )
239 .into_response()
240 },
241 Ok(_) => {},
242 }
243
244 match task_mgr
247 .update_task(
248 id,
249 req.name.as_deref(),
250 req.spec.as_deref(),
251 None, req.status.as_deref(),
253 None, req.priority,
255 )
256 .await
257 {
258 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
259 Err(e) => (
260 StatusCode::BAD_REQUEST,
261 Json(ApiError {
262 code: "INVALID_REQUEST".to_string(),
263 message: format!("Failed to update task: {}", e),
264 details: None,
265 }),
266 )
267 .into_response(),
268 }
269}
270
271pub async fn delete_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
273 let db_pool = match state.get_active_db_pool().await {
274 Ok(pool) => pool,
275 Err(e) => {
276 return (
277 StatusCode::INTERNAL_SERVER_ERROR,
278 Json(ApiError {
279 code: "DATABASE_ERROR".to_string(),
280 message: e,
281 details: None,
282 }),
283 )
284 .into_response()
285 },
286 };
287 let project_path = state
288 .get_active_project()
289 .await
290 .map(|p| p.path.to_string_lossy().to_string())
291 .unwrap_or_default();
292
293 let task_mgr = TaskManager::with_websocket(
294 &db_pool,
295 std::sync::Arc::new(state.ws_state.clone()),
296 project_path,
297 );
298
299 match task_mgr.delete_task(id).await {
300 Ok(_) => (StatusCode::NO_CONTENT).into_response(),
301 Err(e) if e.to_string().contains("not found") => (
302 StatusCode::NOT_FOUND,
303 Json(ApiError {
304 code: "TASK_NOT_FOUND".to_string(),
305 message: format!("Task {} not found", id),
306 details: None,
307 }),
308 )
309 .into_response(),
310 Err(e) => (
311 StatusCode::BAD_REQUEST,
312 Json(ApiError {
313 code: "INVALID_REQUEST".to_string(),
314 message: format!("Failed to delete task: {}", e),
315 details: None,
316 }),
317 )
318 .into_response(),
319 }
320}
321
322pub async fn start_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
324 let db_pool = match state.get_active_db_pool().await {
325 Ok(pool) => pool,
326 Err(e) => {
327 return (
328 StatusCode::INTERNAL_SERVER_ERROR,
329 Json(ApiError {
330 code: "DATABASE_ERROR".to_string(),
331 message: e,
332 details: None,
333 }),
334 )
335 .into_response()
336 },
337 };
338 let project_path = state
339 .get_active_project()
340 .await
341 .map(|p| p.path.to_string_lossy().to_string())
342 .unwrap_or_default();
343
344 let task_mgr = TaskManager::with_websocket(
345 &db_pool,
346 std::sync::Arc::new(state.ws_state.clone()),
347 project_path,
348 );
349
350 match task_mgr.start_task(id, false).await {
351 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
352 Err(e) if e.to_string().contains("not found") => (
353 StatusCode::NOT_FOUND,
354 Json(ApiError {
355 code: "TASK_NOT_FOUND".to_string(),
356 message: format!("Task {} not found", id),
357 details: None,
358 }),
359 )
360 .into_response(),
361 Err(e) => (
362 StatusCode::BAD_REQUEST,
363 Json(ApiError {
364 code: "INVALID_REQUEST".to_string(),
365 message: format!("Failed to start task: {}", e),
366 details: None,
367 }),
368 )
369 .into_response(),
370 }
371}
372
373pub async fn done_task(State(state): State<AppState>) -> impl IntoResponse {
375 let db_pool = match state.get_active_db_pool().await {
376 Ok(pool) => pool,
377 Err(e) => {
378 return (
379 StatusCode::INTERNAL_SERVER_ERROR,
380 Json(ApiError {
381 code: "DATABASE_ERROR".to_string(),
382 message: e,
383 details: None,
384 }),
385 )
386 .into_response()
387 },
388 };
389 let project_path = state
390 .get_active_project()
391 .await
392 .map(|p| p.path.to_string_lossy().to_string())
393 .unwrap_or_default();
394
395 let task_mgr = TaskManager::with_websocket(
396 &db_pool,
397 std::sync::Arc::new(state.ws_state.clone()),
398 project_path,
399 );
400
401 match task_mgr.done_task(false).await {
403 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
404 Err(e) if e.to_string().contains("No current task") => (
405 StatusCode::BAD_REQUEST,
406 Json(ApiError {
407 code: "NO_CURRENT_TASK".to_string(),
408 message: "No current task to complete".to_string(),
409 details: None,
410 }),
411 )
412 .into_response(),
413 Err(e) => (
414 StatusCode::BAD_REQUEST,
415 Json(ApiError {
416 code: "INVALID_REQUEST".to_string(),
417 message: format!("Failed to complete task: {}", e),
418 details: None,
419 }),
420 )
421 .into_response(),
422 }
423}
424
425pub async fn spawn_subtask(
428 State(state): State<AppState>,
429 Path(_parent_id): Path<i64>, Json(req): Json<SpawnSubtaskRequest>,
431) -> impl IntoResponse {
432 let (db_pool, project_path) = match state.get_active_project_context().await {
433 Ok(ctx) => ctx,
434 Err(e) => {
435 return (
436 StatusCode::INTERNAL_SERVER_ERROR,
437 Json(ApiError {
438 code: "DATABASE_ERROR".to_string(),
439 message: e,
440 details: None,
441 }),
442 )
443 .into_response()
444 },
445 };
446
447 let task_mgr = TaskManager::with_websocket(
448 &db_pool,
449 std::sync::Arc::new(state.ws_state.clone()),
450 project_path,
451 );
452
453 match task_mgr.spawn_subtask(&req.name, req.spec.as_deref()).await {
455 Ok(response) => (StatusCode::CREATED, Json(ApiResponse { data: response })).into_response(),
456 Err(e) if e.to_string().contains("No current task") => (
457 StatusCode::BAD_REQUEST,
458 Json(ApiError {
459 code: "NO_CURRENT_TASK".to_string(),
460 message: "No current task to spawn subtask from".to_string(),
461 details: None,
462 }),
463 )
464 .into_response(),
465 Err(e) => (
466 StatusCode::BAD_REQUEST,
467 Json(ApiError {
468 code: "INVALID_REQUEST".to_string(),
469 message: format!("Failed to spawn subtask: {}", e),
470 details: None,
471 }),
472 )
473 .into_response(),
474 }
475}
476
477pub async fn get_current_task(State(state): State<AppState>) -> impl IntoResponse {
479 let db_pool = match state.get_active_db_pool().await {
480 Ok(pool) => pool,
481 Err(e) => {
482 return (
483 StatusCode::INTERNAL_SERVER_ERROR,
484 Json(ApiError {
485 code: "DATABASE_ERROR".to_string(),
486 message: e,
487 details: None,
488 }),
489 )
490 .into_response()
491 },
492 };
493 let workspace_mgr = WorkspaceManager::new(&db_pool);
494
495 match workspace_mgr.get_current_task(None).await {
496 Ok(response) => {
497 if response.task.is_some() {
498 (StatusCode::OK, Json(ApiResponse { data: response })).into_response()
499 } else {
500 (
501 StatusCode::OK,
502 Json(json!({
503 "data": null,
504 "message": "No current task"
505 })),
506 )
507 .into_response()
508 }
509 },
510 Err(e) => (
511 StatusCode::INTERNAL_SERVER_ERROR,
512 Json(ApiError {
513 code: "DATABASE_ERROR".to_string(),
514 message: format!("Failed to get current task: {}", e),
515 details: None,
516 }),
517 )
518 .into_response(),
519 }
520}
521
522pub async fn pick_next_task(State(state): State<AppState>) -> impl IntoResponse {
524 let db_pool = match state.get_active_db_pool().await {
525 Ok(pool) => pool,
526 Err(e) => {
527 return (
528 StatusCode::INTERNAL_SERVER_ERROR,
529 Json(ApiError {
530 code: "DATABASE_ERROR".to_string(),
531 message: e,
532 details: None,
533 }),
534 )
535 .into_response()
536 },
537 };
538 let task_mgr = TaskManager::new(&db_pool);
539
540 match task_mgr.pick_next().await {
541 Ok(response) => (StatusCode::OK, Json(ApiResponse { data: response })).into_response(),
542 Err(e) => (
543 StatusCode::INTERNAL_SERVER_ERROR,
544 Json(ApiError {
545 code: "DATABASE_ERROR".to_string(),
546 message: format!("Failed to pick next task: {}", e),
547 details: None,
548 }),
549 )
550 .into_response(),
551 }
552}
553
554pub async fn list_events(
556 State(state): State<AppState>,
557 Path(task_id): Path<i64>,
558 Query(query): Query<EventListQuery>,
559) -> impl IntoResponse {
560 let db_pool = match state.get_active_db_pool().await {
561 Ok(pool) => pool,
562 Err(e) => {
563 return (
564 StatusCode::INTERNAL_SERVER_ERROR,
565 Json(ApiError {
566 code: "DATABASE_ERROR".to_string(),
567 message: e,
568 details: None,
569 }),
570 )
571 .into_response()
572 },
573 };
574 let event_mgr = EventManager::new(&db_pool);
575
576 match event_mgr
578 .list_events(
579 Some(task_id),
580 query.limit.map(|l| l as i64),
581 query.event_type,
582 query.since,
583 )
584 .await
585 {
586 Ok(events) => (StatusCode::OK, Json(ApiResponse { data: events })).into_response(),
587 Err(e) => (
588 StatusCode::INTERNAL_SERVER_ERROR,
589 Json(ApiError {
590 code: "DATABASE_ERROR".to_string(),
591 message: format!("Failed to list events: {}", e),
592 details: None,
593 }),
594 )
595 .into_response(),
596 }
597}
598
599pub async fn create_event(
601 State(state): State<AppState>,
602 Path(task_id): Path<i64>,
603 Json(req): Json<CreateEventRequest>,
604) -> impl IntoResponse {
605 let (db_pool, project_path) = match state.get_active_project_context().await {
606 Ok(ctx) => ctx,
607 Err(e) => {
608 return (
609 StatusCode::INTERNAL_SERVER_ERROR,
610 Json(ApiError {
611 code: "DATABASE_ERROR".to_string(),
612 message: e,
613 details: None,
614 }),
615 )
616 .into_response()
617 },
618 };
619
620 let event_mgr = EventManager::with_websocket(
621 &db_pool,
622 std::sync::Arc::new(state.ws_state.clone()),
623 project_path,
624 );
625
626 if !["decision", "blocker", "milestone", "note"].contains(&req.event_type.as_str()) {
628 return (
629 StatusCode::BAD_REQUEST,
630 Json(ApiError {
631 code: "INVALID_REQUEST".to_string(),
632 message: format!("Invalid event type: {}", req.event_type),
633 details: None,
634 }),
635 )
636 .into_response();
637 }
638
639 match event_mgr
641 .add_event(task_id, &req.event_type, &req.data)
642 .await
643 {
644 Ok(event) => (StatusCode::CREATED, Json(ApiResponse { data: event })).into_response(),
645 Err(e) => (
646 StatusCode::BAD_REQUEST,
647 Json(ApiError {
648 code: "INVALID_REQUEST".to_string(),
649 message: format!("Failed to create event: {}", e),
650 details: None,
651 }),
652 )
653 .into_response(),
654 }
655}
656
657pub async fn update_event(
659 State(state): State<AppState>,
660 Path((task_id, event_id)): Path<(i64, i64)>,
661 Json(req): Json<UpdateEventRequest>,
662) -> impl IntoResponse {
663 let (db_pool, project_path) = match state.get_active_project_context().await {
664 Ok(ctx) => ctx,
665 Err(e) => {
666 return (
667 StatusCode::INTERNAL_SERVER_ERROR,
668 Json(ApiError {
669 code: "DATABASE_ERROR".to_string(),
670 message: e,
671 details: None,
672 }),
673 )
674 .into_response()
675 },
676 };
677
678 let event_mgr = EventManager::with_websocket(
679 &db_pool,
680 std::sync::Arc::new(state.ws_state.clone()),
681 project_path,
682 );
683
684 if let Some(ref event_type) = req.event_type {
686 if !["decision", "blocker", "milestone", "note"].contains(&event_type.as_str()) {
687 return (
688 StatusCode::BAD_REQUEST,
689 Json(ApiError {
690 code: "INVALID_REQUEST".to_string(),
691 message: format!("Invalid event type: {}", event_type),
692 details: None,
693 }),
694 )
695 .into_response();
696 }
697 }
698
699 match event_mgr
700 .update_event(event_id, req.event_type.as_deref(), req.data.as_deref())
701 .await
702 {
703 Ok(event) => {
704 if event.task_id != task_id {
706 return (
707 StatusCode::BAD_REQUEST,
708 Json(ApiError {
709 code: "INVALID_REQUEST".to_string(),
710 message: format!("Event {} does not belong to task {}", event_id, task_id),
711 details: None,
712 }),
713 )
714 .into_response();
715 }
716 (StatusCode::OK, Json(ApiResponse { data: event })).into_response()
717 },
718 Err(e) => (
719 StatusCode::BAD_REQUEST,
720 Json(ApiError {
721 code: "INVALID_REQUEST".to_string(),
722 message: format!("Failed to update event: {}", e),
723 details: None,
724 }),
725 )
726 .into_response(),
727 }
728}
729
730pub async fn delete_event(
732 State(state): State<AppState>,
733 Path((task_id, event_id)): Path<(i64, i64)>,
734) -> impl IntoResponse {
735 let (db_pool, project_path) = match state.get_active_project_context().await {
736 Ok(ctx) => ctx,
737 Err(e) => {
738 return (
739 StatusCode::INTERNAL_SERVER_ERROR,
740 Json(ApiError {
741 code: "DATABASE_ERROR".to_string(),
742 message: e,
743 details: None,
744 }),
745 )
746 .into_response()
747 },
748 };
749
750 let event_mgr = EventManager::with_websocket(
751 &db_pool,
752 std::sync::Arc::new(state.ws_state.clone()),
753 project_path,
754 );
755
756 match sqlx::query_as::<_, crate::db::models::Event>(crate::sql_constants::SELECT_EVENT_BY_ID)
758 .bind(event_id)
759 .fetch_optional(&db_pool)
760 .await
761 {
762 Ok(Some(event)) => {
763 if event.task_id != task_id {
764 return (
765 StatusCode::BAD_REQUEST,
766 Json(ApiError {
767 code: "INVALID_REQUEST".to_string(),
768 message: format!("Event {} does not belong to task {}", event_id, task_id),
769 details: None,
770 }),
771 )
772 .into_response();
773 }
774 },
775 Ok(None) => {
776 return (
777 StatusCode::NOT_FOUND,
778 Json(ApiError {
779 code: "EVENT_NOT_FOUND".to_string(),
780 message: format!("Event {} not found", event_id),
781 details: None,
782 }),
783 )
784 .into_response();
785 },
786 Err(e) => {
787 return (
788 StatusCode::INTERNAL_SERVER_ERROR,
789 Json(ApiError {
790 code: "DATABASE_ERROR".to_string(),
791 message: format!("Database error: {}", e),
792 details: None,
793 }),
794 )
795 .into_response();
796 },
797 }
798
799 match event_mgr.delete_event(event_id).await {
800 Ok(_) => (StatusCode::NO_CONTENT).into_response(),
801 Err(e) => (
802 StatusCode::BAD_REQUEST,
803 Json(ApiError {
804 code: "INVALID_REQUEST".to_string(),
805 message: format!("Failed to delete event: {}", e),
806 details: None,
807 }),
808 )
809 .into_response(),
810 }
811}
812
813pub async fn search(
815 State(state): State<AppState>,
816 Query(query): Query<SearchQuery>,
817) -> impl IntoResponse {
818 let db_pool = match state.get_active_db_pool().await {
819 Ok(pool) => pool,
820 Err(e) => {
821 return (
822 StatusCode::INTERNAL_SERVER_ERROR,
823 Json(ApiError {
824 code: "DATABASE_ERROR".to_string(),
825 message: e,
826 details: None,
827 }),
828 )
829 .into_response()
830 },
831 };
832 let search_mgr = SearchManager::new(&db_pool);
833
834 match search_mgr
835 .search(
836 &query.query,
837 query.include_tasks,
838 query.include_events,
839 query.limit,
840 query.offset,
841 false,
842 )
843 .await
844 {
845 Ok(results) => (StatusCode::OK, Json(ApiResponse { data: results })).into_response(),
846 Err(e) => (
847 StatusCode::INTERNAL_SERVER_ERROR,
848 Json(ApiError {
849 code: "DATABASE_ERROR".to_string(),
850 message: format!("Search failed: {}", e),
851 details: None,
852 }),
853 )
854 .into_response(),
855 }
856}
857
858pub async fn list_projects(State(state): State<AppState>) -> impl IntoResponse {
860 let port = state.port;
861 let pid = std::process::id();
862 let host_path = state.host_project.path.clone();
863
864 let known_projects = state.known_projects.read().await;
866
867 let projects: Vec<serde_json::Value> = known_projects
868 .values()
869 .map(|proj| {
870 let is_host = proj.path.to_string_lossy() == host_path;
871 json!({
872 "name": proj.name,
873 "path": proj.path.to_string_lossy(),
874 "port": port,
875 "pid": pid,
876 "url": format!("http://127.0.0.1:{}", port),
877 "started_at": chrono::Utc::now().to_rfc3339(),
878 "mcp_connected": false,
879 "is_online": is_host, "mcp_agent": None::<String>,
881 "mcp_last_seen": None::<String>,
882 })
883 })
884 .collect();
885
886 (StatusCode::OK, Json(ApiResponse { data: projects })).into_response()
887}
888
889pub async fn switch_project(
891 State(state): State<AppState>,
892 Json(req): Json<SwitchProjectRequest>,
893) -> impl IntoResponse {
894 use std::path::PathBuf;
895
896 let project_path = PathBuf::from(&req.project_path);
898
899 if let Err(e) = state.add_project(project_path.clone()).await {
901 return (
902 StatusCode::NOT_FOUND,
903 Json(ApiError {
904 code: "PROJECT_NOT_FOUND".to_string(),
905 message: e,
906 details: None,
907 }),
908 )
909 .into_response();
910 }
911
912 if let Err(e) = state.switch_active_project(project_path.clone()).await {
914 return (
915 StatusCode::INTERNAL_SERVER_ERROR,
916 Json(ApiError {
917 code: "SWITCH_ERROR".to_string(),
918 message: e,
919 details: None,
920 }),
921 )
922 .into_response();
923 }
924
925 let project_name = project_path
927 .file_name()
928 .and_then(|n| n.to_str())
929 .unwrap_or("unknown")
930 .to_string();
931 let db_path = project_path.join(".intent-engine").join("project.db");
932
933 tracing::info!(
934 "Switched to project: {} at {}",
935 project_name,
936 project_path.display()
937 );
938
939 (
940 StatusCode::OK,
941 Json(ApiResponse {
942 data: json!({
943 "success": true,
944 "project_name": project_name,
945 "project_path": project_path.display().to_string(),
946 "database": db_path.display().to_string(),
947 }),
948 }),
949 )
950 .into_response()
951}
952
953pub async fn remove_project(
956 State(state): State<AppState>,
957 Json(req): Json<SwitchProjectRequest>,
958) -> impl IntoResponse {
959 use std::path::PathBuf;
960
961 let project_path = PathBuf::from(&req.project_path);
962
963 match state.remove_project(&project_path).await {
964 Ok(()) => {
965 tracing::info!("Removed project: {}", req.project_path);
966 (
967 StatusCode::OK,
968 Json(ApiResponse {
969 data: json!({
970 "success": true,
971 "removed_path": req.project_path,
972 }),
973 }),
974 )
975 .into_response()
976 },
977 Err(e) => (
978 StatusCode::BAD_REQUEST,
979 Json(ApiError {
980 code: "REMOVE_FAILED".to_string(),
981 message: e,
982 details: None,
983 }),
984 )
985 .into_response(),
986 }
987}
988
989pub async fn get_task_context(
991 State(state): State<AppState>,
992 Path(id): Path<i64>,
993) -> impl IntoResponse {
994 let db_pool = match state.get_active_db_pool().await {
995 Ok(pool) => pool,
996 Err(e) => {
997 return (
998 StatusCode::INTERNAL_SERVER_ERROR,
999 Json(ApiError {
1000 code: "DATABASE_ERROR".to_string(),
1001 message: e,
1002 details: None,
1003 }),
1004 )
1005 .into_response()
1006 },
1007 };
1008 let task_mgr = TaskManager::new(&db_pool);
1009
1010 match task_mgr.get_task_context(id).await {
1011 Ok(context) => (StatusCode::OK, Json(ApiResponse { data: context })).into_response(),
1012 Err(e) if e.to_string().contains("not found") => (
1013 StatusCode::NOT_FOUND,
1014 Json(ApiError {
1015 code: "TASK_NOT_FOUND".to_string(),
1016 message: format!("Task {} not found", id),
1017 details: None,
1018 }),
1019 )
1020 .into_response(),
1021 Err(e) => (
1022 StatusCode::INTERNAL_SERVER_ERROR,
1023 Json(ApiError {
1024 code: "DATABASE_ERROR".to_string(),
1025 message: format!("Failed to get task context: {}", e),
1026 details: None,
1027 }),
1028 )
1029 .into_response(),
1030 }
1031}
1032
1033pub async fn handle_cli_notification(
1035 State(state): State<AppState>,
1036 Json(message): Json<crate::dashboard::cli_notifier::NotificationMessage>,
1037) -> impl IntoResponse {
1038 use crate::dashboard::cli_notifier::NotificationMessage;
1039 use std::path::PathBuf;
1040
1041 tracing::debug!("Received CLI notification: {:?}", message);
1042
1043 let project_path = match &message {
1045 NotificationMessage::TaskChanged { project_path, .. } => project_path.clone(),
1046 NotificationMessage::EventAdded { project_path, .. } => project_path.clone(),
1047 NotificationMessage::WorkspaceChanged { project_path, .. } => project_path.clone(),
1048 };
1049
1050 if let Some(ref path_str) = project_path {
1052 let project_path = PathBuf::from(path_str);
1053
1054 if let Err(e) = state.add_project(project_path.clone()).await {
1056 tracing::warn!("Failed to add project from CLI notification: {}", e);
1057 } else {
1058 if let Err(e) = state.switch_active_project(project_path.clone()).await {
1060 tracing::warn!("Failed to switch to project from CLI notification: {}", e);
1061 } else {
1062 let project_name = project_path
1063 .file_name()
1064 .and_then(|n| n.to_str())
1065 .unwrap_or("unknown");
1066 tracing::info!(
1067 "Auto-switched to project: {} (from CLI notification)",
1068 project_name
1069 );
1070 }
1071 }
1072 }
1073
1074 let ui_message = match &message {
1076 NotificationMessage::TaskChanged {
1077 task_id,
1078 operation,
1079 project_path,
1080 } => {
1081 json!({
1083 "type": "db_operation",
1084 "payload": {
1085 "entity": "task",
1086 "operation": operation,
1087 "affected_ids": task_id.map(|id| vec![id]).unwrap_or_default(),
1088 "project_path": project_path
1089 }
1090 })
1091 },
1092 NotificationMessage::EventAdded {
1093 task_id,
1094 event_id,
1095 project_path,
1096 } => {
1097 json!({
1098 "type": "db_operation",
1099 "payload": {
1100 "entity": "event",
1101 "operation": "created",
1102 "affected_ids": vec![*event_id],
1103 "task_id": task_id,
1104 "project_path": project_path
1105 }
1106 })
1107 },
1108 NotificationMessage::WorkspaceChanged {
1109 current_task_id,
1110 project_path,
1111 } => {
1112 json!({
1113 "type": "db_operation",
1114 "payload": {
1115 "entity": "workspace",
1116 "operation": "updated",
1117 "current_task_id": current_task_id,
1118 "project_path": project_path
1119 }
1120 })
1121 },
1122 };
1123
1124 let notification_json = serde_json::to_string(&ui_message).unwrap_or_default();
1125 state.ws_state.broadcast_to_ui(¬ification_json).await;
1126
1127 (StatusCode::OK, Json(json!({"success": true}))).into_response()
1128}
1129
1130pub async fn shutdown_handler(State(state): State<AppState>) -> impl IntoResponse {
1133 tracing::info!("Shutdown requested via HTTP endpoint");
1134
1135 let mut shutdown = state.shutdown_tx.lock().await;
1137 if let Some(tx) = shutdown.take() {
1138 if tx.send(()).is_ok() {
1139 tracing::info!("Shutdown signal sent successfully");
1140 (
1141 StatusCode::OK,
1142 Json(json!({
1143 "status": "ok",
1144 "message": "Dashboard is shutting down gracefully"
1145 })),
1146 )
1147 .into_response()
1148 } else {
1149 tracing::error!("Failed to send shutdown signal");
1150 (
1151 StatusCode::INTERNAL_SERVER_ERROR,
1152 Json(json!({
1153 "status": "error",
1154 "message": "Failed to initiate shutdown"
1155 })),
1156 )
1157 .into_response()
1158 }
1159 } else {
1160 tracing::warn!("Shutdown already initiated");
1161 (
1162 StatusCode::CONFLICT,
1163 Json(json!({
1164 "status": "error",
1165 "message": "Shutdown already in progress"
1166 })),
1167 )
1168 .into_response()
1169 }
1170}