mockforge_ui/handlers/
admin.rs

1//! Admin dashboard and server management handlers
2//!
3//! This module handles admin dashboard operations, server management,
4//! metrics, logs, and configuration.
5
6use 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/// Request metrics for tracking
18#[derive(Debug, Clone, Default)]
19pub struct RequestMetrics {
20    /// Total requests served
21    pub total_requests: u64,
22    /// Active requests currently being processed
23    pub active_requests: u64,
24    /// Average response time in milliseconds
25    pub average_response_time: f64,
26    /// Request rate per second
27    pub requests_per_second: f64,
28    /// Total errors encountered
29    pub total_errors: u64,
30}
31
32/// Get server information
33pub 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
43/// Get health check status
44pub 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
53/// Get logs
54pub async fn get_logs(
55    State(_state): State<AdminState>,
56    Query(params): Query<HashMap<String, String>>,
57) -> Json<ApiResponse<Vec<LogEntry>>> {
58    // Parse query parameters for filtering
59    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    // Get recent logs from the centralized logger
66    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    // Convert RequestLogEntry to LogEntry and apply filters
73    let mut log_entries: Vec<LogEntry> = request_logs
74        .into_iter()
75        .filter(|log| {
76            // Only include HTTP logs for now (matching the UI interface)
77            log.server_type == "HTTP"
78        })
79        .filter(|log| {
80            // Apply method filter
81            method_filter.as_ref().is_none_or(|filter| log.method == *filter)
82        })
83        .filter(|log| {
84            // Apply path filter (simple substring match)
85            path_filter.as_ref().is_none_or(|filter| log.path.contains(filter))
86        })
87        .filter(|log| {
88            // Apply status filter
89            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    // Sort by timestamp descending (most recent first)
103    log_entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
104
105    Json(ApiResponse::success(log_entries))
106}
107
108/// Get metrics data
109pub async fn get_metrics(State(state): State<AdminState>) -> Json<ApiResponse<SimpleMetricsData>> {
110    let metrics = state.metrics.read().await;
111    let error_rate = 0.0; // Note: total_errors field doesn't exist in this RequestMetrics, setting to 0.0
112                          // Note: Some fields from the original RequestMetrics aren't available, using defaults
113    Json(ApiResponse::success(SimpleMetricsData {
114        total_requests: metrics.total_requests,
115        active_requests: metrics.active_connections, // Using active_connections as proxy
116        average_response_time: 0.0, // This field doesn't exist in this RequestMetrics
117        error_rate,
118    }))
119}
120
121/// Update latency configuration
122pub async fn update_latency(
123    State(state): State<super::AdminState>,
124    Json(config): Json<Value>,
125) -> Json<ApiResponse<String>> {
126    // Extract latency configuration from the JSON
127    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    // Update the configuration
136    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
142/// Update fault injection configuration
143pub async fn update_faults(
144    State(state): State<super::AdminState>,
145    Json(config): Json<Value>,
146) -> Json<ApiResponse<String>> {
147    // Extract fault configuration from the JSON
148    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    // Update the configuration
157    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
163/// Update proxy configuration
164pub async fn update_proxy(
165    State(state): State<super::AdminState>,
166    Json(config): Json<Value>,
167) -> Json<ApiResponse<String>> {
168    // Extract proxy configuration from the JSON
169    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    // Update the configuration
174    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
185/// Clear logs
186pub 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
194/// Restart servers
195pub async fn restart_servers(State(state): State<super::AdminState>) -> Json<ApiResponse<String>> {
196    // Check if restart is already in progress
197    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    // Initiate restart status
203    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    // Spawn restart task to avoid blocking the response
211    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
228/// Get restart status
229pub 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
236/// Get configuration
237pub 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}