use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Json},
};
use serde_json::json;
use super::models::*;
use super::server::AppState;
use crate::{
db::models::TaskSortBy,
events::EventManager,
search::SearchManager,
tasks::{TaskManager, TaskUpdate},
workspace::WorkspaceManager,
};
pub async fn list_tasks(
State(state): State<AppState>,
Query(query): Query<TaskListQuery>,
) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let task_mgr = TaskManager::new(&db_pool);
let parent_filter = query.parent.as_deref().map(|p| {
if p == "null" {
None
} else {
p.parse::<i64>().ok()
}
});
let sort_by = match query.sort_by.as_deref() {
Some("id") => Some(TaskSortBy::Id),
Some("priority") => Some(TaskSortBy::Priority),
Some("time") => Some(TaskSortBy::Time),
Some("focus") => Some(TaskSortBy::FocusAware),
_ => Some(TaskSortBy::FocusAware), };
match task_mgr
.find_tasks(
query.status,
parent_filter,
sort_by,
query.limit,
query.offset,
)
.await
{
Ok(result) => (StatusCode::OK, Json(ApiResponse { data: result })).into_response(),
Err(e) => {
tracing::error!(error = %e, "Failed to fetch tasks");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Failed to list tasks: {}", e),
details: None,
}),
)
.into_response()
},
}
}
pub async fn get_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let task_mgr = TaskManager::new(&db_pool);
match task_mgr.get_task(id).await {
Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
Err(e) if e.to_string().contains("not found") => (
StatusCode::NOT_FOUND,
Json(ApiError {
code: "TASK_NOT_FOUND".to_string(),
message: format!("Task {} not found", id),
details: None,
}),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Failed to get task: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn create_task(
State(state): State<AppState>,
Json(req): Json<CreateTaskRequest>,
) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let project_path = state
.get_active_project()
.await
.map(|p| p.path.to_string_lossy().to_string())
.unwrap_or_default();
let task_mgr = TaskManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
let result = task_mgr
.add_task(
req.name.clone(),
req.spec.clone(),
req.parent_id,
None,
None,
None,
)
.await;
match result {
Ok(mut task) => {
if let Some(priority) = req.priority {
if let Ok(updated_task) = task_mgr
.update_task(
task.id,
TaskUpdate {
priority: Some(priority),
..Default::default()
},
)
.await
{
task = updated_task;
}
}
(StatusCode::CREATED, Json(ApiResponse { data: task })).into_response()
},
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to create task: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn update_task(
State(state): State<AppState>,
Path(id): Path<i64>,
Json(req): Json<UpdateTaskRequest>,
) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let project_path = state
.get_active_project()
.await
.map(|p| p.path.to_string_lossy().to_string())
.unwrap_or_default();
let task_mgr = TaskManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
match task_mgr.get_task(id).await {
Err(e) if e.to_string().contains("not found") => {
return (
StatusCode::NOT_FOUND,
Json(ApiError {
code: "TASK_NOT_FOUND".to_string(),
message: format!("Task {} not found", id),
details: None,
}),
)
.into_response()
},
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Database error: {}", e),
details: None,
}),
)
.into_response()
},
Ok(_) => {},
}
match task_mgr
.update_task(
id,
TaskUpdate {
name: req.name.as_deref(),
spec: req.spec.as_deref(),
status: req.status.as_deref(),
priority: req.priority,
..Default::default()
},
)
.await
{
Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to update task: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn delete_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let project_path = state
.get_active_project()
.await
.map(|p| p.path.to_string_lossy().to_string())
.unwrap_or_default();
let task_mgr = TaskManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
match task_mgr.delete_task(id).await {
Ok(_) => (StatusCode::NO_CONTENT).into_response(),
Err(e) if e.to_string().contains("not found") => (
StatusCode::NOT_FOUND,
Json(ApiError {
code: "TASK_NOT_FOUND".to_string(),
message: format!("Task {} not found", id),
details: None,
}),
)
.into_response(),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to delete task: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn start_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let project_path = state
.get_active_project()
.await
.map(|p| p.path.to_string_lossy().to_string())
.unwrap_or_default();
let task_mgr = TaskManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
match task_mgr.start_task(id, false).await {
Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
Err(e) if e.to_string().contains("not found") => (
StatusCode::NOT_FOUND,
Json(ApiError {
code: "TASK_NOT_FOUND".to_string(),
message: format!("Task {} not found", id),
details: None,
}),
)
.into_response(),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to start task: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn done_task(State(state): State<AppState>) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let project_path = state
.get_active_project()
.await
.map(|p| p.path.to_string_lossy().to_string())
.unwrap_or_default();
let task_mgr = TaskManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
match task_mgr.done_task(false).await {
Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
Err(e) if e.to_string().contains("No current task") => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "NO_CURRENT_TASK".to_string(),
message: "No current task to complete".to_string(),
details: None,
}),
)
.into_response(),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to complete task: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn spawn_subtask(
State(state): State<AppState>,
Path(_parent_id): Path<i64>, Json(req): Json<SpawnSubtaskRequest>,
) -> impl IntoResponse {
let (db_pool, project_path) = match state.get_active_project_context().await {
Ok(ctx) => ctx,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let task_mgr = TaskManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
match task_mgr.spawn_subtask(&req.name, req.spec.as_deref()).await {
Ok(response) => (StatusCode::CREATED, Json(ApiResponse { data: response })).into_response(),
Err(e) if e.to_string().contains("No current task") => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "NO_CURRENT_TASK".to_string(),
message: "No current task to spawn subtask from".to_string(),
details: None,
}),
)
.into_response(),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to spawn subtask: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn get_current_task(State(state): State<AppState>) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let workspace_mgr = WorkspaceManager::new(&db_pool);
match workspace_mgr.get_current_task(None).await {
Ok(response) => {
if response.task.is_some() {
(StatusCode::OK, Json(ApiResponse { data: response })).into_response()
} else {
(
StatusCode::OK,
Json(json!({
"data": null,
"message": "No current task"
})),
)
.into_response()
}
},
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Failed to get current task: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn pick_next_task(State(state): State<AppState>) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let task_mgr = TaskManager::new(&db_pool);
match task_mgr.pick_next().await {
Ok(response) => (StatusCode::OK, Json(ApiResponse { data: response })).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Failed to pick next task: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn list_events(
State(state): State<AppState>,
Path(task_id): Path<i64>,
Query(query): Query<EventListQuery>,
) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let event_mgr = EventManager::new(&db_pool);
match event_mgr
.list_events(
Some(task_id),
query.limit.map(|l| l as i64),
query.event_type,
query.since,
)
.await
{
Ok(events) => (StatusCode::OK, Json(ApiResponse { data: events })).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Failed to list events: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn create_event(
State(state): State<AppState>,
Path(task_id): Path<i64>,
Json(req): Json<CreateEventRequest>,
) -> impl IntoResponse {
let (db_pool, project_path) = match state.get_active_project_context().await {
Ok(ctx) => ctx,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let event_mgr = EventManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
if !["decision", "blocker", "milestone", "note"].contains(&req.event_type.as_str()) {
return (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Invalid event type: {}", req.event_type),
details: None,
}),
)
.into_response();
}
match event_mgr
.add_event(task_id, req.event_type.clone(), req.data.clone())
.await
{
Ok(event) => (StatusCode::CREATED, Json(ApiResponse { data: event })).into_response(),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to create event: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn update_event(
State(state): State<AppState>,
Path((task_id, event_id)): Path<(i64, i64)>,
Json(req): Json<UpdateEventRequest>,
) -> impl IntoResponse {
let (db_pool, project_path) = match state.get_active_project_context().await {
Ok(ctx) => ctx,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let event_mgr = EventManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
if let Some(ref event_type) = req.event_type {
if !["decision", "blocker", "milestone", "note"].contains(&event_type.as_str()) {
return (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Invalid event type: {}", event_type),
details: None,
}),
)
.into_response();
}
}
match event_mgr
.update_event(event_id, req.event_type.as_deref(), req.data.as_deref())
.await
{
Ok(event) => {
if event.task_id != task_id {
return (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Event {} does not belong to task {}", event_id, task_id),
details: None,
}),
)
.into_response();
}
(StatusCode::OK, Json(ApiResponse { data: event })).into_response()
},
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to update event: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn delete_event(
State(state): State<AppState>,
Path((task_id, event_id)): Path<(i64, i64)>,
) -> impl IntoResponse {
let (db_pool, project_path) = match state.get_active_project_context().await {
Ok(ctx) => ctx,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let event_mgr = EventManager::with_websocket(
&db_pool,
std::sync::Arc::new(state.ws_state.clone()),
project_path,
);
match sqlx::query_as::<_, crate::db::models::Event>(crate::sql_constants::SELECT_EVENT_BY_ID)
.bind(event_id)
.fetch_optional(&db_pool)
.await
{
Ok(Some(event)) => {
if event.task_id != task_id {
return (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Event {} does not belong to task {}", event_id, task_id),
details: None,
}),
)
.into_response();
}
},
Ok(None) => {
return (
StatusCode::NOT_FOUND,
Json(ApiError {
code: "EVENT_NOT_FOUND".to_string(),
message: format!("Event {} not found", event_id),
details: None,
}),
)
.into_response();
},
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Database error: {}", e),
details: None,
}),
)
.into_response();
},
}
match event_mgr.delete_event(event_id).await {
Ok(_) => (StatusCode::NO_CONTENT).into_response(),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "INVALID_REQUEST".to_string(),
message: format!("Failed to delete event: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn search(
State(state): State<AppState>,
Query(query): Query<SearchQuery>,
) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let search_mgr = SearchManager::new(&db_pool);
match search_mgr
.search(
&query.query,
query.include_tasks,
query.include_events,
query.limit,
query.offset,
false,
)
.await
{
Ok(results) => (StatusCode::OK, Json(ApiResponse { data: results })).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Search failed: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn list_projects(State(state): State<AppState>) -> impl IntoResponse {
let host_path = state.host_project.path.clone();
let known_projects = state.known_projects.read().await;
let projects: Vec<serde_json::Value> = known_projects
.values()
.map(|proj| {
let is_host = proj.path.to_string_lossy() == host_path;
json!({
"name": proj.name,
"path": proj.path.to_string_lossy(),
"is_online": is_host, "mcp_connected": false, })
})
.collect();
(StatusCode::OK, Json(ApiResponse { data: projects })).into_response()
}
pub async fn switch_project(
State(state): State<AppState>,
Json(req): Json<SwitchProjectRequest>,
) -> impl IntoResponse {
use std::path::PathBuf;
let project_path = PathBuf::from(&req.project_path);
if let Err(e) = state.add_project(project_path.clone()).await {
return (
StatusCode::NOT_FOUND,
Json(ApiError {
code: "PROJECT_NOT_FOUND".to_string(),
message: e,
details: None,
}),
)
.into_response();
}
if let Err(e) = state.switch_active_project(project_path.clone()).await {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "SWITCH_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response();
}
let project_name = project_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let db_path = project_path.join(".intent-engine").join("project.db");
tracing::info!(
"Switched to project: {} at {}",
project_name,
project_path.display()
);
(
StatusCode::OK,
Json(ApiResponse {
data: json!({
"success": true,
"project_name": project_name,
"project_path": project_path.display().to_string(),
"database": db_path.display().to_string(),
}),
}),
)
.into_response()
}
pub async fn remove_project(
State(state): State<AppState>,
Json(req): Json<SwitchProjectRequest>,
) -> impl IntoResponse {
use std::path::PathBuf;
let project_path = PathBuf::from(&req.project_path);
match state.remove_project(&project_path).await {
Ok(()) => {
tracing::info!("Removed project: {}", req.project_path);
(
StatusCode::OK,
Json(ApiResponse {
data: json!({
"success": true,
"removed_path": req.project_path,
}),
}),
)
.into_response()
},
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ApiError {
code: "REMOVE_FAILED".to_string(),
message: e,
details: None,
}),
)
.into_response(),
}
}
pub async fn get_task_context(
State(state): State<AppState>,
Path(id): Path<i64>,
) -> impl IntoResponse {
let db_pool = match state.get_active_db_pool().await {
Ok(pool) => pool,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: e,
details: None,
}),
)
.into_response()
},
};
let task_mgr = TaskManager::new(&db_pool);
match task_mgr.get_task_context(id).await {
Ok(context) => (StatusCode::OK, Json(ApiResponse { data: context })).into_response(),
Err(e) if e.to_string().contains("not found") => (
StatusCode::NOT_FOUND,
Json(ApiError {
code: "TASK_NOT_FOUND".to_string(),
message: format!("Task {} not found", id),
details: None,
}),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
code: "DATABASE_ERROR".to_string(),
message: format!("Failed to get task context: {}", e),
details: None,
}),
)
.into_response(),
}
}
pub async fn handle_cli_notification(
State(state): State<AppState>,
Json(message): Json<crate::dashboard::cli_notifier::NotificationMessage>,
) -> impl IntoResponse {
use crate::dashboard::cli_notifier::NotificationMessage;
use std::path::PathBuf;
tracing::debug!("Received CLI notification: {:?}", message);
let project_path = match &message {
NotificationMessage::TaskChanged { project_path, .. } => project_path.clone(),
NotificationMessage::EventAdded { project_path, .. } => project_path.clone(),
NotificationMessage::WorkspaceChanged { project_path, .. } => project_path.clone(),
};
if let Some(ref path_str) = project_path {
let project_path = PathBuf::from(path_str);
if let Err(e) = state.add_project(project_path.clone()).await {
tracing::warn!("Failed to add project from CLI notification: {}", e);
} else {
if let Err(e) = state.switch_active_project(project_path.clone()).await {
tracing::warn!("Failed to switch to project from CLI notification: {}", e);
} else {
let project_name = project_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
tracing::info!(
"Auto-switched to project: {} (from CLI notification)",
project_name
);
}
}
}
let ui_message = match &message {
NotificationMessage::TaskChanged {
task_id,
operation,
project_path,
} => {
json!({
"type": "db_operation",
"payload": {
"entity": "task",
"operation": operation,
"affected_ids": task_id.map(|id| vec![id]).unwrap_or_default(),
"project_path": project_path
}
})
},
NotificationMessage::EventAdded {
task_id,
event_id,
project_path,
} => {
json!({
"type": "db_operation",
"payload": {
"entity": "event",
"operation": "created",
"affected_ids": vec![*event_id],
"task_id": task_id,
"project_path": project_path
}
})
},
NotificationMessage::WorkspaceChanged {
current_task_id,
project_path,
} => {
json!({
"type": "db_operation",
"payload": {
"entity": "workspace",
"operation": "updated",
"current_task_id": current_task_id,
"project_path": project_path
}
})
},
};
let notification_json = serde_json::to_string(&ui_message).unwrap_or_default();
state.ws_state.broadcast_to_ui(¬ification_json).await;
(StatusCode::OK, Json(json!({"success": true}))).into_response()
}
pub async fn shutdown_handler(State(state): State<AppState>) -> impl IntoResponse {
tracing::info!("Shutdown requested via HTTP endpoint");
let mut shutdown = state.shutdown_tx.lock().await;
if let Some(tx) = shutdown.take() {
if tx.send(()).is_ok() {
tracing::info!("Shutdown signal sent successfully");
(
StatusCode::OK,
Json(json!({
"status": "ok",
"message": "Dashboard is shutting down gracefully"
})),
)
.into_response()
} else {
tracing::error!("Failed to send shutdown signal");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"status": "error",
"message": "Failed to initiate shutdown"
})),
)
.into_response()
}
} else {
tracing::warn!("Shutdown already initiated");
(
StatusCode::CONFLICT,
Json(json!({
"status": "error",
"message": "Shutdown already in progress"
})),
)
.into_response()
}
}