use axum::{ body::Body, extract::{State, Query}, http::{HeaderValue, StatusCode}, response::{IntoResponse, Response}, Router, }; use tower_http::cors::{Any, CorsLayer}; use crate::db; #[path = "validate.rs"] mod validate; use base64::{Engine as _, engine::general_purpose::STANDARD}; use image::GenericImageView; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::num::NonZeroUsize; use lru::LruCache; use tokio::sync::Semaphore; fn get_api_key() -> String { std::env::var("MOMENTRY_API_KEY") .expect("MOMENTRY_API_KEY environment variable must be set") } pub fn get_output_dir() -> std::path::PathBuf { std::env::var_os("MOMENTRY_OUTPUT_DIR") .map(std::path::PathBuf::from) .or_else(|| { std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.join("output"))) }) .unwrap_or_else(|| std::path::PathBuf::from("output")) } fn get_data_dir() -> std::path::PathBuf { std::env::var_os("MOMENTRY_DATA_DIR") .map(std::path::PathBuf::from) .or_else(|| { std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.join("data"))) }) .unwrap_or_else(|| std::path::PathBuf::from("data")) } fn get_admin_config_path() -> std::path::PathBuf { std::env::var_os("MOMENTRY_ADMIN_CONFIG") .map(std::path::PathBuf::from) .or_else(|| { std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.join("config/admin_config.json"))) }) .unwrap_or_else(|| std::path::PathBuf::from("config/admin_config.json")) } const CORE_API: &str = "http://127.0.0.1:3002"; const MARKBASE_API: &str = "http://127.0.0.1:11438"; const DIST_DIR: &str = "../dist"; static FACE_THUMB_CACHE: Mutex>> = Mutex::new(None); static PROFILE_PROXY_CACHE: Mutex>> = Mutex::new(None); static THUMB_SEM_MAP_PROXY: Mutex>>> = Mutex::new(None); fn acquire_thumb_semaphore(uuid: &str) -> Arc { let mut map = THUMB_SEM_MAP_PROXY.lock().unwrap(); let map = map.get_or_insert_with(HashMap::new); map.entry(uuid.to_string()) .or_insert_with(|| Arc::new(Semaphore::new(1))) .clone() } fn get_face_thumb_cache() -> std::sync::MutexGuard<'static, Option>> { let mut guard = FACE_THUMB_CACHE.lock().unwrap(); if guard.is_none() { *guard = Some(LruCache::new(NonZeroUsize::new(500).unwrap())); } guard } fn get_profile_proxy_cache() -> std::sync::MutexGuard<'static, Option>> { let mut guard = PROFILE_PROXY_CACHE.lock().unwrap(); if guard.is_none() { *guard = Some(LruCache::new(NonZeroUsize::new(300).unwrap())); } guard } #[derive(Clone)] pub struct ProxyState { pub client: reqwest::Client, pub serve_static: bool, } pub async fn start_proxy_server() { let serve_static = std::env::var("SERVE_STATIC").is_ok(); let state = ProxyState { client: reqwest::Client::builder() .connect_timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(600)) .pool_max_idle_per_host(10) .build() .unwrap(), serve_static, }; let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); let app = Router::new() .route("/health", axum::routing::get(health_handler)) .route("/api/v1/auth/login", axum::routing::post(login_handler)) .route("/api/v1/face-thumbnail", axum::routing::get(get_face_thumbnail_handler)) .route("/api/v1/media/frame", axum::routing::get(get_frame_handler)) .route("/api/v1/identity-matches", axum::routing::get(get_identity_matches_handler)) .route("/api/v1/trace-profile-file", axum::routing::get(get_trace_profile_file_handler)) .route("/api/v1/cluster-results", axum::routing::get(get_cluster_results_handler)) .route("/api/v1/processor-json", axum::routing::get(get_processor_json_handler)) .route("/api/v1/file/thumbnail", axum::routing::get(get_file_thumbnail_by_path_handler)) .route("/api/v1/file/:file_uuid/pose", axum::routing::get(get_pose_handler)) .route("/api/v1/file/:file_uuid/appearance", axum::routing::get(get_appearance_handler)) .route("/api/v1/admin/config", axum::routing::get(get_admin_config_handler)) .route("/api/v1/admin/config", axum::routing::post(save_admin_config_handler)) .route("/api/v1/system-stats", axum::routing::get(get_system_stats_handler)) .route("/api/v1/system-stats/validate", axum::routing::get(validate_system_stats_handler)) .fallback(fallback_handler) .layer(cors) .with_state(state); let listener = match tokio::net::TcpListener::bind("0.0.0.0:8888").await { Ok(l) => l, Err(e) => { eprintln!("[proxy] Failed to bind port 8888: {}. Proxy will not be available.", e); return; } }; eprintln!("[proxy] HTTP server listening on http://0.0.0.0:8888 (serve_static={})", serve_static); if let Err(e) = axum::serve(listener, app).await { eprintln!("[proxy] Server error: {}", e); } } async fn health_handler() -> &'static str { "ok" } #[derive(serde::Deserialize)] struct LoginRequest { username: String, password: String, } #[derive(serde::Serialize)] struct LoginResponse { success: bool, user: Option, token: Option, error: Option, } #[derive(serde::Serialize)] struct UserInfo { id: String, username: String, display_name: Option, role: String, } async fn login_handler(body: axum::body::Bytes) -> Response { let req: Result = serde_json::from_slice(&body); match req { Ok(login_req) => { match db::verify_user(&login_req.username, &login_req.password) { Ok(Some(user)) => { let token = uuid::Uuid::new_v4().to_string(); let response = LoginResponse { success: true, user: Some(UserInfo { id: user.id, username: user.username, display_name: user.display_name, role: user.role, }), token: Some(token), error: None, }; (StatusCode::OK, axum::Json(response)).into_response() } Ok(None) => { let response = LoginResponse { success: false, user: None, token: None, error: Some("Invalid username or password".to_string()), }; (StatusCode::UNAUTHORIZED, axum::Json(response)).into_response() } Err(e) => { let response = LoginResponse { success: false, user: None, token: None, error: Some(e), }; (StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response)).into_response() } } } Err(e) => { let response = LoginResponse { success: false, user: None, token: None, error: Some(format!("Invalid request: {}", e)), }; (StatusCode::BAD_REQUEST, axum::Json(response)).into_response() } } } async fn fallback_handler(State(state): State, req: axum::extract::Request) -> Response { let path = req.uri().path().to_string(); let method = req.method().clone(); // Handle identity profile GET locally if method == axum::http::Method::GET { if let Some(rest) = path.strip_prefix("/api/v1/identity/") { if rest.ends_with("/profile") { let uuid = rest.trim_end_matches("/profile").to_string(); return get_identity_profile_handler_inner(uuid).await; } } } // Handle identity profile-image POST: proxy to Core API + invalidate cache if method == axum::http::Method::POST { if let Some(rest) = path.strip_prefix("/api/v1/identity/") { if rest.ends_with("/profile-image") { let uuid = rest.trim_end_matches("/profile-image").to_string(); let response = proxy_to(state, req, CORE_API, true).await; // Invalidate profile cache on successful upload let status = response.status(); if status == StatusCode::OK || status == StatusCode::CREATED { let mut cache = get_profile_proxy_cache(); if let Some(c) = cache.as_mut() { c.pop(&uuid); } eprintln!("[proxy] PROFILE cache invalidated for {}", uuid); } return response; } } } // Handle search-history CRUD locally if path.starts_with("/api/v1/search-history") || path.starts_with("/api/v1/bookmarks") || path.starts_with("/api/v1/video-marks") || path.starts_with("/api/v1/video-mark/") { return handle_local_api(req).await; } if path.starts_with("/api/v2/") { return proxy_to(state, req, MARKBASE_API, false).await; } else if path.starts_with("/api/") { return proxy_to(state, req, CORE_API, true).await; } else if state.serve_static { return serve_spa(&path).await; } else { return (StatusCode::NOT_FOUND, "Not found").into_response(); } } async fn handle_local_api(req: axum::extract::Request) -> Response { let path = req.uri().path().to_string(); let query_str = req.uri().query().unwrap_or("").to_string(); let method = req.method().clone(); let body_bytes = axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await.unwrap_or_default(); if method == axum::http::Method::GET && path == "/api/v1/search-history" { return get_search_history_handler_inner(); } if method == axum::http::Method::POST && path == "/api/v1/search-history" { return save_search_history_handler_inner(body_bytes); } if method == axum::http::Method::PATCH { if let Some(rest) = path.strip_prefix("/api/v1/search-history/") { if let Some(id) = rest.strip_suffix("/rename") { return rename_search_history_handler_inner(id.to_string(), body_bytes); } if let Some(id) = rest.strip_suffix("/pin") { return pin_search_history_handler_inner(id.to_string(), body_bytes); } } } if method == axum::http::Method::DELETE { if let Some(id) = path.strip_prefix("/api/v1/search-history/") { return delete_search_history_handler_inner(id.to_string()); } if let Some(id_str) = path.strip_prefix("/api/v1/bookmarks/") { if let Ok(id) = id_str.parse::() { return delete_bookmark_handler_inner(id); } } } if method == axum::http::Method::GET && path == "/api/v1/bookmarks" { return get_bookmarks_handler_inner(); } if method == axum::http::Method::POST && path == "/api/v1/bookmarks" { return save_bookmark_handler_inner(body_bytes); } // Video marks (local SQLite) if method == axum::http::Method::GET && path == "/api/v1/video-marks" { return get_video_marks_handler_inner(&query_str); } if method == axum::http::Method::POST && path == "/api/v1/video-marks" { return save_video_mark_handler_inner(body_bytes); } if method == axum::http::Method::DELETE { if let Some(id_str) = path.strip_prefix("/api/v1/video-mark/") { if let Ok(id) = id_str.parse::() { return delete_video_mark_handler_inner(id); } } } (StatusCode::NOT_FOUND, "Not found").into_response() } async fn proxy_to(state: ProxyState, req: axum::extract::Request, base: &str, inject_api_key: bool) -> Response { let path = req.uri().path().to_string(); let method = req.method().clone(); let headers = req.headers().clone(); let start = std::time::Instant::now(); let query_str = req.uri().query().unwrap_or("").to_string(); let mut url = if inject_api_key { format!("{}{}?api_key={}", base, path, get_api_key()) } else { format!("{}{}", base, path) }; for pair in query_str.split('&') { if pair.is_empty() || (inject_api_key && pair.starts_with("api_key=")) { continue; } url.push_str(&format!("&{}", pair)); } let target = if base == CORE_API { "core api" } else { "markbase" }; let range_hdr = headers.get("range").and_then(|v| v.to_str().ok()).unwrap_or("-"); eprintln!("[proxy] --> {} {} ({}) range={}", method, path, target, range_hdr); let reqwest_method = match method.as_str() { "POST" => reqwest::Method::POST, "PUT" => reqwest::Method::PUT, "PATCH" => reqwest::Method::PATCH, "DELETE" => reqwest::Method::DELETE, "HEAD" => reqwest::Method::HEAD, "OPTIONS" => reqwest::Method::OPTIONS, _ => reqwest::Method::GET, }; let mut req_builder = state.client.request(reqwest_method, &url); for (name, value) in headers.iter() { let name_str = name.as_str(); if name_str == "host" || name_str == "origin" || name_str == "referer" { continue; } if let Ok(v) = String::from_utf8(value.as_bytes().to_vec()) { req_builder = req_builder.header(name_str, v); } } let body_bytes = axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await.unwrap_or_default(); if !body_bytes.is_empty() { if method == axum::http::Method::POST && path.contains("/unbind") { eprintln!("[proxy] UNBIND body: {}", String::from_utf8_lossy(&body_bytes)); } req_builder = req_builder.body(body_bytes.to_vec()); } // Check if this is a video request (stream instead of buffer) let is_video_request = path.contains("/video") || headers.contains_key("range"); match req_builder.send().await { Ok(resp) => { let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); let resp_headers = resp.headers().clone(); // Check if response is video content let content_type = resp_headers.get("content-type") .and_then(|v| v.to_str().ok()) .unwrap_or(""); let should_stream = is_video_request || content_type.starts_with("video/") || content_type == "application/octet-stream"; if should_stream { eprintln!("[proxy] <-- {} {} {} streaming {}ms cl={} ar={} cr={}", method, path, status.as_u16(), start.elapsed().as_millis(), resp_headers.get("content-length").and_then(|v| v.to_str().ok()).unwrap_or("-"), resp_headers.get("accept-ranges").and_then(|v| v.to_str().ok()).unwrap_or("-"), resp_headers.get("content-range").and_then(|v| v.to_str().ok()).unwrap_or("-")); let mut builder = axum::http::Response::builder().status(status); for (name, value) in resp_headers.iter() { let name_str = name.as_str(); if name_str == "transfer-encoding" || name_str == "content-encoding" { continue; } if let Ok(v) = HeaderValue::from_bytes(value.as_bytes()) { builder = builder.header(name_str, v); } } // Stream the body directly let stream_body = Body::from_stream(resp.bytes_stream()); return match builder.body(stream_body) { Ok(r) => r.into_response(), Err(e) => { eprintln!("[proxy] Error building streaming response: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, "Proxy error").into_response() } }; } // Non-video: buffer entire response let resp_bytes = match resp.bytes().await { Ok(b) => b.to_vec(), Err(e) => { eprintln!("[proxy] Error reading response body: {}", e); return (StatusCode::BAD_GATEWAY, "Error reading response").into_response(); } }; let size = resp_bytes.len(); if method == axum::http::Method::POST && path.contains("/unbind") { eprintln!("[proxy] UNBIND response: {} {}", status.as_u16(), String::from_utf8_lossy(&resp_bytes)); } let mut builder = axum::http::Response::builder().status(status); for (name, value) in resp_headers.iter() { let name_str = name.as_str(); if name_str == "transfer-encoding" || name_str == "content-encoding" { continue; } if let Ok(v) = HeaderValue::from_bytes(value.as_bytes()) { builder = builder.header(name_str, v); } } let accept_ranges = resp_headers.get("accept-ranges").and_then(|v| v.to_str().ok()).unwrap_or("-"); let content_range = resp_headers.get("content-range").and_then(|v| v.to_str().ok()).unwrap_or("-"); let content_length = resp_headers.get("content-length").and_then(|v| v.to_str().ok()).unwrap_or("-"); eprintln!("[proxy] <-- {} {} {} {} {}ms cl={} ar={} cr={}", method, path, status.as_u16(), size, start.elapsed().as_millis(), content_length, accept_ranges, content_range); match builder.body(Body::from(resp_bytes)) { Ok(r) => r.into_response(), Err(e) => { eprintln!("[proxy] Error building response: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, "Proxy error").into_response() } } } Err(e) => { eprintln!("[proxy] <-- {} {} ERR {}ms: {}", method, path, start.elapsed().as_millis(), e); (StatusCode::BAD_GATEWAY, format!("Proxy error: {}", e)).into_response() } } } async fn serve_spa(path: &str) -> Response { let file_path = path.trim_start_matches('/'); if file_path.is_empty() || file_path == "index.html" { return serve_file(DIST_DIR, "index.html").await; } let safe_path = file_path.replace("..", ""); if tokio::fs::try_exists(format!("{}/{}", DIST_DIR, safe_path)).await.unwrap_or(false) { return serve_file(DIST_DIR, &safe_path).await; } serve_file(DIST_DIR, "index.html").await } fn content_type_for(path: &str) -> &'static str { match path.rsplit('.').next() { Some("js") => "application/javascript; charset=utf-8", Some("css") => "text/css; charset=utf-8", Some("html") => "text/html; charset=utf-8", Some("json") => "application/json", Some("png") => "image/png", Some("jpg") | Some("jpeg") => "image/jpeg", Some("svg") => "image/svg+xml", Some("ico") => "image/x-icon", Some("woff") => "font/woff", Some("woff2") => "font/woff2", Some("map") => "application/json", _ => "application/octet-stream", } } async fn serve_file(dir: &str, name: &str) -> Response { match tokio::fs::read(format!("{}/{}", dir, name)).await { Ok(bytes) => { let ct = content_type_for(name); ( StatusCode::OK, [(axum::http::header::CONTENT_TYPE, HeaderValue::from_static(ct))], bytes, ) .into_response() } Err(_) => (StatusCode::NOT_FOUND, "Not found").into_response(), } } // ===== Local SQLite API handlers ===== fn get_search_history_handler_inner() -> Response { match db::get_search_history(Some(30)) { Ok(items) => axum::Json(items).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn save_search_history_handler_inner(body: axum::body::Bytes) -> Response { let req: serde_json::Value = match serde_json::from_slice(&body) { Ok(v) => v, Err(e) => return (StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)).into_response(), }; let id = req.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(); let query = req.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string(); let title = req.get("title").and_then(|v| v.as_str()).unwrap_or(&query).to_string(); let chat_state = req.get("chat_state").and_then(|v| v.as_str()).map(|s| s.to_string()); let mode = req.get("mode").and_then(|v| v.as_str()).map(|s| s.to_string()); match db::save_search_history(id, query, title, chat_state, mode) { Ok(item) => axum::Json(item).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn rename_search_history_handler_inner(id: String, body: axum::body::Bytes) -> Response { let req: serde_json::Value = match serde_json::from_slice(&body) { Ok(v) => v, Err(_) => return (StatusCode::BAD_REQUEST, "Invalid JSON").into_response(), }; let title = req.get("title").and_then(|v| v.as_str()).unwrap_or("").to_string(); match db::rename_search_history(id, title) { Ok(()) => axum::Json(serde_json::json!({"success": true})).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn pin_search_history_handler_inner(id: String, body: axum::body::Bytes) -> Response { let req: serde_json::Value = match serde_json::from_slice(&body) { Ok(v) => v, Err(_) => return (StatusCode::BAD_REQUEST, "Invalid JSON").into_response(), }; let pinned = req.get("pinned").and_then(|v| v.as_bool()).unwrap_or(false); match db::pin_search_history(id, pinned) { Ok(()) => axum::Json(serde_json::json!({"success": true})).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn delete_search_history_handler_inner(id: String) -> Response { match db::delete_search_history(id) { Ok(()) => axum::Json(serde_json::json!({"success": true})).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn get_bookmarks_handler_inner() -> Response { match db::get_bookmarks() { Ok(items) => axum::Json(items).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn save_bookmark_handler_inner(body: axum::body::Bytes) -> Response { let req: serde_json::Value = match serde_json::from_slice(&body) { Ok(v) => v, Err(e) => return (StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)).into_response(), }; let label = req.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string(); let history_id = req.get("history_id").and_then(|v| v.as_str()).map(|s| s.to_string()); match db::save_bookmark(label, history_id) { Ok(item) => axum::Json(item).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn delete_bookmark_handler_inner(id: i64) -> Response { match db::delete_bookmark(id) { Ok(()) => axum::Json(serde_json::json!({"success": true})).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn get_video_marks_handler_inner(query_str: &str) -> Response { let file_uuid = query_str .split('&') .find_map(|pair| { let mut it = pair.splitn(2, '='); let k = it.next().unwrap_or(""); let v = it.next().unwrap_or(""); if k == "file_uuid" { Some(v.to_string()) } else { None } }) .unwrap_or_default(); match db::get_video_marks(file_uuid) { Ok(items) => axum::Json(items).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn save_video_mark_handler_inner(body: axum::body::Bytes) -> Response { let req: serde_json::Value = match serde_json::from_slice(&body) { Ok(v) => v, Err(e) => return (StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)).into_response(), }; let file_uuid = req.get("file_uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(); let tag = req.get("tag").and_then(|v| v.as_str()).unwrap_or("default").to_string(); let start_frame = req.get("start_frame").and_then(|v| v.as_i64()).unwrap_or(0); let end_frame = req.get("end_frame").and_then(|v| v.as_i64()); let note = req.get("note").and_then(|v| v.as_str()).map(|s| s.to_string()); match db::save_video_mark(file_uuid, tag, start_frame, end_frame, note) { Ok(item) => axum::Json(item).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } fn delete_video_mark_handler_inner(id: i64) -> Response { match db::delete_video_mark(id) { Ok(()) => axum::Json(serde_json::json!({"success": true})).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(), } } async fn get_identity_profile_handler_inner(uuid: String) -> Response { let start = std::time::Instant::now(); let no_dash = uuid.replace('-', ""); eprintln!("[proxy] PROFILE handler called for {} (no_dash={})", uuid, no_dash); { let mut cache = get_profile_proxy_cache(); if let Some(c) = cache.as_mut() { if let Some(val) = c.get(&uuid) { eprintln!("[proxy] <-- PROFILE {} 200 (cached) {}ms", uuid, start.elapsed().as_millis()); return ([("content-type", "text/plain")], val.clone()).into_response(); } } } let result = (|| -> Option { let output_dir = get_output_dir(); let identities_dir = output_dir.join("identities"); for candidate in [&no_dash, &uuid] { let path = identities_dir.join(candidate).join("profile.jpg"); if path.exists() { eprintln!("[proxy] PROFILE FOUND: {:?}", path); let bytes = std::fs::read(&path).ok()?; return Some(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))); } } None })(); match result { Some(data) => { let mut cache = get_profile_proxy_cache(); if let Some(c) = cache.as_mut() { c.put(uuid.clone(), data.clone()); } eprintln!("[proxy] <-- PROFILE {} 200 {} {}ms", uuid, data.len(), start.elapsed().as_millis()); ([("content-type", "text/plain")], data).into_response() } None => { eprintln!("[proxy] <-- PROFILE {} 404 {}ms", uuid, start.elapsed().as_millis()); (StatusCode::NOT_FOUND, "Profile image not found").into_response() } } } async fn get_face_thumbnail_handler(State(state): State, axum::extract::Query(params): axum::extract::Query>) -> Response { let start = std::time::Instant::now(); let uuid = match params.get("uuid") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing uuid").into_response(), }; let frame: u32 = params.get("frame").and_then(|v| v.parse().ok()).unwrap_or(0); let bbox_x = params.get("bbox_x").and_then(|v| v.parse::().ok()); let bbox_y = params.get("bbox_y").and_then(|v| v.parse::().ok()); let bbox_w = params.get("bbox_w").and_then(|v| v.parse::().ok()); let bbox_h = params.get("bbox_h").and_then(|v| v.parse::().ok()); let cache_key = format!("{}:{}:{}:{}:{}:{}", uuid, frame, bbox_x.map_or(0.0, |v| v), bbox_y.map_or(0.0, |v| v), bbox_w.map_or(0.0, |v| v), bbox_h.map_or(0.0, |v| v)); { let mut cache = get_face_thumb_cache(); if let Some(c) = cache.as_mut() { if let Some(val) = c.get(&cache_key) { eprintln!("[proxy] <-- FACE-THUMB {} (cached) {}ms", uuid, start.elapsed().as_millis()); return ([("content-type", "text/plain")], val.clone()).into_response(); } } } let _permit = acquire_thumb_semaphore(&uuid).acquire_owned().await.unwrap(); // Double-check cache after acquiring semaphore { let mut cache = get_face_thumb_cache(); if let Some(c) = cache.as_mut() { if let Some(val) = c.get(&cache_key) { eprintln!("[proxy] <-- FACE-THUMB {} (cached after wait) {}ms", uuid, start.elapsed().as_millis()); return ([("content-type", "text/plain")], val.clone()).into_response(); } } } let url = format!("{}/api/v1/file/{}/thumbnail?api_key={}&frame={}", CORE_API, uuid, get_api_key(), frame); let bytes = match state.client.get(&url).send().await { Ok(resp) => match resp.bytes().await { Ok(b) => b.to_vec(), Err(e) => return (StatusCode::BAD_GATEWAY, format!("Read failed: {}", e)).into_response(), }, Err(e) => return (StatusCode::BAD_GATEWAY, format!("Request failed: {}", e)).into_response(), }; let result = if let (Some(bx), Some(by), Some(bw), Some(bh)) = (bbox_x, bbox_y, bbox_w, bbox_h) { let img = match image::load_from_memory(&bytes) { Ok(i) => i, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Image decode failed: {}", e)).into_response(), }; let (w, h) = img.dimensions(); let (px, py, pw, ph) = if bx <= 1.0 && by <= 1.0 && bw <= 1.0 && bh <= 1.0 { ((bx * w as f64) as u32, (by * h as f64) as u32, (bw * w as f64) as u32, (bh * h as f64) as u32) } else { (bx as u32, by as u32, bw as u32, bh as u32) }; let cx = px.min(w.saturating_sub(1)); let cy = py.min(h.saturating_sub(1)); let cw = pw.min(w.saturating_sub(cx)).max(1); let ch = ph.min(h.saturating_sub(cy)).max(1); let cropped = img.crop_imm(cx, cy, cw, ch); let mut buf = std::io::Cursor::new(Vec::new()); if let Err(e) = cropped.write_to(&mut buf, image::ImageFormat::Jpeg) { return (StatusCode::INTERNAL_SERVER_ERROR, format!("Encode failed: {}", e)).into_response(); } format!("data:image/jpeg;base64,{}", STANDARD.encode(buf.into_inner())) } else { format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes)) }; { let mut cache = get_face_thumb_cache(); if let Some(c) = cache.as_mut() { c.put(cache_key, result.clone()); } } eprintln!("[proxy] <-- FACE-THUMB {} {} {}ms", uuid, result.len(), start.elapsed().as_millis()); ([("content-type", "text/plain")], result).into_response() } async fn get_identity_matches_handler(axum::extract::Query(params): axum::extract::Query>) -> Response { let file_hash = match params.get("file_hash") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(), }; let output_base = get_output_dir(); let match_path = format!("{}/{}.identity_match_round1.json", output_base.display(), file_hash); let match_path = if std::path::Path::new(&match_path).exists() { Some(match_path) } else { None }; match match_path { Some(path) => match std::fs::read_to_string(&path) { Ok(content) => { let parsed: serde_json::Value = match serde_json::from_str(&content) { Ok(v) => v, Err(e) => { return (StatusCode::INTERNAL_SERVER_ERROR, format!("Parse failed: {}", e)).into_response(); } }; axum::Json(parsed).into_response() } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(), }, None => (StatusCode::NOT_FOUND, "Identity match file not found").into_response(), } } async fn get_trace_profile_file_handler(axum::extract::Query(params): axum::extract::Query>) -> Response { let file_uuid = match params.get("file_uuid") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing file_uuid").into_response(), }; let trace_id = match params.get("trace_id") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing trace_id").into_response(), }; let output_base = get_output_dir(); let profile_path = format!("{}/trace_{}/trace_profile.json", output_base.display(), trace_id); let profile_path = if std::path::Path::new(&profile_path).exists() { Some(profile_path) } else { None }; match profile_path { Some(path) => match std::fs::read_to_string(&path) { Ok(content) => { let parsed: serde_json::Value = match serde_json::from_str(&content) { Ok(v) => v, Err(e) => { return (StatusCode::INTERNAL_SERVER_ERROR, format!("Parse failed: {}", e)).into_response(); } }; axum::Json(parsed).into_response() } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(), }, None => (StatusCode::NOT_FOUND, "Trace profile file not found").into_response(), } } async fn get_cluster_results_handler(axum::extract::Query(params): axum::extract::Query>) -> Response { let file_hash = match params.get("file_hash") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(), }; let client = reqwest::Client::new(); // Try new cluster-results endpoint first, fallback to face-groups let url = format!("{}/api/v1/cluster-results?file_hash={}", CORE_API, file_hash); let resp = client.get(&url).header("X-API-Key", get_api_key()).send().await; match resp { Ok(r) => { let status = r.status(); let b = r.text().await.unwrap_or_default(); // Check if response has data if status.is_success() { if let Ok(v) = serde_json::from_str::(&b) { let fg = v.get("face_groups").and_then(|x| x.as_array()).map(|x| x.len()).unwrap_or(0); if fg > 0 { return ([(axum::http::header::CONTENT_TYPE, "application/json")], b).into_response(); } } } } Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Request failed: {}", e)).into_response(), }; // Fallback to face-groups let fg_url = format!("{}/api/v1/file/{}/face-groups", CORE_API, file_hash); let fg_resp = match client.get(&fg_url).header("X-API-Key", get_api_key()).send().await { Ok(r) => r, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Face groups request failed: {}", e)).into_response(), }; let fg_status = fg_resp.status(); let fg_body = match fg_resp.text().await { Ok(b) => b, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(), }; let axum_status = axum::http::StatusCode::from_u16(fg_status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); if !axum_status.is_success() { return (axum_status, fg_body).into_response(); } let result: serde_json::Value = match serde_json::from_str(&fg_body) { Ok(v) => v, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON parse failed: {}", e)).into_response(), }; let clusters = result.get("face_groups").cloned().unwrap_or(serde_json::json!([])); axum::Json(serde_json::json!({ "success": true, "clusters": clusters })).into_response() } async fn get_processor_json_handler( axum::extract::Query(params): axum::extract::Query>, ) -> Response { let file_hash = match params.get("file_hash") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(), }; let processor = match params.get("processor") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing processor").into_response(), }; let client = reqwest::Client::new(); let json_url = format!("{}/api/v1/file/{}/json/{}", CORE_API, file_hash, processor); let json_resp = match client .post(&json_url) .header("X-API-Key", get_api_key()) .send() .await { Ok(r) => r, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON request failed: {}", e)).into_response(), }; let status = json_resp.status(); let body = match json_resp.text().await { Ok(b) => b, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(), }; let axum_status = axum::http::StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); if axum_status.is_success() { match serde_json::from_str::(&body) { Ok(v) => axum::Json(v).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON parse failed: {}", e)).into_response(), } } else { (axum_status, body).into_response() } } async fn get_file_thumbnail_by_path_handler(axum::extract::Query(params): axum::extract::Query>) -> Response { let path = match params.get("path") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing path").into_response(), }; let frame: u32 = params.get("frame").and_then(|v| v.parse().ok()).unwrap_or(30); if !std::path::Path::new(&path).exists() { return (StatusCode::NOT_FOUND, "File not found").into_response(); } let ext = path.rsplit('.').next().unwrap_or("").to_lowercase(); let is_image = matches!(ext.as_str(), "jpg" | "jpeg" | "png" | "gif" | "webp"); if is_image { // For images, read directly match std::fs::read(&path) { Ok(bytes) => { let mime = match ext.as_str() { "png" => "image/png", "gif" => "image/gif", "webp" => "image/webp", _ => "image/jpeg", }; ([( "content-type", mime )], axum::body::Body::from(bytes)).into_response() } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(), } } else { // For videos, extract frame using ffmpeg let time_sec = frame as f64 / 24.0; let output = tokio::process::Command::new("ffmpeg") .args([ "-ss", &time_sec.to_string(), "-i", &path, "-vframes", "1", "-q:v", "2", "-f", "mjpeg", "-" ]) .output() .await; match output { Ok(out) if out.status.success() => { let jpeg = out.stdout; ([("content-type", "image/jpeg")], axum::body::Body::from(jpeg)).into_response() } _ => (StatusCode::INTERNAL_SERVER_ERROR, "Thumbnail extraction failed").into_response(), } } } async fn get_frame_handler(State(state): State, axum::extract::Query(params): axum::extract::Query>) -> Response { let file_uuid = match params.get("file_uuid") { Some(v) => v.clone(), None => return (StatusCode::BAD_REQUEST, "Missing file_uuid").into_response(), }; let frame: u32 = params.get("frame").and_then(|v| v.parse().ok()).unwrap_or(0); // Get file path from Core API let url = format!("{}/api/v1/file/{}?api_key={}", CORE_API, file_uuid, get_api_key()); let file_info = match state.client.get(&url).send().await { Ok(resp) => match resp.json::().await { Ok(json) => json, Err(e) => return (StatusCode::BAD_GATEWAY, format!("Parse failed: {}", e)).into_response(), }, Err(e) => return (StatusCode::BAD_GATEWAY, format!("Request failed: {}", e)).into_response(), }; let file_path = match file_info["file_path"].as_str() { Some(p) => p.to_string(), None => return (StatusCode::NOT_FOUND, "File path not found").into_response(), }; if !std::path::Path::new(&file_path).exists() { return (StatusCode::NOT_FOUND, "File not found").into_response(); } let ext = file_path.rsplit('.').next().unwrap_or("").to_lowercase(); let is_image = matches!(ext.as_str(), "jpg" | "jpeg" | "png" | "gif" | "webp"); if is_image { match std::fs::read(&file_path) { Ok(bytes) => { let mime = match ext.as_str() { "png" => "image/png", "gif" => "image/gif", "webp" => "image/webp", _ => "image/jpeg", }; ([("content-type", mime)], axum::body::Body::from(bytes)).into_response() } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(), } } else { // Use select filter for exact frame extraction instead of -ss seeking let output = tokio::process::Command::new("ffmpeg") .args([ "-i", &file_path, "-vf", &format!("select=eq(n\\,{frame})"), "-vframes", "1", "-q:v", "2", "-f", "mjpeg", "-" ]) .output() .await; match output { Ok(out) if out.status.success() => { let jpeg = out.stdout; ([("content-type", "image/jpeg")], axum::body::Body::from(jpeg)).into_response() } _ => (StatusCode::INTERNAL_SERVER_ERROR, "Frame extraction failed").into_response(), } } } async fn get_pose_handler( axum::extract::Path(file_uuid): axum::extract::Path, axum::extract::Query(params): axum::extract::Query>, ) -> Response { let frame = match params.get("frame").and_then(|v| v.parse::().ok()) { Some(f) => f, None => return (StatusCode::BAD_REQUEST, "Missing frame").into_response(), }; let mut url = format!("{}/api/v1/file/{}/pose?frame={}", CORE_API, file_uuid, frame); if let Some(bbox_x) = params.get("bbox_x") { url.push_str(&format!("&bbox_x={}", bbox_x)); } if let Some(bbox_y) = params.get("bbox_y") { url.push_str(&format!("&bbox_y={}", bbox_y)); } if let Some(bbox_w) = params.get("bbox_w") { url.push_str(&format!("&bbox_w={}", bbox_w)); } if let Some(bbox_h) = params.get("bbox_h") { url.push_str(&format!("&bbox_h={}", bbox_h)); } let client = reqwest::Client::new(); let resp = match client .get(&url) .header("X-API-Key", get_api_key()) .send() .await { Ok(r) => r, Err(e) => return (StatusCode::BAD_GATEWAY, format!("Request failed: {}", e)).into_response(), }; let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); let body = match resp.text().await { Ok(b) => b, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(), }; (status, body).into_response() } async fn get_appearance_handler( axum::extract::Path(file_uuid): axum::extract::Path, axum::extract::Query(params): axum::extract::Query>, ) -> Response { let frame = match params.get("frame").and_then(|v| v.parse::().ok()) { Some(f) => f, None => return (StatusCode::BAD_REQUEST, "Missing frame").into_response(), }; let mut url = format!("{}/api/v1/file/{}/appearance?frame={}", CORE_API, file_uuid, frame); if let Some(bbox_x) = params.get("bbox_x") { url.push_str(&format!("&bbox_x={}", bbox_x)); } if let Some(bbox_y) = params.get("bbox_y") { url.push_str(&format!("&bbox_y={}", bbox_y)); } if let Some(bbox_w) = params.get("bbox_w") { url.push_str(&format!("&bbox_w={}", bbox_w)); } if let Some(bbox_h) = params.get("bbox_h") { url.push_str(&format!("&bbox_h={}", bbox_h)); } let client = reqwest::Client::new(); let resp = match client .get(&url) .header("X-API-Key", get_api_key()) .send() .await { Ok(r) => r, Err(e) => return (StatusCode::BAD_GATEWAY, format!("Request failed: {}", e)).into_response(), }; let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); let body = match resp.text().await { Ok(b) => b, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(), }; (status, body).into_response() } async fn get_admin_config_handler() -> Response { let config_path = get_admin_config_path(); match std::fs::read_to_string(&config_path) { Ok(content) => { match serde_json::from_str::(&content) { Ok(v) => axum::Json(v).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Parse failed: {}", e)).into_response(), } } Err(e) => (StatusCode::NOT_FOUND, format!("Config file not found: {}", e)).into_response(), } } async fn save_admin_config_handler(axum::extract::Json(payload): axum::extract::Json) -> Response { let config_path = get_admin_config_path(); match serde_json::to_string_pretty(&payload) { Ok(formatted) => { match std::fs::write(&config_path, formatted) { Ok(()) => axum::Json(serde_json::json!({"success": true, "message": "Config saved"})).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Write failed: {}", e)).into_response(), } } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Format failed: {}", e)).into_response(), } } #[cfg(target_arch = "aarch64")] mod macmon_sampler { use macmon::Sampler; pub struct MacmonData { pub cpu_power: f32, pub gpu_power: f32, pub ane_power: f32, pub sys_power: f32, pub ram_power: f32, pub cpu_temp: f32, pub gpu_temp: f32, pub ecpu_freq_mhz: u32, pub pcpu_freq_mhz: u32, pub gpu_freq_mhz: u32, pub cpu_scaled_ratio: f32, pub gpu_scaled_ratio: f32, pub chip_name: String, pub ecpu_cores: u8, pub pcpu_cores: u8, pub gpu_cores: u8, pub ram_total: u64, pub ram_usage: u64, pub swap_total: u64, pub swap_used: u64, } fn get_swap_info() -> (u64, u64) { use std::process::Command; let output = match Command::new("sysctl") .args(["-n", "vm.swapusage"]) .output() { Ok(o) => o, Err(_) => return (0, 0), }; let s = String::from_utf8_lossy(&output.stdout); let parts: Vec<&str> = s.split_whitespace().collect(); let total_mb = parts.get(2) .and_then(|v| v.trim_end_matches('M').parse::().ok()) .unwrap_or(0.0) as u64; let used_mb = parts.get(5) .and_then(|v| v.trim_end_matches('M').parse::().ok()) .unwrap_or(0.0) as u64; (total_mb, used_mb) } pub fn get_metrics() -> Option { let mut sampler = Sampler::new().ok()?; let metrics = sampler.get_metrics(100).ok()?; let soc = sampler.get_soc_info(); let (swap_total, swap_used) = get_swap_info(); Some(MacmonData { cpu_power: metrics.cpu_power, gpu_power: metrics.gpu_power, ane_power: metrics.ane_power, sys_power: metrics.sys_power, ram_power: metrics.ram_power, cpu_temp: metrics.temp.cpu_temp_avg, gpu_temp: metrics.temp.gpu_temp_avg, ecpu_freq_mhz: metrics.ecpu_freq_mhz, pcpu_freq_mhz: metrics.pcpu_freq_mhz, gpu_freq_mhz: metrics.gpu_freq_mhz, cpu_scaled_ratio: metrics.cpu_scaled_ratio, gpu_scaled_ratio: metrics.gpu_scaled_ratio, chip_name: soc.chip_name.clone(), ecpu_cores: soc.ecpu_cores, pcpu_cores: soc.pcpu_cores, gpu_cores: soc.gpu_cores, ram_total: metrics.memory.ram_total, ram_usage: metrics.memory.ram_usage, swap_total, swap_used, }) } } #[cfg(not(target_arch = "aarch64"))] mod macmon_sampler { pub struct MacmonData; pub fn get_metrics() -> Option { None } } #[derive(serde::Serialize, serde::Deserialize)] pub struct SystemStatsResponse { pub cpu_used_percent: f64, pub memory_used_percent: f64, pub memory_available_mb: u64, pub memory_total_mb: u64, pub swap_used_percent: f64, pub swap_available_mb: u64, pub swap_total_mb: u64, pub gpu_available: bool, pub gpu_utilization: Option, pub gpu_memory_used_pct: Option, pub ane_available: bool, pub ane_utilization: Option, pub source: String, pub chip_name: Option, pub cpu_power: Option, pub gpu_power: Option, pub ane_power: Option, pub sys_power: Option, pub ram_power: Option, pub cpu_temp: Option, pub gpu_temp: Option, pub ecpu_freq_mhz: Option, pub pcpu_freq_mhz: Option, pub gpu_freq_mhz: Option, pub ecpu_cores: Option, pub pcpu_cores: Option, pub gpu_cores: Option, } async fn get_system_stats_handler() -> Response { // 1. Try macmon first (Apple Silicon only) if let Some(m) = macmon_sampler::get_metrics() { let ram_total_mb = m.ram_total / 1024 / 1024; let ram_usage_mb = m.ram_usage / 1024 / 1024; let ram_available_mb = ram_total_mb.saturating_sub(ram_usage_mb); let memory_used_percent = if ram_total_mb > 0 { (ram_usage_mb as f64 / ram_total_mb as f64) * 100.0 } else { 0.0 }; let swap_total_mb = m.swap_total; let swap_used_mb = m.swap_used; let swap_available_mb = swap_total_mb.saturating_sub(swap_used_mb); let swap_used_percent = if swap_total_mb > 0 { (swap_used_mb as f64 / swap_total_mb as f64) * 100.0 } else { 0.0 }; let stats = SystemStatsResponse { cpu_used_percent: m.cpu_scaled_ratio as f64 * 100.0, memory_used_percent, memory_available_mb: ram_available_mb, memory_total_mb: ram_total_mb, swap_used_percent, swap_available_mb, swap_total_mb, gpu_available: true, gpu_utilization: Some(m.gpu_scaled_ratio as f64 * 100.0), gpu_memory_used_pct: None, ane_available: m.ane_power > 0.0, ane_utilization: None, source: "macmon".to_string(), chip_name: Some(m.chip_name), cpu_power: Some(m.cpu_power as f64), gpu_power: Some(m.gpu_power as f64), ane_power: Some(m.ane_power as f64), sys_power: Some(m.sys_power as f64), ram_power: Some(m.ram_power as f64), cpu_temp: Some(m.cpu_temp as f64), gpu_temp: Some(m.gpu_temp as f64), ecpu_freq_mhz: Some(m.ecpu_freq_mhz), pcpu_freq_mhz: Some(m.pcpu_freq_mhz), gpu_freq_mhz: Some(m.gpu_freq_mhz), ecpu_cores: Some(m.ecpu_cores), pcpu_cores: Some(m.pcpu_cores), gpu_cores: Some(m.gpu_cores), }; return axum::Json(stats).into_response(); } // 2. Fallback: Use Core API let client = reqwest::Client::new(); let url = format!("{}/health/detailed", CORE_API); match client.get(&url).header("api_key", get_api_key()).send().await { Ok(resp) => { match resp.json::().await { Ok(data) => { if let Some(resources) = data.get("resources") { let stats = SystemStatsResponse { cpu_used_percent: resources.get("cpu_used_percent").and_then(|v| v.as_f64()).unwrap_or(0.0), memory_used_percent: resources.get("memory_used_percent").and_then(|v| v.as_f64()).unwrap_or(0.0), memory_available_mb: resources.get("memory_available_mb").and_then(|v| v.as_u64()).unwrap_or(0), memory_total_mb: resources.get("memory_total_mb").and_then(|v| v.as_u64()).unwrap_or(0), swap_used_percent: 0.0, swap_available_mb: 0, swap_total_mb: 0, gpu_available: resources.get("gpu_available").and_then(|v| v.as_bool()).unwrap_or(false), gpu_utilization: resources.get("gpu_utilization").and_then(|v| v.as_f64()), gpu_memory_used_pct: resources.get("gpu_memory_used_pct").and_then(|v| v.as_f64()), ane_available: resources.get("ane_available").and_then(|v| v.as_bool()).unwrap_or(false), ane_utilization: resources.get("ane_utilization").and_then(|v| v.as_f64()), source: "fallback".to_string(), chip_name: None, cpu_power: None, gpu_power: None, ane_power: None, sys_power: None, ram_power: None, cpu_temp: None, gpu_temp: None, ecpu_freq_mhz: None, pcpu_freq_mhz: None, gpu_freq_mhz: None, ecpu_cores: None, pcpu_cores: None, gpu_cores: None, }; axum::Json(stats).into_response() } else { (StatusCode::INTERNAL_SERVER_ERROR, "No resources in response").into_response() } } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Parse failed: {}", e)).into_response(), } } Err(e) => (StatusCode::BAD_GATEWAY, format!("Core API error: {}", e)).into_response(), } } async fn validate_system_stats_handler( Query(params): Query>, ) -> Response { // Parse layer parameter let layers: Vec = params .get("layers") .map(|s| s.split(',').filter_map(|n| n.trim().parse().ok()).collect()) .unwrap_or_else(|| vec![1, 2, 4]); // Get current macmon stats let stats = if let Some(m) = macmon_sampler::get_metrics() { let ram_total_mb = m.ram_total / 1024 / 1024; let ram_usage_mb = m.ram_usage / 1024 / 1024; let ram_available_mb = ram_total_mb.saturating_sub(ram_usage_mb); let memory_used_percent = if ram_total_mb > 0 { (ram_usage_mb as f64 / ram_total_mb as f64) * 100.0 } else { 0.0 }; let swap_total_mb = m.swap_total; let swap_used_mb = m.swap_used; let swap_available_mb = swap_total_mb.saturating_sub(swap_used_mb); let swap_used_percent = if swap_total_mb > 0 { (swap_used_mb as f64 / swap_total_mb as f64) * 100.0 } else { 0.0 }; SystemStatsResponse { cpu_used_percent: m.cpu_scaled_ratio as f64 * 100.0, memory_used_percent, memory_available_mb: ram_available_mb, memory_total_mb: ram_total_mb, swap_used_percent, swap_available_mb, swap_total_mb, gpu_available: true, gpu_utilization: Some(m.gpu_scaled_ratio as f64 * 100.0), gpu_memory_used_pct: None, ane_available: m.ane_power > 0.0, ane_utilization: None, source: "macmon".to_string(), chip_name: Some(m.chip_name), cpu_power: Some(m.cpu_power as f64), gpu_power: Some(m.gpu_power as f64), ane_power: Some(m.ane_power as f64), sys_power: Some(m.sys_power as f64), ram_power: Some(m.ram_power as f64), cpu_temp: Some(m.cpu_temp as f64), gpu_temp: Some(m.gpu_temp as f64), ecpu_freq_mhz: Some(m.ecpu_freq_mhz), pcpu_freq_mhz: Some(m.pcpu_freq_mhz), gpu_freq_mhz: Some(m.gpu_freq_mhz), ecpu_cores: Some(m.ecpu_cores), pcpu_cores: Some(m.pcpu_cores), gpu_cores: Some(m.gpu_cores), } } else { return (StatusCode::INTERNAL_SERVER_ERROR, "macmon not available").into_response(); }; // Run validation let result = super::validate::run_validation(&layers, &stats); axum::Json(result).into_response() }