botserver/src/auto_task/autotask_api.rs

1952 lines
64 KiB
Rust

use crate::auto_task::task_types::{
AutoTask, AutoTaskStatus, ExecutionMode, PendingApproval, PendingDecision, TaskPriority,
};
use crate::auto_task::intent_classifier::IntentClassifier;
use crate::auto_task::intent_compiler::IntentCompiler;
use crate::auto_task::safety_layer::{SafetyLayer, SimulationResult};
use crate::shared::state::AppState;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use chrono::Utc;
use diesel::prelude::*;
use diesel::sql_query;
use diesel::sql_types::{Text, Uuid as DieselUuid};
use log::{error, info, trace};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Deserialize)]
pub struct CompileIntentRequest {
pub intent: String,
pub execution_mode: Option<String>,
pub priority: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ClassifyIntentRequest {
pub intent: String,
pub auto_process: Option<bool>,
}
#[derive(Debug, Serialize)]
pub struct ClassifyIntentResponse {
pub success: bool,
pub intent_type: String,
pub confidence: f64,
pub suggested_name: Option<String>,
pub requires_clarification: bool,
pub clarification_question: Option<String>,
pub result: Option<IntentResultResponse>,
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct IntentResultResponse {
pub success: bool,
pub message: String,
pub app_url: Option<String>,
pub task_id: Option<String>,
pub schedule_id: Option<String>,
pub tool_triggers: Vec<String>,
pub created_resources: Vec<CreatedResourceResponse>,
pub next_steps: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct CreateAndExecuteRequest {
pub intent: String,
}
#[derive(Debug, Serialize)]
pub struct CreateAndExecuteResponse {
pub success: bool,
pub task_id: String,
pub status: String,
pub message: String,
pub app_url: Option<String>,
pub created_resources: Vec<CreatedResourceResponse>,
pub pending_items: Vec<PendingItemResponse>,
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct PendingItemResponse {
pub id: String,
pub label: String,
pub config_key: String,
pub reason: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CreatedResourceResponse {
pub resource_type: String,
pub name: String,
pub path: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CompileIntentResponse {
pub success: bool,
pub plan_id: Option<String>,
pub plan_name: Option<String>,
pub plan_description: Option<String>,
pub steps: Vec<PlanStepResponse>,
pub alternatives: Vec<AlternativeResponse>,
pub confidence: f64,
pub risk_level: String,
pub estimated_duration_minutes: i32,
pub estimated_cost: f64,
pub resource_estimate: ResourceEstimateResponse,
pub basic_program: Option<String>,
pub requires_approval: bool,
pub mcp_servers: Vec<String>,
pub external_apis: Vec<String>,
pub risks: Vec<RiskResponse>,
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct PlanStepResponse {
pub id: String,
pub order: i32,
pub name: String,
pub description: String,
pub keywords: Vec<String>,
pub priority: String,
pub risk_level: String,
pub estimated_minutes: i32,
pub requires_approval: bool,
}
#[derive(Debug, Serialize)]
pub struct AlternativeResponse {
pub id: String,
pub description: String,
pub confidence: f64,
pub pros: Vec<String>,
pub cons: Vec<String>,
pub estimated_cost: Option<f64>,
pub estimated_time_hours: Option<f64>,
}
#[derive(Debug, Serialize)]
pub struct ResourceEstimateResponse {
pub compute_hours: f64,
pub storage_gb: f64,
pub api_calls: i32,
pub llm_tokens: i32,
pub estimated_cost_usd: f64,
}
#[derive(Debug, Serialize)]
pub struct RiskResponse {
pub id: String,
pub category: String,
pub description: String,
pub probability: f64,
pub impact: String,
}
#[derive(Debug, Deserialize)]
pub struct ExecutePlanRequest {
pub plan_id: String,
pub execution_mode: Option<String>,
pub priority: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ExecutePlanResponse {
pub success: bool,
pub task_id: Option<String>,
pub status: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ListTasksQuery {
pub filter: Option<String>,
pub status: Option<String>,
pub priority: Option<String>,
pub limit: Option<i32>,
pub offset: Option<i32>,
}
#[derive(Debug, Serialize)]
pub struct AutoTaskStatsResponse {
pub total: i32,
pub running: i32,
pub pending: i32,
pub completed: i32,
pub failed: i32,
pub pending_approval: i32,
pub pending_decision: i32,
}
#[derive(Debug, Serialize)]
pub struct TaskActionResponse {
pub success: bool,
pub message: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct DecisionRequest {
pub decision_id: String,
pub option_id: Option<String>,
pub skip: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct ApprovalRequest {
pub approval_id: String,
pub action: String,
pub comment: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SimulationResponse {
pub success: bool,
pub confidence: f64,
pub risk_score: f64,
pub risk_level: String,
pub step_outcomes: Vec<StepOutcomeResponse>,
pub impact: ImpactResponse,
pub side_effects: Vec<SideEffectResponse>,
pub recommendations: Vec<RecommendationResponse>,
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct StepOutcomeResponse {
pub step_id: String,
pub step_name: String,
pub would_succeed: bool,
pub success_probability: f64,
pub failure_modes: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ImpactResponse {
pub risk_score: f64,
pub risk_level: String,
pub data_impact: DataImpactResponse,
pub cost_impact: CostImpactResponse,
pub time_impact: TimeImpactResponse,
pub security_impact: SecurityImpactResponse,
}
#[derive(Debug, Serialize)]
pub struct DataImpactResponse {
pub records_created: i32,
pub records_modified: i32,
pub records_deleted: i32,
pub tables_affected: Vec<String>,
pub reversible: bool,
}
#[derive(Debug, Serialize)]
pub struct CostImpactResponse {
pub api_costs: f64,
pub compute_costs: f64,
pub storage_costs: f64,
pub total_estimated_cost: f64,
}
#[derive(Debug, Serialize)]
pub struct TimeImpactResponse {
pub estimated_duration_seconds: i32,
pub blocking: bool,
}
#[derive(Debug, Serialize)]
pub struct SecurityImpactResponse {
pub risk_level: String,
pub credentials_accessed: Vec<String>,
pub external_systems: Vec<String>,
pub concerns: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct SideEffectResponse {
pub effect_type: String,
pub description: String,
pub severity: String,
pub mitigation: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct RecommendationResponse {
pub id: String,
pub recommendation_type: String,
pub description: String,
pub action: Option<String>,
}
pub async fn create_and_execute_handler(
State(state): State<Arc<AppState>>,
Json(request): Json<CreateAndExecuteRequest>,
) -> impl IntoResponse {
info!(
"Create and execute: {}",
&request.intent[..request.intent.len().min(100)]
);
let session = match get_current_session(&state) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(CreateAndExecuteResponse {
success: false,
task_id: String::new(),
status: "error".to_string(),
message: format!("Authentication error: {}", e),
app_url: None,
created_resources: Vec::new(),
pending_items: Vec::new(),
error: Some(e.to_string()),
}),
);
}
};
// Create task record first
let task_id = Uuid::new_v4();
if let Err(e) = create_task_record(&state, task_id, &session, &request.intent) {
error!("Failed to create task record: {}", e);
}
// Update status to running
let _ = update_task_status_db(&state, task_id, "running", None);
// Use IntentClassifier to classify and process
let classifier = IntentClassifier::new(Arc::clone(&state));
match classifier
.classify_and_process(&request.intent, &session)
.await
{
Ok(result) => {
let status = if result.success {
"completed"
} else {
"failed"
};
let _ = update_task_status_db(&state, task_id, status, result.error.as_deref());
// Get any pending items (ASK LATER)
let pending_items = get_pending_items_for_bot(&state, session.bot_id);
(
StatusCode::OK,
Json(CreateAndExecuteResponse {
success: result.success,
task_id: task_id.to_string(),
status: status.to_string(),
message: result.message,
app_url: result.app_url,
created_resources: result
.created_resources
.into_iter()
.map(|r| CreatedResourceResponse {
resource_type: r.resource_type,
name: r.name,
path: r.path,
})
.collect(),
pending_items,
error: result.error,
}),
)
}
Err(e) => {
let _ = update_task_status_db(&state, task_id, "failed", Some(&e.to_string()));
error!("Failed to process intent: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(CreateAndExecuteResponse {
success: false,
task_id: task_id.to_string(),
status: "failed".to_string(),
message: "Failed to process request".to_string(),
app_url: None,
created_resources: Vec::new(),
pending_items: Vec::new(),
error: Some(e.to_string()),
}),
)
}
}
}
pub async fn classify_intent_handler(
State(state): State<Arc<AppState>>,
Json(request): Json<ClassifyIntentRequest>,
) -> impl IntoResponse {
info!(
"Classifying intent: {}",
&request.intent[..request.intent.len().min(100)]
);
let session = match get_current_session(&state) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(ClassifyIntentResponse {
success: false,
intent_type: "UNKNOWN".to_string(),
confidence: 0.0,
suggested_name: None,
requires_clarification: false,
clarification_question: None,
result: None,
error: Some(format!("Authentication error: {}", e)),
}),
);
}
};
let classifier = IntentClassifier::new(Arc::clone(&state));
let auto_process = request.auto_process.unwrap_or(true);
if auto_process {
// Classify and process in one step
match classifier
.classify_and_process(&request.intent, &session)
.await
{
Ok(result) => {
let response = ClassifyIntentResponse {
success: result.success,
intent_type: result.intent_type.to_string(),
confidence: 0.0, // Would come from classification
suggested_name: None,
requires_clarification: false,
clarification_question: None,
result: Some(IntentResultResponse {
success: result.success,
message: result.message,
app_url: result.app_url,
task_id: result.task_id,
schedule_id: result.schedule_id,
tool_triggers: result.tool_triggers,
created_resources: result
.created_resources
.into_iter()
.map(|r| CreatedResourceResponse {
resource_type: r.resource_type,
name: r.name,
path: r.path,
})
.collect(),
next_steps: result.next_steps,
}),
error: result.error,
};
(StatusCode::OK, Json(response))
}
Err(e) => {
error!("Failed to classify/process intent: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ClassifyIntentResponse {
success: false,
intent_type: "UNKNOWN".to_string(),
confidence: 0.0,
suggested_name: None,
requires_clarification: false,
clarification_question: None,
result: None,
error: Some(e.to_string()),
}),
)
}
}
} else {
// Just classify, don't process
match classifier.classify(&request.intent, &session).await {
Ok(classification) => {
let response = ClassifyIntentResponse {
success: true,
intent_type: classification.intent_type.to_string(),
confidence: classification.confidence,
suggested_name: classification.suggested_name,
requires_clarification: classification.requires_clarification,
clarification_question: classification.clarification_question,
result: None,
error: None,
};
(StatusCode::OK, Json(response))
}
Err(e) => {
error!("Failed to classify intent: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ClassifyIntentResponse {
success: false,
intent_type: "UNKNOWN".to_string(),
confidence: 0.0,
suggested_name: None,
requires_clarification: false,
clarification_question: None,
result: None,
error: Some(e.to_string()),
}),
)
}
}
}
}
pub async fn compile_intent_handler(
State(state): State<Arc<AppState>>,
Json(request): Json<CompileIntentRequest>,
) -> impl IntoResponse {
info!(
"Compiling intent: {}",
&request.intent[..request.intent.len().min(100)]
);
let session = match get_current_session(&state) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(CompileIntentResponse {
success: false,
plan_id: None,
plan_name: None,
plan_description: None,
steps: Vec::new(),
alternatives: Vec::new(),
confidence: 0.0,
risk_level: "unknown".to_string(),
estimated_duration_minutes: 0,
estimated_cost: 0.0,
resource_estimate: ResourceEstimateResponse {
compute_hours: 0.0,
storage_gb: 0.0,
api_calls: 0,
llm_tokens: 0,
estimated_cost_usd: 0.0,
},
basic_program: None,
requires_approval: false,
mcp_servers: Vec::new(),
external_apis: Vec::new(),
risks: Vec::new(),
error: Some(format!("Authentication error: {}", e)),
}),
);
}
};
let compiler = IntentCompiler::new(Arc::clone(&state));
match compiler.compile(&request.intent, &session).await {
Ok(compiled) => {
let response = CompileIntentResponse {
success: true,
plan_id: Some(compiled.plan.id.clone()),
plan_name: Some(compiled.plan.name.clone()),
plan_description: Some(compiled.plan.description.clone()),
steps: compiled
.plan
.steps
.iter()
.map(|s| PlanStepResponse {
id: s.id.clone(),
order: s.order,
name: s.name.clone(),
description: s.description.clone(),
keywords: s.keywords.clone(),
priority: format!("{:?}", s.priority),
risk_level: format!("{:?}", s.risk_level),
estimated_minutes: s.estimated_minutes,
requires_approval: s.requires_approval,
})
.collect(),
alternatives: compiled
.alternatives
.iter()
.map(|a| AlternativeResponse {
id: a.id.clone(),
description: a.description.clone(),
confidence: a.confidence,
pros: a.pros.clone(),
cons: a.cons.clone(),
estimated_cost: a.estimated_cost,
estimated_time_hours: a.estimated_time_hours,
})
.collect(),
confidence: compiled.confidence,
risk_level: format!("{:?}", compiled.risk_assessment.overall_risk),
estimated_duration_minutes: compiled.plan.estimated_duration_minutes,
estimated_cost: compiled.resource_estimate.estimated_cost_usd,
resource_estimate: ResourceEstimateResponse {
compute_hours: compiled.resource_estimate.compute_hours,
storage_gb: compiled.resource_estimate.storage_gb,
api_calls: compiled.resource_estimate.api_calls,
llm_tokens: compiled.resource_estimate.llm_tokens,
estimated_cost_usd: compiled.resource_estimate.estimated_cost_usd,
},
basic_program: Some(compiled.basic_program.clone()),
requires_approval: compiled.plan.requires_approval,
mcp_servers: compiled.resource_estimate.mcp_servers_needed.clone(),
external_apis: compiled.resource_estimate.external_services.clone(),
risks: compiled
.risk_assessment
.risks
.iter()
.map(|r| RiskResponse {
id: r.id.clone(),
category: format!("{:?}", r.category),
description: r.description.clone(),
probability: r.probability,
impact: format!("{:?}", r.impact),
})
.collect(),
error: None,
};
(StatusCode::OK, Json(response))
}
Err(e) => {
error!("Failed to compile intent: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(CompileIntentResponse {
success: false,
plan_id: None,
plan_name: None,
plan_description: None,
steps: Vec::new(),
alternatives: Vec::new(),
confidence: 0.0,
risk_level: "unknown".to_string(),
estimated_duration_minutes: 0,
estimated_cost: 0.0,
resource_estimate: ResourceEstimateResponse {
compute_hours: 0.0,
storage_gb: 0.0,
api_calls: 0,
llm_tokens: 0,
estimated_cost_usd: 0.0,
},
basic_program: None,
requires_approval: false,
mcp_servers: Vec::new(),
external_apis: Vec::new(),
risks: Vec::new(),
error: Some(e.to_string()),
}),
)
}
}
}
pub async fn execute_plan_handler(
State(state): State<Arc<AppState>>,
Json(request): Json<ExecutePlanRequest>,
) -> impl IntoResponse {
info!("Executing plan: {}", request.plan_id);
let session = match get_current_session(&state) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(ExecutePlanResponse {
success: false,
task_id: None,
status: None,
error: Some(format!("Authentication error: {}", e)),
}),
);
}
};
let execution_mode = match request.execution_mode.as_deref() {
Some("fully-automatic") => ExecutionMode::FullyAutomatic,
Some("supervised") => ExecutionMode::Supervised,
Some("manual") => ExecutionMode::Manual,
Some("dry-run") => ExecutionMode::DryRun,
_ => ExecutionMode::SemiAutomatic,
};
let priority = match request.priority.as_deref() {
Some("critical") => TaskPriority::Critical,
Some("high") => TaskPriority::High,
Some("low") => TaskPriority::Low,
Some("background") => TaskPriority::Background,
_ => TaskPriority::Medium,
};
match create_auto_task_from_plan(&state, &session, &request.plan_id, execution_mode, priority) {
Ok(task) => match start_task_execution(&state, &task.id) {
Ok(_) => (
StatusCode::OK,
Json(ExecutePlanResponse {
success: true,
task_id: Some(task.id),
status: Some(task.status.to_string()),
error: None,
}),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ExecutePlanResponse {
success: false,
task_id: Some(task.id),
status: Some("failed".to_string()),
error: Some(e.to_string()),
}),
),
},
Err(e) => {
error!("Failed to create task: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ExecutePlanResponse {
success: false,
task_id: None,
status: None,
error: Some(e.to_string()),
}),
)
}
}
}
pub async fn list_tasks_handler(
State(state): State<Arc<AppState>>,
Query(query): Query<ListTasksQuery>,
) -> impl IntoResponse {
let filter = query.filter.as_deref().unwrap_or("all");
let limit = query.limit.unwrap_or(50);
let offset = query.offset.unwrap_or(0);
match list_auto_tasks(&state, filter, limit, offset) {
Ok(tasks) => {
let html = render_task_list_html(&tasks);
(StatusCode::OK, axum::response::Html(html))
}
Err(e) => {
error!("Failed to list tasks: {}", e);
let html = format!(
r#"<div class="error-message">
<span class="error-icon">❌</span>
<p>Failed to load tasks: {}</p>
</div>"#,
html_escape(&e.to_string())
);
(
StatusCode::INTERNAL_SERVER_ERROR,
axum::response::Html(html),
)
}
}
}
pub async fn get_stats_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
match get_auto_task_stats(&state) {
Ok(stats) => (StatusCode::OK, Json(stats)),
Err(e) => {
error!("Failed to get stats: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(AutoTaskStatsResponse {
total: 0,
running: 0,
pending: 0,
completed: 0,
failed: 0,
pending_approval: 0,
pending_decision: 0,
}),
)
}
}
}
pub async fn pause_task_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
) -> impl IntoResponse {
match update_task_status(&state, &task_id, AutoTaskStatus::Paused) {
Ok(_) => (
StatusCode::OK,
Json(TaskActionResponse {
success: true,
message: Some("Task paused".to_string()),
error: None,
}),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(TaskActionResponse {
success: false,
message: None,
error: Some(e.to_string()),
}),
),
}
}
pub async fn resume_task_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
) -> impl IntoResponse {
match update_task_status(&state, &task_id, AutoTaskStatus::Running) {
Ok(_) => {
let _ = start_task_execution(&state, &task_id);
(
StatusCode::OK,
Json(TaskActionResponse {
success: true,
message: Some("Task resumed".to_string()),
error: None,
}),
)
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(TaskActionResponse {
success: false,
message: None,
error: Some(e.to_string()),
}),
),
}
}
pub async fn cancel_task_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
) -> impl IntoResponse {
match update_task_status(&state, &task_id, AutoTaskStatus::Cancelled) {
Ok(_) => (
StatusCode::OK,
Json(TaskActionResponse {
success: true,
message: Some("Task cancelled".to_string()),
error: None,
}),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(TaskActionResponse {
success: false,
message: None,
error: Some(e.to_string()),
}),
),
}
}
pub async fn simulate_task_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
) -> impl IntoResponse {
let session = match get_current_session(&state) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(SimulationResponse {
success: false,
confidence: 0.0,
risk_score: 0.0,
risk_level: "unknown".to_string(),
step_outcomes: Vec::new(),
impact: ImpactResponse {
risk_score: 0.0,
risk_level: "unknown".to_string(),
data_impact: DataImpactResponse {
records_created: 0,
records_modified: 0,
records_deleted: 0,
tables_affected: Vec::new(),
reversible: true,
},
cost_impact: CostImpactResponse {
api_costs: 0.0,
compute_costs: 0.0,
storage_costs: 0.0,
total_estimated_cost: 0.0,
},
time_impact: TimeImpactResponse {
estimated_duration_seconds: 0,
blocking: false,
},
security_impact: SecurityImpactResponse {
risk_level: "unknown".to_string(),
credentials_accessed: Vec::new(),
external_systems: Vec::new(),
concerns: Vec::new(),
},
},
side_effects: Vec::new(),
recommendations: Vec::new(),
error: Some(format!("Authentication error: {}", e)),
}),
);
}
};
let safety_layer = SafetyLayer::new(Arc::clone(&state));
match simulate_task_execution(&state, &safety_layer, &task_id, &session) {
Ok(result) => {
let response = SimulationResponse {
success: result.success,
confidence: result.confidence,
risk_score: result.impact.risk_score,
risk_level: format!("{}", result.impact.risk_level),
step_outcomes: result
.step_outcomes
.iter()
.map(|s| StepOutcomeResponse {
step_id: s.step_id.clone(),
step_name: s.step_name.clone(),
would_succeed: s.would_succeed,
success_probability: s.success_probability,
failure_modes: s
.failure_modes
.iter()
.map(|f| f.failure_type.clone())
.collect(),
})
.collect(),
impact: ImpactResponse {
risk_score: result.impact.risk_score,
risk_level: format!("{}", result.impact.risk_level),
data_impact: DataImpactResponse {
records_created: result.impact.data_impact.records_created,
records_modified: result.impact.data_impact.records_modified,
records_deleted: result.impact.data_impact.records_deleted,
tables_affected: result.impact.data_impact.tables_affected.clone(),
reversible: result.impact.data_impact.reversible,
},
cost_impact: CostImpactResponse {
api_costs: result.impact.cost_impact.api_costs,
compute_costs: result.impact.cost_impact.compute_costs,
storage_costs: result.impact.cost_impact.storage_costs,
total_estimated_cost: result.impact.cost_impact.total_estimated_cost,
},
time_impact: TimeImpactResponse {
estimated_duration_seconds: result
.impact
.time_impact
.estimated_duration_seconds,
blocking: result.impact.time_impact.blocking,
},
security_impact: SecurityImpactResponse {
risk_level: format!("{}", result.impact.security_impact.risk_level),
credentials_accessed: result
.impact
.security_impact
.credentials_accessed
.clone(),
external_systems: result.impact.security_impact.external_systems.clone(),
concerns: result.impact.security_impact.concerns.clone(),
},
},
side_effects: result
.side_effects
.iter()
.map(|s| SideEffectResponse {
effect_type: s.effect_type.clone(),
description: s.description.clone(),
severity: format!("{:?}", s.severity),
mitigation: s.mitigation.clone(),
})
.collect(),
recommendations: result
.recommendations
.iter()
.enumerate()
.map(|(i, r)| RecommendationResponse {
id: format!("rec-{}", i),
recommendation_type: format!("{:?}", r.recommendation_type),
description: r.description.clone(),
action: r.action.clone(),
})
.collect(),
error: None,
};
(StatusCode::OK, Json(response))
}
Err(e) => {
error!("Simulation failed: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(SimulationResponse {
success: false,
confidence: 0.0,
risk_score: 1.0,
risk_level: "unknown".to_string(),
step_outcomes: Vec::new(),
impact: ImpactResponse {
risk_score: 1.0,
risk_level: "unknown".to_string(),
data_impact: DataImpactResponse {
records_created: 0,
records_modified: 0,
records_deleted: 0,
tables_affected: Vec::new(),
reversible: true,
},
cost_impact: CostImpactResponse {
api_costs: 0.0,
compute_costs: 0.0,
storage_costs: 0.0,
total_estimated_cost: 0.0,
},
time_impact: TimeImpactResponse {
estimated_duration_seconds: 0,
blocking: false,
},
security_impact: SecurityImpactResponse {
risk_level: "unknown".to_string(),
credentials_accessed: Vec::new(),
external_systems: Vec::new(),
concerns: Vec::new(),
},
},
side_effects: Vec::new(),
recommendations: Vec::new(),
error: Some(e.to_string()),
}),
)
}
}
}
pub async fn get_decisions_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
) -> impl IntoResponse {
match get_pending_decisions(&state, &task_id) {
Ok(decisions) => (StatusCode::OK, Json(decisions)),
Err(e) => {
error!("Failed to get decisions: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(Vec::<PendingDecision>::new()),
)
}
}
}
pub async fn submit_decision_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
Json(request): Json<DecisionRequest>,
) -> impl IntoResponse {
match submit_decision(&state, &task_id, &request) {
Ok(_) => (
StatusCode::OK,
Json(TaskActionResponse {
success: true,
message: Some("Decision submitted".to_string()),
error: None,
}),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(TaskActionResponse {
success: false,
message: None,
error: Some(e.to_string()),
}),
),
}
}
pub async fn get_approvals_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
) -> impl IntoResponse {
match get_pending_approvals(&state, &task_id) {
Ok(approvals) => (StatusCode::OK, Json(approvals)),
Err(e) => {
error!("Failed to get approvals: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(Vec::<PendingApproval>::new()),
)
}
}
}
pub async fn submit_approval_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
Json(request): Json<ApprovalRequest>,
) -> impl IntoResponse {
match submit_approval(&state, &task_id, &request) {
Ok(_) => (
StatusCode::OK,
Json(TaskActionResponse {
success: true,
message: Some(format!("Approval {}", request.action)),
error: None,
}),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(TaskActionResponse {
success: false,
message: None,
error: Some(e.to_string()),
}),
),
}
}
pub async fn simulate_plan_handler(
State(state): State<Arc<AppState>>,
Path(plan_id): Path<String>,
) -> impl IntoResponse {
let session = match get_current_session(&state) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(SimulationResponse {
success: false,
confidence: 0.0,
risk_score: 0.0,
risk_level: "unknown".to_string(),
step_outcomes: Vec::new(),
impact: ImpactResponse {
risk_score: 0.0,
risk_level: "unknown".to_string(),
data_impact: DataImpactResponse {
records_created: 0,
records_modified: 0,
records_deleted: 0,
tables_affected: Vec::new(),
reversible: true,
},
cost_impact: CostImpactResponse {
api_costs: 0.0,
compute_costs: 0.0,
storage_costs: 0.0,
total_estimated_cost: 0.0,
},
time_impact: TimeImpactResponse {
estimated_duration_seconds: 0,
blocking: false,
},
security_impact: SecurityImpactResponse {
risk_level: "unknown".to_string(),
credentials_accessed: Vec::new(),
external_systems: Vec::new(),
concerns: Vec::new(),
},
},
side_effects: Vec::new(),
recommendations: Vec::new(),
error: Some(format!("Authentication error: {}", e)),
}),
);
}
};
let safety_layer = SafetyLayer::new(Arc::clone(&state));
match simulate_plan_execution(&state, &safety_layer, &plan_id, &session) {
Ok(result) => {
let response = SimulationResponse {
success: result.success,
confidence: result.confidence,
risk_score: result.impact.risk_score,
risk_level: format!("{}", result.impact.risk_level),
step_outcomes: result
.step_outcomes
.iter()
.map(|s| StepOutcomeResponse {
step_id: s.step_id.clone(),
step_name: s.step_name.clone(),
would_succeed: s.would_succeed,
success_probability: s.success_probability,
failure_modes: s
.failure_modes
.iter()
.map(|f| f.failure_type.clone())
.collect(),
})
.collect(),
impact: ImpactResponse {
risk_score: result.impact.risk_score,
risk_level: format!("{}", result.impact.risk_level),
data_impact: DataImpactResponse {
records_created: result.impact.data_impact.records_created,
records_modified: result.impact.data_impact.records_modified,
records_deleted: result.impact.data_impact.records_deleted,
tables_affected: result.impact.data_impact.tables_affected.clone(),
reversible: result.impact.data_impact.reversible,
},
cost_impact: CostImpactResponse {
api_costs: result.impact.cost_impact.api_costs,
compute_costs: result.impact.cost_impact.compute_costs,
storage_costs: result.impact.cost_impact.storage_costs,
total_estimated_cost: result.impact.cost_impact.total_estimated_cost,
},
time_impact: TimeImpactResponse {
estimated_duration_seconds: result
.impact
.time_impact
.estimated_duration_seconds,
blocking: result.impact.time_impact.blocking,
},
security_impact: SecurityImpactResponse {
risk_level: format!("{}", result.impact.security_impact.risk_level),
credentials_accessed: result
.impact
.security_impact
.credentials_accessed
.clone(),
external_systems: result.impact.security_impact.external_systems.clone(),
concerns: result.impact.security_impact.concerns.clone(),
},
},
side_effects: result
.side_effects
.iter()
.map(|s| SideEffectResponse {
effect_type: s.effect_type.clone(),
description: s.description.clone(),
severity: format!("{:?}", s.severity),
mitigation: s.mitigation.clone(),
})
.collect(),
recommendations: result
.recommendations
.iter()
.enumerate()
.map(|(i, r)| RecommendationResponse {
id: format!("rec-{}", i),
recommendation_type: format!("{:?}", r.recommendation_type),
description: r.description.clone(),
action: r.action.clone(),
})
.collect(),
error: None,
};
(StatusCode::OK, Json(response))
}
Err(e) => {
error!("Plan simulation failed: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(SimulationResponse {
success: false,
confidence: 0.0,
risk_score: 1.0,
risk_level: "unknown".to_string(),
step_outcomes: Vec::new(),
impact: ImpactResponse {
risk_score: 1.0,
risk_level: "unknown".to_string(),
data_impact: DataImpactResponse {
records_created: 0,
records_modified: 0,
records_deleted: 0,
tables_affected: Vec::new(),
reversible: true,
},
cost_impact: CostImpactResponse {
api_costs: 0.0,
compute_costs: 0.0,
storage_costs: 0.0,
total_estimated_cost: 0.0,
},
time_impact: TimeImpactResponse {
estimated_duration_seconds: 0,
blocking: false,
},
security_impact: SecurityImpactResponse {
risk_level: "unknown".to_string(),
credentials_accessed: Vec::new(),
external_systems: Vec::new(),
concerns: Vec::new(),
},
},
side_effects: Vec::new(),
recommendations: Vec::new(),
error: Some(e.to_string()),
}),
)
}
}
}
fn get_current_session(
state: &Arc<AppState>,
) -> Result<crate::shared::models::UserSession, Box<dyn std::error::Error + Send + Sync>> {
use crate::shared::models::user_sessions::dsl::*;
use diesel::prelude::*;
let mut conn = state
.conn
.get()
.map_err(|e| format!("DB connection error: {}", e))?;
let session = user_sessions
.order(created_at.desc())
.first::<crate::shared::models::UserSession>(&mut conn)
.optional()
.map_err(|e| format!("DB query error: {}", e))?
.ok_or("No active session found")?;
Ok(session)
}
fn create_auto_task_from_plan(
_state: &Arc<AppState>,
session: &crate::shared::models::UserSession,
plan_id: &str,
execution_mode: ExecutionMode,
priority: TaskPriority,
) -> Result<AutoTask, Box<dyn std::error::Error + Send + Sync>> {
let task = AutoTask {
id: Uuid::new_v4().to_string(),
title: format!("Task from plan {}", plan_id),
intent: String::new(),
status: AutoTaskStatus::Ready,
mode: execution_mode,
priority,
plan_id: Some(plan_id.to_string()),
basic_program: None,
current_step: 0,
total_steps: 0,
progress: 0.0,
step_results: Vec::new(),
pending_decisions: Vec::new(),
pending_approvals: Vec::new(),
risk_summary: None,
resource_usage: crate::auto_task::task_types::ResourceUsage::default(),
error: None,
rollback_state: None,
session_id: session.id.to_string(),
bot_id: session.bot_id.to_string(),
created_by: session.user_id.to_string(),
assigned_to: "auto".to_string(),
schedule: None,
tags: Vec::new(),
parent_task_id: None,
subtask_ids: Vec::new(),
depends_on: Vec::new(),
dependents: Vec::new(),
mcp_servers: Vec::new(),
external_apis: Vec::new(),
created_at: Utc::now(),
updated_at: Utc::now(),
started_at: None,
completed_at: None,
estimated_completion: None,
};
Ok(task)
}
fn start_task_execution(
_state: &Arc<AppState>,
task_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!("Starting task execution task_id={}", task_id);
Ok(())
}
fn list_auto_tasks(
state: &Arc<AppState>,
filter: &str,
limit: i32,
offset: i32,
) -> Result<Vec<AutoTask>, Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
let status_filter = match filter {
"running" => Some("running"),
"pending" => Some("pending"),
"completed" => Some("completed"),
"failed" => Some("failed"),
_ => None,
};
#[derive(QueryableByName)]
struct TaskRow {
#[diesel(sql_type = Text)]
id: String,
#[diesel(sql_type = Text)]
title: String,
#[diesel(sql_type = Text)]
intent: String,
#[diesel(sql_type = Text)]
status: String,
#[diesel(sql_type = diesel::sql_types::Float8)]
progress: f64,
}
let query = if let Some(status) = status_filter {
format!(
"SELECT id::text, title, intent, status, progress FROM auto_tasks WHERE status = '{}' ORDER BY created_at DESC LIMIT {} OFFSET {}",
status, limit, offset
)
} else {
format!(
"SELECT id::text, title, intent, status, progress FROM auto_tasks ORDER BY created_at DESC LIMIT {} OFFSET {}",
limit, offset
)
};
let rows: Vec<TaskRow> = sql_query(&query).get_results(&mut conn).unwrap_or_default();
Ok(rows
.into_iter()
.map(|r| AutoTask {
id: r.id,
title: r.title,
intent: r.intent,
status: match r.status.as_str() {
"running" => AutoTaskStatus::Running,
"completed" => AutoTaskStatus::Completed,
"failed" => AutoTaskStatus::Failed,
"paused" => AutoTaskStatus::Paused,
"cancelled" => AutoTaskStatus::Cancelled,
_ => AutoTaskStatus::Draft,
},
mode: ExecutionMode::FullyAutomatic,
priority: TaskPriority::Medium,
plan_id: None,
basic_program: None,
current_step: 0,
total_steps: 0,
progress: r.progress,
step_results: Vec::new(),
pending_decisions: Vec::new(),
pending_approvals: Vec::new(),
risk_summary: None,
resource_usage: Default::default(),
error: None,
rollback_state: None,
session_id: String::new(),
bot_id: String::new(),
created_by: String::new(),
assigned_to: String::new(),
schedule: None,
tags: Vec::new(),
parent_task_id: None,
subtask_ids: Vec::new(),
depends_on: Vec::new(),
dependents: Vec::new(),
mcp_servers: Vec::new(),
external_apis: Vec::new(),
created_at: Utc::now(),
updated_at: Utc::now(),
started_at: None,
completed_at: None,
estimated_completion: None,
})
.collect())
}
fn get_auto_task_stats(
state: &Arc<AppState>,
) -> Result<AutoTaskStatsResponse, Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
#[derive(QueryableByName)]
struct CountRow {
#[diesel(sql_type = diesel::sql_types::BigInt)]
count: i64,
}
let total: i64 = sql_query("SELECT COUNT(*) as count FROM auto_tasks")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let running: i64 =
sql_query("SELECT COUNT(*) as count FROM auto_tasks WHERE status = 'running'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let pending: i64 =
sql_query("SELECT COUNT(*) as count FROM auto_tasks WHERE status = 'pending'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let completed: i64 =
sql_query("SELECT COUNT(*) as count FROM auto_tasks WHERE status = 'completed'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let failed: i64 = sql_query("SELECT COUNT(*) as count FROM auto_tasks WHERE status = 'failed'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let pending_approval: i64 =
sql_query("SELECT COUNT(*) as count FROM task_approvals WHERE status = 'pending'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let pending_decision: i64 =
sql_query("SELECT COUNT(*) as count FROM task_decisions WHERE status = 'pending'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
Ok(AutoTaskStatsResponse {
total: total as i32,
running: running as i32,
pending: pending as i32,
completed: completed as i32,
failed: failed as i32,
pending_approval: pending_approval as i32,
pending_decision: pending_decision as i32,
})
}
fn update_task_status(
state: &Arc<AppState>,
task_id: &str,
status: AutoTaskStatus,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!(
"Updating task status task_id={} status={:?}",
task_id, status
);
let status_str = match status {
AutoTaskStatus::Running => "running",
AutoTaskStatus::Completed => "completed",
AutoTaskStatus::Failed => "failed",
AutoTaskStatus::Paused => "paused",
AutoTaskStatus::Cancelled => "cancelled",
_ => "pending",
};
let mut conn = state.conn.get()?;
sql_query("UPDATE auto_tasks SET status = $1, updated_at = NOW() WHERE id = $2::uuid")
.bind::<Text, _>(status_str)
.bind::<Text, _>(task_id)
.execute(&mut conn)?;
Ok(())
}
fn create_task_record(
state: &Arc<AppState>,
task_id: Uuid,
session: &crate::shared::models::UserSession,
intent: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
let title = if intent.len() > 100 {
format!("{}...", &intent[..97])
} else {
intent.to_string()
};
sql_query(
"INSERT INTO auto_tasks (id, bot_id, session_id, title, intent, status, execution_mode, priority, progress, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, 'pending', 'autonomous', 'normal', 0.0, NOW(), NOW())"
)
.bind::<DieselUuid, _>(task_id)
.bind::<DieselUuid, _>(session.bot_id)
.bind::<DieselUuid, _>(session.id)
.bind::<Text, _>(&title)
.bind::<Text, _>(intent)
.execute(&mut conn)?;
Ok(())
}
fn update_task_status_db(
state: &Arc<AppState>,
task_id: Uuid,
status: &str,
error: Option<&str>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
if let Some(err) = error {
sql_query(
"UPDATE auto_tasks SET status = $1, error = $2, updated_at = NOW() WHERE id = $3",
)
.bind::<Text, _>(status)
.bind::<Text, _>(err)
.bind::<DieselUuid, _>(task_id)
.execute(&mut conn)?;
} else {
let completed_at = if status == "completed" || status == "failed" {
", completed_at = NOW()"
} else {
""
};
let query = format!(
"UPDATE auto_tasks SET status = $1, updated_at = NOW(){} WHERE id = $2",
completed_at
);
sql_query(&query)
.bind::<Text, _>(status)
.bind::<DieselUuid, _>(task_id)
.execute(&mut conn)?;
}
Ok(())
}
fn get_pending_items_for_bot(state: &Arc<AppState>, bot_id: Uuid) -> Vec<PendingItemResponse> {
let Ok(mut conn) = state.conn.get() else {
return Vec::new();
};
#[derive(QueryableByName)]
struct PendingRow {
#[diesel(sql_type = Text)]
id: String,
#[diesel(sql_type = Text)]
field_label: String,
#[diesel(sql_type = Text)]
config_key: String,
#[diesel(sql_type = diesel::sql_types::Nullable<Text>)]
reason: Option<String>,
}
let rows: Vec<PendingRow> = sql_query(
"SELECT id::text, field_label, config_key, reason FROM pending_info WHERE bot_id = $1 AND is_filled = false"
)
.bind::<DieselUuid, _>(bot_id)
.get_results(&mut conn)
.unwrap_or_default();
rows.into_iter()
.map(|r| PendingItemResponse {
id: r.id,
label: r.field_label,
config_key: r.config_key,
reason: r.reason,
})
.collect()
}
fn simulate_task_execution(
_state: &Arc<AppState>,
safety_layer: &SafetyLayer,
task_id: &str,
session: &crate::shared::models::UserSession,
) -> Result<SimulationResult, Box<dyn std::error::Error + Send + Sync>> {
info!("Simulating task execution task_id={task_id}");
safety_layer.simulate_execution(task_id, session)
}
fn simulate_plan_execution(
_state: &Arc<AppState>,
safety_layer: &SafetyLayer,
plan_id: &str,
session: &crate::shared::models::UserSession,
) -> Result<SimulationResult, Box<dyn std::error::Error + Send + Sync>> {
info!("Simulating plan execution plan_id={plan_id}");
safety_layer.simulate_execution(plan_id, session)
}
fn get_pending_decisions(
_state: &Arc<AppState>,
task_id: &str,
) -> Result<Vec<PendingDecision>, Box<dyn std::error::Error + Send + Sync>> {
trace!("Getting pending decisions for task_id={}", task_id);
Ok(Vec::new())
}
fn submit_decision(
_state: &Arc<AppState>,
task_id: &str,
request: &DecisionRequest,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!(
"Submitting decision task_id={} decision_id={}",
task_id, request.decision_id
);
Ok(())
}
fn get_pending_approvals(
_state: &Arc<AppState>,
task_id: &str,
) -> Result<Vec<PendingApproval>, Box<dyn std::error::Error + Send + Sync>> {
trace!("Getting pending approvals for task_id={}", task_id);
Ok(Vec::new())
}
fn submit_approval(
_state: &Arc<AppState>,
task_id: &str,
request: &ApprovalRequest,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!(
"Submitting approval task_id={} approval_id={} action={}",
task_id, request.approval_id, request.action
);
Ok(())
}
fn render_task_list_html(tasks: &[AutoTask]) -> String {
if tasks.is_empty() {
return r#"<div class="empty-state"><p>No tasks found</p></div>"#.to_string();
}
use std::fmt::Write;
let mut html = String::from(r#"<div class="task-list">"#);
for task in tasks {
let _ = write!(
html,
r#"<div class="task-item" data-task-id="{}">
<div class="task-title">{}</div>
<div class="task-status">{}</div>
<div class="task-progress">{}%</div>
</div>"#,
html_escape(&task.id),
html_escape(&task.title),
html_escape(&task.status.to_string()),
(task.progress * 100.0) as i32
);
}
html.push_str("</div>");
html
}
fn html_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
}
// =============================================================================
// MISSING ENDPOINTS - Required by botui/autotask.js
// =============================================================================
pub async fn execute_task_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
) -> impl IntoResponse {
info!("Executing task: {}", task_id);
match start_task_execution(&state, &task_id) {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({
"success": true,
"task_id": task_id,
"message": "Task execution started"
})),
)
.into_response(),
Err(e) => {
error!("Failed to execute task {}: {}", task_id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"success": false,
"error": e.to_string()
})),
)
.into_response()
}
}
}
pub async fn get_task_logs_handler(
State(state): State<Arc<AppState>>,
Path(task_id): Path<String>,
) -> impl IntoResponse {
info!("Getting logs for task: {}", task_id);
let logs = get_task_logs(&state, &task_id);
(
StatusCode::OK,
Json(serde_json::json!({
"task_id": task_id,
"logs": logs
})),
)
.into_response()
}
pub async fn apply_recommendation_handler(
State(state): State<Arc<AppState>>,
Path(rec_id): Path<String>,
) -> impl IntoResponse {
info!("Applying recommendation: {}", rec_id);
match apply_recommendation(&state, &rec_id) {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({
"success": true,
"recommendation_id": rec_id,
"message": "Recommendation applied successfully"
})),
)
.into_response(),
Err(e) => {
error!("Failed to apply recommendation {}: {}", rec_id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"success": false,
"error": e.to_string()
})),
)
.into_response()
}
}
}
// =============================================================================
// HELPER FUNCTIONS FOR NEW ENDPOINTS
// =============================================================================
fn get_task_logs(_state: &Arc<AppState>, task_id: &str) -> Vec<serde_json::Value> {
// TODO: Fetch from database when task execution is implemented
vec![
serde_json::json!({
"timestamp": Utc::now().to_rfc3339(),
"level": "info",
"message": format!("Task {} initialized", task_id)
}),
serde_json::json!({
"timestamp": Utc::now().to_rfc3339(),
"level": "info",
"message": "Waiting for execution"
}),
]
}
fn apply_recommendation(
_state: &Arc<AppState>,
rec_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!("Applying recommendation: {}", rec_id);
// TODO: Implement recommendation application logic
Ok(())
}
#[derive(Debug, Serialize)]
pub struct PendingItemsResponse {
pub items: Vec<PendingItemResponse>,
pub count: usize,
}
pub async fn get_pending_items_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
info!("Getting pending items");
let Ok(session) = get_current_session(&state) else {
return (
StatusCode::UNAUTHORIZED,
Json(PendingItemsResponse {
items: Vec::new(),
count: 0,
}),
)
.into_response();
};
let items = get_pending_items_for_bot(&state, session.bot_id);
(
StatusCode::OK,
Json(PendingItemsResponse {
count: items.len(),
items,
}),
)
.into_response()
}
#[derive(Debug, Deserialize)]
pub struct SubmitPendingItemRequest {
pub value: String,
}
pub async fn submit_pending_item_handler(
State(state): State<Arc<AppState>>,
Path(item_id): Path<String>,
Json(request): Json<SubmitPendingItemRequest>,
) -> impl IntoResponse {
info!("Submitting pending item {item_id}: {}", request.value);
let Ok(session) = get_current_session(&state) else {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"success": false,
"error": "Authentication required"
})),
)
.into_response();
};
match resolve_pending_item(&state, &item_id, &request.value, session.bot_id) {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({
"success": true,
"message": "Pending item resolved"
})),
)
.into_response(),
Err(e) => {
error!("Failed to resolve pending item {item_id}: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"success": false,
"error": e.to_string()
})),
)
.into_response()
}
}
}
fn resolve_pending_item(
state: &Arc<AppState>,
item_id: &str,
value: &str,
bot_id: Uuid,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
let item_uuid = Uuid::parse_str(item_id)?;
sql_query(
"UPDATE pending_info SET resolved = true, resolved_value = $1, resolved_at = NOW()
WHERE id = $2 AND bot_id = $3",
)
.bind::<Text, _>(value)
.bind::<DieselUuid, _>(item_uuid)
.bind::<DieselUuid, _>(bot_id)
.execute(&mut conn)?;
info!("Resolved pending item {item_id} with value: {value}");
Ok(())
}