use axum::{ extract::{Multipart, Path, State}, Json, }; use crate::phd2::parse_phd2_log; use super::{AppError, AppState}; pub async fn upload_phd2( State(state): State, mut multipart: Multipart, ) -> Result, AppError> { let mut filename = String::new(); let mut content = String::new(); while let Some(field) = multipart.next_field().await.map_err(|e| AppError::Internal(e.to_string()))? { let name = field.name().unwrap_or("").to_string(); match name.as_str() { "file" => { filename = field.file_name().unwrap_or("phd2.log").to_string(); let bytes = field.bytes().await.map_err(|e| AppError::Internal(e.to_string()))?; content = String::from_utf8_lossy(&bytes).to_string(); } _ => {} } } if content.is_empty() { return Err(AppError::BadRequest("No file content".to_string())); } let analysis = parse_phd2_log(&content) .map_err(|e| AppError::BadRequest(format!("PHD2 parse error: {}", e)))?; let session_date = &analysis.session_date; // Check for duplicates: same session_date, similar duration, and similar RMS stats let existing: Option<(i32, i32)> = sqlx::query_as( r#"SELECT id, duration_min FROM phd2_logs WHERE session_date = ? AND abs(duration_min - ?) < 2 AND abs(rms_total - ?) < 0.1 LIMIT 1"# ) .bind(session_date) .bind(analysis.duration_min as i32) .bind(analysis.rms_total_arcsec) .fetch_optional(&state.pool) .await?; if let Some((dup_id, _)) = existing { return Ok(Json(serde_json::json!({ "duplicate": true, "duplicate_id": dup_id, "message": format!("Duplicate session detected (ID: {}). Not inserted.", dup_id), "analysis": analysis, "filename": filename, }))); } let id: i64 = sqlx::query_scalar( r#"INSERT INTO phd2_logs (session_date, filename, rms_total, rms_ra, rms_dec, peak_error, star_lost_count, duration_min, guide_star_snr) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id"#, ) .bind(session_date) .bind(&filename) .bind(analysis.rms_total_arcsec) .bind(analysis.rms_ra_arcsec) .bind(analysis.rms_dec_arcsec) .bind(analysis.peak_error_arcsec) .bind(analysis.star_lost_count) .bind(analysis.duration_min) .bind(analysis.mean_snr) .fetch_one(&state.pool) .await?; Ok(Json(serde_json::json!({ "id": id, "duplicate": false, "analysis": analysis, "filename": filename, }))) } pub async fn list_phd2( State(state): State, ) -> Result, AppError> { let rows = sqlx::query( "SELECT * FROM phd2_logs ORDER BY session_date DESC, created_at DESC", ) .fetch_all(&state.pool) .await?; let items: Vec = rows.iter().map(|r| { use sqlx::Row; serde_json::json!({ "id": r.try_get::("id").unwrap_or_default(), "session_date": r.try_get::("session_date").unwrap_or_default(), "filename": r.try_get::("filename").unwrap_or_default(), "rms_total": r.try_get::, _>("rms_total").unwrap_or_default(), "rms_ra": r.try_get::, _>("rms_ra").unwrap_or_default(), "rms_dec": r.try_get::, _>("rms_dec").unwrap_or_default(), "peak_error": r.try_get::, _>("peak_error").unwrap_or_default(), "star_lost_count": r.try_get::, _>("star_lost_count").unwrap_or_default(), "duration_min": r.try_get::, _>("duration_min").unwrap_or_default(), "guide_star_snr": r.try_get::, _>("guide_star_snr").unwrap_or_default(), "created_at": r.try_get::("created_at").unwrap_or_default(), }) }).collect(); Ok(Json(serde_json::json!({ "items": items }))) } pub async fn get_phd2( State(state): State, Path(id): Path, ) -> Result, AppError> { let row = sqlx::query("SELECT * FROM phd2_logs WHERE id = ?") .bind(id) .fetch_optional(&state.pool) .await? .ok_or_else(|| AppError::NotFound(format!("PHD2 log {} not found", id)))?; use sqlx::Row; Ok(Json(serde_json::json!({ "id": row.try_get::("id").unwrap_or_default(), "session_date": row.try_get::("session_date").unwrap_or_default(), "filename": row.try_get::("filename").unwrap_or_default(), "rms_total": row.try_get::, _>("rms_total").unwrap_or_default(), "rms_ra": row.try_get::, _>("rms_ra").unwrap_or_default(), "rms_dec": row.try_get::, _>("rms_dec").unwrap_or_default(), "peak_error": row.try_get::, _>("peak_error").unwrap_or_default(), "star_lost_count": row.try_get::, _>("star_lost_count").unwrap_or_default(), "duration_min": row.try_get::, _>("duration_min").unwrap_or_default(), "guide_star_snr": row.try_get::, _>("guide_star_snr").unwrap_or_default(), }))) } pub async fn delete_phd2( State(state): State, Path(id): Path, ) -> Result, AppError> { let result = sqlx::query("DELETE FROM phd2_logs WHERE id = ?") .bind(id) .execute(&state.pool) .await?; if result.rows_affected() == 0 { return Err(AppError::NotFound(format!("PHD2 log {} not found", id))); } Ok(Json(serde_json::json!({ "status": "deleted", "id": id, }))) }