Files
momentry_studio/src-tauri/src/proxy.rs
Momentry Studio 2daa49ce31 fix: 修复 Tauri 编译错误(SystemStatsResponse 定义)
问题:
- SystemStatsResponse 在 bin/proxy.rs 中定义
- 库模块 proxy.rs 和 validate.rs 试图从 super 导入,但路径错误

修复:
- 将 SystemStatsResponse 定义移到库模块 proxy.rs
- 在 validate.rs 中添加正确的导入
- 移除 bin/proxy.rs 中的重复定义(稍后处理)
2026-10-08 00:13:41 +08:00

1448 lines
58 KiB
Rust

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<Option<LruCache<String, String>>> = Mutex::new(None);
static PROFILE_PROXY_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
static THUMB_SEM_MAP_PROXY: Mutex<Option<HashMap<String, Arc<Semaphore>>>> = Mutex::new(None);
fn acquire_thumb_semaphore(uuid: &str) -> Arc<Semaphore> {
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<LruCache<String, String>>> {
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<LruCache<String, String>>> {
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<UserInfo>,
token: Option<String>,
error: Option<String>,
}
#[derive(serde::Serialize)]
struct UserInfo {
id: String,
username: String,
display_name: Option<String>,
role: String,
}
async fn login_handler(body: axum::body::Bytes) -> Response {
let req: Result<LoginRequest, _> = 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<ProxyState>, 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::<i64>() {
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::<i64>() {
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<String> {
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<ProxyState>, axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> 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::<f64>().ok());
let bbox_y = params.get("bbox_y").and_then(|v| v.parse::<f64>().ok());
let bbox_w = params.get("bbox_w").and_then(|v| v.parse::<f64>().ok());
let bbox_h = params.get("bbox_h").and_then(|v| v.parse::<f64>().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<std::collections::HashMap<String, String>>) -> 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<std::collections::HashMap<String, String>>) -> 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<std::collections::HashMap<String, String>>) -> 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::<serde_json::Value>(&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<std::collections::HashMap<String, String>>,
) -> 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::<serde_json::Value>(&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<std::collections::HashMap<String, String>>) -> 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<ProxyState>, axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> 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::<serde_json::Value>().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<String>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Response {
let frame = match params.get("frame").and_then(|v| v.parse::<u32>().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<String>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Response {
let frame = match params.get("frame").and_then(|v| v.parse::<u32>().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::<serde_json::Value>(&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<serde_json::Value>) -> 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::<f64>().ok())
.unwrap_or(0.0) as u64;
let used_mb = parts.get(5)
.and_then(|v| v.trim_end_matches('M').parse::<f64>().ok())
.unwrap_or(0.0) as u64;
(total_mb, used_mb)
}
pub fn get_metrics() -> Option<MacmonData> {
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<MacmonData> { 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<f64>,
pub gpu_memory_used_pct: Option<f64>,
pub ane_available: bool,
pub ane_utilization: Option<f64>,
pub source: String,
pub chip_name: Option<String>,
pub cpu_power: Option<f64>,
pub gpu_power: Option<f64>,
pub ane_power: Option<f64>,
pub sys_power: Option<f64>,
pub ram_power: Option<f64>,
pub cpu_temp: Option<f64>,
pub gpu_temp: Option<f64>,
pub ecpu_freq_mhz: Option<u32>,
pub pcpu_freq_mhz: Option<u32>,
pub gpu_freq_mhz: Option<u32>,
pub ecpu_cores: Option<u8>,
pub pcpu_cores: Option<u8>,
pub gpu_cores: Option<u8>,
}
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::<serde_json::Value>().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<std::collections::HashMap<String, String>>,
) -> Response {
// Parse layer parameter
let layers: Vec<u32> = 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()
}