use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use serde::{Deserialize, Serialize};
use atomcode_core::auth;
use atomcode_core::coding_plan;
use atomcode_telemetry::{CodingplanErrorKind, CodingplanResult, Event};
use crate::{
api_auth::{pending_invite_for_login, poll_login_session, LoginPollStep},
api_config::{cleanup_expired_sessions, config_response, load_config, save_config},
daemon_scope, json_error, AppState,
};
#[derive(Debug, Deserialize)]
pub(crate) struct CodingPlanSetupRequest {
pub login_id: Option<String>,
}
#[derive(Debug, Serialize)]
struct CodingPlanSetupResponse {
success: bool,
report_text: String,
default_provider: String,
providers: Vec<crate::ProviderInfo>,
steps: SetupSteps,
}
#[derive(Debug, Serialize)]
struct SetupSteps {
login: StepInfo,
claim: StepInfo,
models: StepInfo,
status: StepInfo,
}
#[derive(Debug, Serialize)]
struct StepInfo {
status: String,
message: String,
}
pub(crate) async fn codingplan_setup(
State(state): State<AppState>,
axum::Extension(client_mode): axum::Extension<atomcode_telemetry::SessionMode>,
Json(req): Json<CodingPlanSetupRequest>,
) -> impl IntoResponse {
let state_clone = state.clone();
daemon_scope(&state, None, client_mode, || async move {
let state = state_clone;
cleanup_expired_sessions(&state.login_sessions).await;
let is_logged_in = auth::get_stored_auth().is_some();
if !is_logged_in {
match req.login_id {
None => {
state.telemetry.track(Event::TakeCodingplan {
type_: CodingplanResult::Fail,
error_kind: Some(CodingplanErrorKind::AuthError),
error_data: Some(serde_json::json!({
"step": "login",
"message": "Not logged in. Call /auth/login/start first.",
}).to_string()),
});
return json_error(
StatusCode::UNAUTHORIZED,
"Not logged in. Call /auth/login/start first.",
)
.into_response();
}
Some(login_id) => {
match poll_login_session(&state, &login_id).await {
Ok(LoginPollStep::Pending) => {
state.telemetry.track(Event::TakeCodingplan {
type_: CodingplanResult::Fail,
error_kind: Some(CodingplanErrorKind::AuthError),
error_data: Some(serde_json::json!({
"step": "login",
"message": "Login still pending",
}).to_string()),
});
return (
StatusCode::CONFLICT,
Json(serde_json::json!({
"success": false,
"status": "login_pending",
"error": "Login still pending. Poll /auth/login/:login_id/poll until authorized."
})),
)
.into_response();
}
Ok(LoginPollStep::Authorized(user)) => {
state
.telemetry
.set_account_id(Some(user.id.clone()));
let (invite_code, install_uuid) = pending_invite_for_login();
state.telemetry.track(Event::LoginSuccess {
invite_code,
install_uuid,
});
}
Err((status, message)) => {
state.telemetry.track(Event::TakeCodingplan {
type_: CodingplanResult::Fail,
error_kind: Some(CodingplanErrorKind::AuthError),
error_data: Some(serde_json::json!({
"step": "login",
"message": message,
}).to_string()),
});
return json_error(status, message).into_response();
}
}
}
}
}
let mut config = match load_config() {
Ok(c) => c,
Err(e) => {
state.telemetry.track(Event::TakeCodingplan {
type_: CodingplanResult::Fail,
error_kind: Some(CodingplanErrorKind::ExecutionFailed),
error_data: Some(serde_json::json!({
"step": "config_save",
"message": e,
}).to_string()),
});
return json_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response();
}
};
let setup_result = tokio::task::spawn_blocking(move || {
let report = coding_plan::run(&mut config, None)?;
Ok::<_, anyhow::Error>((config, report))
})
.await;
let (config, report) = match setup_result {
Ok(Ok(v)) => v,
Ok(Err(e)) => {
state.telemetry.track(Event::TakeCodingplan {
type_: CodingplanResult::Fail,
error_kind: Some(CodingplanErrorKind::ExecutionFailed),
error_data: Some(serde_json::json!({
"step": "claim",
"message": format!("CodingPlan setup failed: {:#}", e),
}).to_string()),
});
return json_error(
StatusCode::INTERNAL_SERVER_ERROR,
format!("CodingPlan setup failed: {:#}", e),
)
.into_response();
}
Err(e) => {
state.telemetry.track(Event::TakeCodingplan {
type_: CodingplanResult::Fail,
error_kind: Some(CodingplanErrorKind::ExecutionFailed),
error_data: Some(serde_json::json!({
"step": "claim",
"message": format!("CodingPlan setup task failed: {:#}", e),
}).to_string()),
});
return json_error(
StatusCode::INTERNAL_SERVER_ERROR,
format!("CodingPlan setup task failed: {:#}", e),
)
.into_response();
}
};
let result_type = if report.should_persist_config() {
CodingplanResult::Success
} else {
CodingplanResult::Fail
};
if report.should_persist_config() {
if let Err(e) = save_config(&config) {
state.telemetry.track(Event::TakeCodingplan {
type_: result_type,
error_kind: Some(CodingplanErrorKind::ExecutionFailed),
error_data: Some(serde_json::json!({
"step": "config_save",
"message": e,
}).to_string()),
});
return json_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response();
}
if let Err(e) = coding_plan::write_last_sync_now() {
state.telemetry.track(Event::TakeCodingplan {
type_: result_type,
error_kind: Some(CodingplanErrorKind::ExecutionFailed),
error_data: Some(serde_json::json!({
"step": "sync_marker",
"message": format!("Failed to write CodingPlan sync marker: {:#}", e),
}).to_string()),
});
return json_error(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to write CodingPlan sync marker: {:#}", e),
)
.into_response();
}
}
state.telemetry.track(Event::TakeCodingplan {
type_: result_type,
error_kind: None,
error_data: if result_type == CodingplanResult::Success {
Some(serde_json::json!({
"step": null,
}).to_string())
} else {
None
},
});
let report_text = report.render();
let steps = SetupSteps {
login: step_info_from_result(&report.login),
claim: step_info_from_result(&report.claim),
models: step_info_from_result(&report.models),
status: step_info_from_result(&report.status),
};
let config_resp = config_response(&config);
Json(CodingPlanSetupResponse {
success: report.should_persist_config(),
report_text,
default_provider: config_resp.default_provider,
providers: config_resp.providers,
steps,
})
.into_response()
})
.await
}
fn step_info_from_result<T: std::fmt::Debug>(result: &coding_plan::StepResult<T>) -> StepInfo {
match result {
coding_plan::StepResult::Ok(_) => StepInfo {
status: "ok".to_string(),
message: String::new(),
},
coding_plan::StepResult::Skipped(msg) => StepInfo {
status: "skipped".to_string(),
message: msg.clone(),
},
coding_plan::StepResult::Err(msg) => StepInfo {
status: "error".to_string(),
message: msg.clone(),
},
}
}