use axum::{ extract::State, http::{header, StatusCode}, response::{ sse::{Event, KeepAlive, Sse}, IntoResponse, Json, Response, }, }; use serde_json::{json, Value}; use std::convert::Infallible; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use crate::{ capture::load_all_captures, error::{BridgeError, BridgeResult}, proxy::ProxyState, }; /// List all captured requests. pub async fn get_captures(State(state): State) -> BridgeResult> { let captures = load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?; let unknown: Vec<_> = captures .iter() .filter(|c| c.mapped_to_core.is_none()) .collect(); Ok(Json(json!({ "total": captures.len(), "unknown_endpoints": unknown.len(), "captures": captures, }))) } /// List only unknown (unmapped) endpoints, deduplicated by method+path. pub async fn get_unknown_endpoints(State(state): State) -> BridgeResult> { let captures = load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?; let mut seen = std::collections::HashSet::new(); let unknown: Vec = captures .iter() .filter(|c| c.mapped_to_core.is_none()) .filter_map(|c| { let key = format!("{} {}", c.method, c.path); if seen.insert(key) { Some(json!({ "method": c.method, "path": c.path, "first_seen": c.timestamp, "capture_id": c.id, })) } else { None } }) .collect(); Ok(Json(json!({ "unknown_endpoint_count": unknown.len(), "endpoints": unknown, }))) } /// Wipe all capture files from disk. pub async fn delete_captures(State(state): State) -> BridgeResult> { let dir = std::path::Path::new(&state.config.captures_dir); let mut deleted = 0u64; if dir.exists() { for entry in std::fs::read_dir(dir).map_err(BridgeError::Io)? { let entry = entry.map_err(BridgeError::Io)?; let path = entry.path(); if path.extension().map(|e| e == "json").unwrap_or(false) { std::fs::remove_file(&path).map_err(BridgeError::Io)?; deleted += 1; } } } Ok(Json( json!({ "deleted": deleted, "message": "capture folder cleared" }), )) } /// Server-sent events stream: pushes each new capture to subscribed clients. pub async fn get_captures_stream( State(state): State, ) -> Sse>> { let rx = state.capture_tx.subscribe(); let stream = BroadcastStream::new(rx).filter_map(|result| { result.ok().and_then(|capture| { serde_json::to_string(&capture) .ok() .map(|json| Ok(Event::default().event("capture").data(json))) }) }); Sse::new(stream).keep_alive(KeepAlive::default()) } /// Endpoint status summary: known/mapped/unknown across all captures. pub async fn get_endpoint_status(State(state): State) -> BridgeResult> { use crate::mapper::map_to_core; let captures = load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?; let mut seen: std::collections::HashMap = std::collections::HashMap::new(); for c in &captures { let key = format!("{} {}", c.method, c.path); seen.entry(key).or_insert_with(|| { let status = if c.mapped_to_core.is_some() { "mapped" } else if map_to_core(&c.method, &c.path).is_some() { "known" } else { "unknown" }; json!({ "method": c.method, "path": c.path, "status": status, "mapped_to": c.mapped_to_core, "first_seen": c.timestamp, }) }); } let mut endpoints: Vec = seen.into_values().collect(); endpoints.sort_by(|a, b| { a["path"] .as_str() .unwrap_or("") .cmp(b["path"].as_str().unwrap_or("")) }); Ok(Json(json!({ "total_unique_endpoints": endpoints.len(), "endpoints": endpoints, }))) } /// Simple HTML admin dashboard. pub async fn get_admin_dashboard() -> impl IntoResponse { let html = include_str!("../admin.html"); Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/html; charset=utf-8") .body(axum::body::Body::from(html)) .unwrap() }