use crate::error::AppError; use crate::models::credential::TuleapCredentials; use crate::models::module::{ProjectModule, MODULE_TULEAP_AUTO_RESOLVE}; use crate::models::ticket::{ProcessedTicket, ProjectThroughputStats}; use crate::models::tracker::WatchedTracker; use crate::services::tuleap_client::TuleapClient; use crate::services::{crypto, filter_engine, notifier}; use crate::AppState; use tauri::{Emitter, State}; #[tauri::command] pub async fn manual_poll( state: State<'_, AppState>, app_handle: tauri::AppHandle, tracker_id: String, ) -> Result, AppError> { let (tracker, client) = { let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; let tracker = WatchedTracker::get_by_id(&db, &tracker_id)?; let module_enabled = ProjectModule::is_enabled(&db, &tracker.project_id, MODULE_TULEAP_AUTO_RESOLVE)?; if !module_enabled { return Err(AppError::from( "Le module Polling Tuleap + auto-resolve est désactivé pour ce projet".to_string(), )); } if tracker.status != "valid" { return Err(AppError::from( "Tracker is invalid. Reconfigure analyst/developer agents first.".to_string(), )); } let cred = TuleapCredentials::get(&db)? .ok_or_else(|| AppError::from("No Tuleap credentials configured".to_string()))?; let password = crypto::decrypt(&state.encryption_key, &cred.password_encrypted) .map_err(AppError::from)?; let client = TuleapClient::new( &state.http_client, &cred.tuleap_url, &cred.username, &password, ); (tracker, client) }; // lock dropped here let _ = app_handle.emit( "polling-started", serde_json::json!({ "project_id": &tracker.project_id, "tracker_id": &tracker.id, "tracker_label": &tracker.tracker_label, "source": "manual", }), ); let artifacts = match client.get_artifacts(tracker.tracker_id).await { Ok(a) => a, Err(e) => { let _ = app_handle.emit( "polling-error", serde_json::json!({ "project_id": &tracker.project_id, "tracker_id": &tracker.id, "tracker_label": &tracker.tracker_label, "source": "manual", "error": e, }), ); return Err(AppError::from(e)); } }; let filtered = filter_engine::apply_filters(&artifacts, &tracker.filters); let mut newly_inserted: Vec = Vec::new(); { let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; for artifact in &filtered { let artifact_id = artifact.get("id").and_then(|v| v.as_i64()).unwrap_or(0) as i32; let artifact_title = artifact .get("title") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let artifact_data = serde_json::to_string(artifact).unwrap_or_else(|_| "{}".to_string()); if let Some(ticket) = ProcessedTicket::insert_if_new( &db, &tracker.id, artifact_id, &artifact_title, &artifact_data, )? { newly_inserted.push(ticket); } } WatchedTracker::update_last_polled(&db, &tracker.id)?; } if !newly_inserted.is_empty() { let _ = app_handle.emit( "new-tickets-detected", serde_json::json!({ "project_id": &tracker.project_id, "tracker_id": &tracker.id, "tracker_label": &tracker.tracker_label, "count": newly_inserted.len(), }), ); for ticket in &newly_inserted { notifier::notify_new_ticket( &state.db, &app_handle, &tracker.project_id, &ticket.id, ticket.artifact_id, &ticket.artifact_title, ); } } let _ = app_handle.emit( "polling-finished", serde_json::json!({ "project_id": &tracker.project_id, "tracker_id": &tracker.id, "tracker_label": &tracker.tracker_label, "source": "manual", "new_tickets_count": newly_inserted.len(), }), ); Ok(newly_inserted) } #[tauri::command] pub fn get_queue_status( state: State<'_, AppState>, project_id: String, ) -> Result, AppError> { let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; let tickets = ProcessedTicket::list_by_project(&db, &project_id)?; Ok(tickets) } #[tauri::command] pub fn get_project_throughput( state: State<'_, AppState>, project_id: String, ) -> Result { let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; let stats = ProcessedTicket::get_project_throughput_stats(&db, &project_id)?; Ok(stats) }