1use axum::{
4 extract::{Path, State},
5 http::StatusCode as AxumStatusCode,
6 response::IntoResponse,
7 Json,
8};
9use serde::{Deserialize, Serialize};
10
11use super::server::AppState;
12use super::websocket::{DashboardUpdate, TaskCancelEvent, TaskRetryEvent};
13use crate::storage::TaskUpdate;
14use crate::task::TaskStatus;
15use chrono::Utc;
16
17type StatusCode = AxumStatusCode;
19
20const MAX_RETRY_COUNT: u32 = 3;
22
23const BASE_RETRY_DELAY_SECS: u64 = 10;
25
26#[derive(Debug, Clone)]
28pub struct ApiError(AxumStatusCode);
29
30impl IntoResponse for ApiError {
31 fn into_response(self) -> axum::response::Response {
32 self.0.into_response()
33 }
34}
35
36impl From<AxumStatusCode> for ApiError {
37 fn from(status: AxumStatusCode) -> Self {
38 ApiError(status)
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ErrorResponse {
45 pub status: u16,
47 pub error_code: String,
49 pub message: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub details: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub request_id: Option<String>,
57 pub timestamp: chrono::DateTime<Utc>,
59}
60
61impl ErrorResponse {
62 pub fn new(
64 status: StatusCode,
65 error_code: impl Into<String>,
66 message: impl Into<String>,
67 ) -> Self {
68 Self {
69 status: status.as_u16(),
70 error_code: error_code.into(),
71 message: message.into(),
72 details: None,
73 request_id: None,
74 timestamp: Utc::now(),
75 }
76 }
77
78 pub fn with_details(mut self, details: impl Into<String>) -> Self {
80 self.details = Some(details.into());
81 self
82 }
83
84 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
86 self.request_id = Some(request_id.into());
87 self
88 }
89}
90
91impl IntoResponse for ErrorResponse {
92 fn into_response(self) -> axum::response::Response {
93 let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
94 (status, Json(self)).into_response()
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Agent {
101 pub id: u32,
102 pub name: String,
103 pub role: String,
104 pub category: AgentCategory,
105 pub status: AgentStatus,
106 pub tasks: u32,
107 pub color: AgentColor,
108 pub description: String,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum AgentCategory {
114 Coding,
115 Business,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum AgentStatus {
121 Active,
122 Working,
123 Idle,
124 Error,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(rename_all = "lowercase")]
129pub enum AgentColor {
130 Leader,
131 Executor,
132 Analyst,
133 Support,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct SystemStatus {
139 pub status: String,
140 pub active_agents: u32,
141 pub total_agents: u32,
142 pub active_tasks: u32,
143 pub queued_tasks: u32,
144 pub task_throughput: f64,
145 pub avg_completion_time: f64,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct TimelineEvent {
151 pub id: String,
152 #[serde(rename = "type")]
153 pub event_type: String,
154 pub message: String,
155 pub timestamp: String,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub task_id: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub agent_id: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub agent_name: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub agent_type: Option<String>,
164}
165
166pub async fn health_check() -> (StatusCode, &'static str) {
168 (StatusCode::OK, "OK")
169}
170
171pub async fn get_agents() -> Json<Vec<Agent>> {
173 match crate::http::fetch_real_agents().await {
175 Ok(agents) => Json(agents),
176 Err(e) => {
177 tracing::error!("Failed to fetch real agents: {}", e);
178 Json(vec![])
180 },
181 }
182}
183
184pub async fn get_system_status() -> Json<SystemStatus> {
186 match crate::http::fetch_real_system_status().await {
188 Ok(status) => Json(status),
189 Err(e) => {
190 tracing::error!("Failed to fetch real system status: {}", e);
191 Json(SystemStatus {
193 status: "error".to_string(),
194 active_agents: 0,
195 total_agents: 21,
196 active_tasks: 0,
197 queued_tasks: 0,
198 task_throughput: 0.0,
199 avg_completion_time: 0.0,
200 })
201 },
202 }
203}
204
205pub async fn get_events() -> Json<Vec<TimelineEvent>> {
207 match crate::http::fetch_real_events().await {
209 Ok(events) => Json(events),
210 Err(e) => {
211 tracing::error!("Failed to fetch real events: {}", e);
212 Json(vec![])
214 },
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct DagNode {
221 pub id: String,
222 pub label: String,
223 pub status: String, pub agent: String,
225 #[serde(rename = "agentType")]
226 pub agent_type: String,
227 pub priority: String, #[serde(rename = "estimatedMinutes")]
230 pub estimated_minutes: u32, pub description: String, pub module: String, pub layer: String, }
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct DagEdge {
239 pub from: String,
240 pub to: String,
241 #[serde(rename = "type")]
242 pub edge_type: String, }
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct DagData {
248 #[serde(rename = "workflowId")]
249 pub workflow_id: String,
250 pub nodes: Vec<DagNode>,
251 pub edges: Vec<DagEdge>,
252}
253
254pub async fn get_workflow_dag() -> Json<DagData> {
256 match crate::http::fetch_real_workflow_dag().await {
259 Ok(dag_data) => Json(dag_data),
260 Err(e) => {
261 tracing::error!("Failed to fetch workflow DAG: {}", e);
262 Json(crate::http::create_sample_dag_public())
265 },
266 }
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct TaskRetryRequest {
274 pub reason: Option<String>,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct TaskRetryResponse {
281 pub task_id: String,
283 pub status: String,
285 pub message: String,
287 pub retry_count: u32,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct TaskCancelResponse {
294 pub task_id: String,
296 pub status: String,
298 pub message: String,
300}
301
302pub async fn retry_task(
318 State(state): State<AppState>,
319 Path(task_id): Path<String>,
320 Json(payload): Json<TaskRetryRequest>,
321) -> Result<Json<TaskRetryResponse>, ErrorResponse> {
322 tracing::info!("Retrying task: {} (reason: {:?})", task_id, payload.reason);
323
324 let id = task_id.parse::<u64>().map_err(|e| {
326 tracing::error!("Invalid task ID format: {} - {}", task_id, e);
327 ErrorResponse::new(StatusCode::BAD_REQUEST, "INVALID_TASK_ID", "Invalid task ID format")
328 .with_details(format!("Task ID '{}' must be a valid integer: {}", task_id, e))
329 })?;
330
331 let task = state.storage.get_task(id).await.map_err(|e| {
333 tracing::error!("Failed to fetch task {}: {}", id, e);
334 ErrorResponse::new(
335 StatusCode::INTERNAL_SERVER_ERROR,
336 "STORAGE_ERROR",
337 "Failed to retrieve task from storage",
338 )
339 .with_details(format!("Storage error for task {}: {}", id, e))
340 })?;
341
342 let mut task = task.ok_or_else(|| {
344 tracing::warn!("Task {} not found", id);
345 ErrorResponse::new(
346 StatusCode::NOT_FOUND,
347 "TASK_NOT_FOUND",
348 format!("Task {} does not exist", id),
349 )
350 })?;
351
352 if task.status != TaskStatus::Failed {
354 tracing::warn!("Task {} is not in failed state (current: {:?})", id, task.status);
355 return Err(ErrorResponse::new(
356 StatusCode::CONFLICT,
357 "INVALID_TASK_STATE",
358 "Task must be in failed state to retry",
359 )
360 .with_details(format!("Current task status: {:?}", task.status)));
361 }
362
363 if task.retry_count >= MAX_RETRY_COUNT {
365 tracing::warn!(
366 "Task {} has reached max retry limit ({}/{})",
367 id,
368 task.retry_count,
369 MAX_RETRY_COUNT
370 );
371 return Err(ErrorResponse::new(
372 StatusCode::TOO_MANY_REQUESTS,
373 "MAX_RETRIES_EXCEEDED",
374 format!("Maximum retry limit of {} attempts reached", MAX_RETRY_COUNT),
375 )
376 .with_details(format!("Current retry count: {}/{}", task.retry_count, MAX_RETRY_COUNT)));
377 }
378
379 task.retry_count += 1;
381
382 let retry_reason = payload.reason.clone();
384 let description = payload
385 .reason
386 .or(Some(format!("Retry attempt {} - Previous failure", task.retry_count)));
387
388 let update = TaskUpdate {
390 status: Some(TaskStatus::Submitted),
391 description,
392 agent: None,
393 priority: None,
394 retry_count: Some(task.retry_count),
395 };
396
397 state.storage.update_task(id, update).await.map_err(|e| {
398 tracing::error!("Failed to update task {}: {}", id, e);
399 ErrorResponse::new(
400 StatusCode::INTERNAL_SERVER_ERROR,
401 "STORAGE_UPDATE_ERROR",
402 "Failed to update task status",
403 )
404 .with_details(format!("Storage update error for task {}: {}", id, e))
405 })?;
406
407 let delay_secs = BASE_RETRY_DELAY_SECS * 2u64.pow(task.retry_count - 1); let next_retry_at = Utc::now() + chrono::Duration::seconds(delay_secs as i64);
410
411 let retry_event = DashboardUpdate::TaskRetry {
413 event: TaskRetryEvent {
414 task_id: task_id.clone(),
415 retry_count: task.retry_count,
416 reason: retry_reason,
417 next_retry_at: Some(next_retry_at),
418 timestamp: Utc::now(),
419 },
420 };
421
422 if let Err(e) = state.ws_state.tx.send(retry_event) {
423 tracing::warn!("Failed to broadcast retry event for task {}: {}", id, e);
425 }
426
427 tracing::info!(
428 "Task {} queued for retry (attempt {}, next retry at {})",
429 id,
430 task.retry_count,
431 next_retry_at
432 );
433
434 Ok(Json(TaskRetryResponse {
435 task_id: task_id.clone(),
436 status: "submitted".to_string(),
437 message: format!("Task {} has been queued for retry", task_id),
438 retry_count: task.retry_count,
439 }))
440}
441
442pub async fn cancel_task(
456 State(state): State<AppState>,
457 Path(task_id): Path<String>,
458) -> Result<Json<TaskCancelResponse>, ErrorResponse> {
459 tracing::info!("Cancelling task: {}", task_id);
460
461 let id = task_id.parse::<u64>().map_err(|e| {
463 tracing::error!("Invalid task ID format: {} - {}", task_id, e);
464 ErrorResponse::new(StatusCode::BAD_REQUEST, "INVALID_TASK_ID", "Invalid task ID format")
465 .with_details(format!("Task ID '{}' must be a valid integer: {}", task_id, e))
466 })?;
467
468 let task = state.storage.get_task(id).await.map_err(|e| {
470 tracing::error!("Failed to fetch task {}: {}", id, e);
471 ErrorResponse::new(
472 StatusCode::INTERNAL_SERVER_ERROR,
473 "STORAGE_ERROR",
474 "Failed to retrieve task from storage",
475 )
476 .with_details(format!("Storage error for task {}: {}", id, e))
477 })?;
478
479 let task = task.ok_or_else(|| {
481 tracing::warn!("Task {} not found", id);
482 ErrorResponse::new(
483 StatusCode::NOT_FOUND,
484 "TASK_NOT_FOUND",
485 format!("Task {} does not exist", id),
486 )
487 })?;
488
489 if task.status != TaskStatus::Submitted && task.status != TaskStatus::Working {
491 tracing::warn!("Task {} is not cancellable (current state: {:?})", id, task.status);
492 return Err(ErrorResponse::new(
493 StatusCode::CONFLICT,
494 "INVALID_TASK_STATE",
495 "Task must be in Submitted or Working state to cancel",
496 )
497 .with_details(format!(
498 "Current task status: {:?}. Only Submitted or Working tasks can be cancelled.",
499 task.status
500 )));
501 }
502
503 let update = TaskUpdate {
505 status: Some(TaskStatus::Cancelled),
506 description: Some(format!("Task cancelled by user at {}", chrono::Utc::now())),
507 agent: None,
508 priority: None,
509 retry_count: None,
510 };
511
512 state.storage.update_task(id, update).await.map_err(|e| {
513 tracing::error!("Failed to update task {}: {}", id, e);
514 ErrorResponse::new(
515 StatusCode::INTERNAL_SERVER_ERROR,
516 "STORAGE_UPDATE_ERROR",
517 "Failed to update task status",
518 )
519 .with_details(format!("Storage update error for task {}: {}", id, e))
520 })?;
521
522 let cancel_event = DashboardUpdate::TaskCancel {
527 event: TaskCancelEvent {
528 task_id: task_id.clone(),
529 reason: format!("Task cancelled by user at {}", Utc::now()),
530 timestamp: Utc::now(),
531 },
532 };
533
534 if let Err(e) = state.ws_state.tx.send(cancel_event) {
535 tracing::warn!("Failed to broadcast cancel event for task {}: {}", id, e);
537 }
538
539 tracing::info!("Task {} has been cancelled", id);
540
541 Ok(Json(TaskCancelResponse {
542 task_id: task_id.clone(),
543 status: "cancelled".to_string(),
544 message: format!("Task {} has been cancelled", task_id),
545 }))
546}