mockforge_ui/handlers/
admin.rs1use axum::{
7 extract::{Query, State},
8 response::Json,
9};
10use chrono::Utc;
11use serde_json::{json, Value};
12use std::collections::HashMap;
13
14use crate::handlers::AdminState;
15use crate::models::*;
16
17#[derive(Debug, Clone, Default)]
19pub struct RequestMetrics {
20 pub total_requests: u64,
22 pub active_requests: u64,
24 pub average_response_time: f64,
26 pub requests_per_second: f64,
28 pub total_errors: u64,
30}
31
32pub async fn get_server_info(State(state): State<AdminState>) -> Json<Value> {
34 Json(json!({
35 "http_server": state.http_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
36 "ws_server": state.ws_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
37 "grpc_server": state.grpc_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
38 "graphql_server": state.graphql_server_addr.map(|addr| addr.to_string()).unwrap_or_else(|| "disabled".to_string()),
39 "api_enabled": state.api_enabled,
40 }))
41}
42
43pub async fn get_health() -> Json<HealthCheck> {
45 Json(HealthCheck {
46 status: "healthy".to_string(),
47 services: HashMap::new(),
48 last_check: Utc::now(),
49 issues: Vec::new(),
50 })
51}
52
53pub async fn get_logs(
55 State(_state): State<AdminState>,
56 Query(params): Query<HashMap<String, String>>,
57) -> Json<ApiResponse<Vec<LogEntry>>> {
58 let limit = params.get("limit").and_then(|s| s.parse::<usize>().ok()).unwrap_or(100);
60
61 let method_filter = params.get("method").map(|s| s.to_string());
62 let path_filter = params.get("path").map(|s| s.to_string());
63 let status_filter = params.get("status").and_then(|s| s.parse::<u16>().ok());
64
65 let request_logs = if let Some(global_logger) = mockforge_core::get_global_logger() {
67 global_logger.get_recent_logs(Some(limit * 2)).await
68 } else {
69 Vec::new()
70 };
71
72 let mut log_entries: Vec<LogEntry> = request_logs
74 .into_iter()
75 .filter(|log| {
76 log.server_type == "HTTP"
78 })
79 .filter(|log| {
80 method_filter.as_ref().is_none_or(|filter| log.method == *filter)
82 })
83 .filter(|log| {
84 path_filter.as_ref().is_none_or(|filter| log.path.contains(filter))
86 })
87 .filter(|log| {
88 status_filter.is_none_or(|filter| log.status_code == filter)
90 })
91 .map(|log| LogEntry {
92 timestamp: log.timestamp,
93 status: log.status_code,
94 method: log.method,
95 url: log.path,
96 response_time: log.response_time_ms,
97 size: log.response_size_bytes,
98 })
99 .take(limit)
100 .collect();
101
102 log_entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
104
105 Json(ApiResponse::success(log_entries))
106}
107
108pub async fn get_metrics(State(state): State<AdminState>) -> Json<ApiResponse<SimpleMetricsData>> {
110 let metrics = state.metrics.read().await;
111 let error_rate = 0.0; Json(ApiResponse::success(SimpleMetricsData {
114 total_requests: metrics.total_requests,
115 active_requests: metrics.active_connections, average_response_time: 0.0, error_rate,
118 }))
119}
120
121pub async fn update_latency(
123 State(state): State<super::AdminState>,
124 Json(config): Json<Value>,
125) -> Json<ApiResponse<String>> {
126 let base_ms = config.get("base_ms").and_then(|v| v.as_u64()).unwrap_or(50);
128 let jitter_ms = config.get("jitter_ms").and_then(|v| v.as_u64()).unwrap_or(20);
129 let tag_overrides = config
130 .get("tag_overrides")
131 .and_then(|v| v.as_object())
132 .map(|obj| obj.iter().filter_map(|(k, v)| v.as_u64().map(|val| (k.clone(), val))).collect())
133 .unwrap_or_default();
134
135 state.update_latency_config(base_ms, jitter_ms, tag_overrides).await;
137
138 tracing::info!("Updated latency profile: base_ms={}, jitter_ms={}", base_ms, jitter_ms);
139 Json(ApiResponse::success("Latency configuration updated".to_string()))
140}
141
142pub async fn update_faults(
144 State(state): State<super::AdminState>,
145 Json(config): Json<Value>,
146) -> Json<ApiResponse<String>> {
147 let enabled = config.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
149 let failure_rate = config.get("failure_rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
150 let status_codes = config
151 .get("status_codes")
152 .and_then(|v| v.as_array())
153 .map(|arr| arr.iter().filter_map(|v| v.as_u64().map(|n| n as u16)).collect())
154 .unwrap_or_default();
155
156 state.update_fault_config(enabled, failure_rate, status_codes).await;
158
159 tracing::info!("Updated fault config: enabled={}, failure_rate={}", enabled, failure_rate);
160 Json(ApiResponse::success("Fault configuration updated".to_string()))
161}
162
163pub async fn update_proxy(
165 State(state): State<super::AdminState>,
166 Json(config): Json<Value>,
167) -> Json<ApiResponse<String>> {
168 let enabled = config.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
170 let upstream_url = config.get("upstream_url").and_then(|v| v.as_str()).map(|s| s.to_string());
171 let timeout_seconds = config.get("timeout_seconds").and_then(|v| v.as_u64()).unwrap_or(30);
172
173 state.update_proxy_config(enabled, upstream_url.clone(), timeout_seconds).await;
175
176 tracing::info!(
177 "Updated proxy config: enabled={}, upstream_url={:?}, timeout_seconds={}",
178 enabled,
179 upstream_url,
180 timeout_seconds
181 );
182 Json(ApiResponse::success("Proxy configuration updated".to_string()))
183}
184
185pub async fn clear_logs(State(_state): State<AdminState>) -> Json<ApiResponse<String>> {
187 if let Some(global_logger) = mockforge_core::get_global_logger() {
188 global_logger.clear_logs().await;
189 }
190 tracing::info!("Request logs cleared via admin UI");
191 Json(ApiResponse::success("Logs cleared".to_string()))
192}
193
194pub async fn restart_servers(State(state): State<super::AdminState>) -> Json<ApiResponse<String>> {
196 let current_status = state.get_restart_status().await;
198 if current_status.in_progress {
199 return Json(ApiResponse::error("Server restart already in progress".to_string()));
200 }
201
202 if let Err(e) = state
204 .initiate_restart("Manual restart requested via admin UI".to_string())
205 .await
206 {
207 return Json(ApiResponse::error(format!("Failed to initiate restart: {}", e)));
208 }
209
210 let state_clone = state.clone();
212 tokio::spawn(async move {
213 if let Err(e) = super::perform_server_restart(&state_clone).await {
214 tracing::error!("Server restart failed: {}", e);
215 state_clone.complete_restart(false).await;
216 } else {
217 tracing::info!("Server restart completed successfully");
218 state_clone.complete_restart(true).await;
219 }
220 });
221
222 tracing::info!("Server restart initiated via admin UI");
223 Json(ApiResponse::success(
224 "Server restart initiated. Please wait for completion.".to_string(),
225 ))
226}
227
228pub async fn get_restart_status(
230 State(state): State<super::AdminState>,
231) -> Json<ApiResponse<super::RestartStatus>> {
232 let status = state.get_restart_status().await;
233 Json(ApiResponse::success(status))
234}
235
236pub async fn get_config(State(state): State<super::AdminState>) -> Json<ApiResponse<Value>> {
238 let config = state.get_config().await;
239 Json(ApiResponse::success(serde_json::to_value(config).unwrap_or_else(|_| json!({}))))
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn create_test_state() -> super::AdminState {
247 super::AdminState::new(None, None, None, None, false, 8080)
248 }
249
250 #[tokio::test]
251 async fn test_get_restart_status() {
252 let state = create_test_state();
253 let response = get_restart_status(axum::extract::State(state)).await;
254
255 assert!(response.0.success);
256 }
257
258 #[tokio::test]
259 async fn test_get_config() {
260 let state = create_test_state();
261 let response = get_config(axum::extract::State(state)).await;
262
263 assert!(response.0.success);
264 }
265}