mockforge_intelligence/handlers/drift_budget/
gitops.rs1use super::DriftBudgetStateProvider;
4use super::DriftGitOps;
5use crate::handlers::structural_incidents::StructuralIncidents;
6use axum::{extract::State, http::StatusCode, response::Json};
7use mockforge_foundation::incidents_types::{
8 DriftIncident, IncidentQuery, IncidentStatus, IncidentType,
9};
10use serde::Deserialize;
11
12#[derive(Debug, Deserialize)]
14pub struct GeneratePRRequest {
15 pub incident_ids: Option<Vec<String>>,
17 pub workspace_id: Option<String>,
19 pub status: Option<String>,
21}
22
23fn emit_threshold_event<S: DriftBudgetStateProvider>(
24 state: &S,
25 incidents: &[DriftIncident],
26 workspace_override: Option<&str>,
27) {
28 let exceeded = incidents
29 .iter()
30 .filter(|incident| matches!(incident.incident_type, IncidentType::ThresholdExceeded))
31 .count();
32 if exceeded == 0 {
33 return;
34 }
35 let workspace = workspace_override
36 .and_then(|id| uuid::Uuid::parse_str(id).ok())
37 .or_else(|| {
38 incidents
39 .first()
40 .and_then(|incident| incident.workspace_id.as_deref())
41 .and_then(|id| uuid::Uuid::parse_str(id).ok())
42 })
43 .unwrap_or_else(uuid::Uuid::new_v4);
44 let endpoint = incidents
45 .first()
46 .map(|incident| format!("{} {}", incident.method, incident.endpoint))
47 .unwrap_or_else(|| "unknown".to_string());
48 state.publish_drift_threshold(workspace, endpoint, exceeded as i32, incidents.len() as i32);
49}
50
51pub async fn generate_gitops_pr<S: DriftBudgetStateProvider>(
55 State(state): State<S>,
56 Json(request): Json<GeneratePRRequest>,
57) -> Result<Json<serde_json::Value>, StatusCode> {
58 let handler = state.gitops_handler().ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
59
60 let mut query = IncidentQuery::default();
62
63 if let Some(incident_ids) = &request.incident_ids {
64 let all_incidents = state.incident_manager().query_incidents(query).await;
67 let incidents: Vec<_> =
68 all_incidents.into_iter().filter(|inc| incident_ids.contains(&inc.id)).collect();
69
70 match handler.generate_pr_from_incidents(&incidents).await {
71 Ok(Some(pr_result)) => {
72 emit_threshold_event(&state, &incidents, None);
73
74 Ok(Json(serde_json::json!({
75 "success": true,
76 "pr": pr_result,
77 })))
78 }
79 Ok(None) => Ok(Json(serde_json::json!({
80 "success": false,
81 "message": "No PR generated (no file changes or incidents)",
82 }))),
83 Err(_e) => Err(StatusCode::INTERNAL_SERVER_ERROR),
84 }
85 } else {
86 let workspace_id_str = request.workspace_id.clone();
88 query.workspace_id = request.workspace_id;
89 if let Some(status_str) = &request.status {
90 query.status = match status_str.as_str() {
91 "open" => Some(IncidentStatus::Open),
92 "acknowledged" => Some(IncidentStatus::Acknowledged),
93 _ => None,
94 };
95 }
96
97 let incidents = state.incident_manager().query_incidents(query).await;
98
99 match handler.generate_pr_from_incidents(&incidents).await {
100 Ok(Some(pr_result)) => {
101 emit_threshold_event(&state, &incidents, workspace_id_str.as_deref());
102
103 Ok(Json(serde_json::json!({
104 "success": true,
105 "pr": pr_result,
106 "incidents_included": incidents.len(),
107 })))
108 }
109 Ok(None) => Ok(Json(serde_json::json!({
110 "success": false,
111 "message": "No PR generated (no file changes or incidents)",
112 }))),
113 Err(_e) => Err(StatusCode::INTERNAL_SERVER_ERROR),
114 }
115 }
116}