feat: trace-level matching, health watcher/worker status, timezone config

This commit is contained in:
Accusys
2026-05-21 01:08:30 +08:00
parent 8ede4be159
commit bebaa743ed
60 changed files with 6110 additions and 1586 deletions

View File

@@ -1,5 +1,5 @@
use axum::{
extract::{Path, Query},
extract::{Path, Query, State},
http::StatusCode,
response::Json,
routing::{get, post},
@@ -77,7 +77,7 @@ pub async fn bind_identity(
// Get identity_id from identity_uuid
let identity_row: Option<(i64, String)> = sqlx::query_as(&format!(
"SELECT id, COALESCE(real_name, actor_name) AS name FROM {} WHERE uuid = $1::uuid",
"SELECT id, name FROM {} WHERE uuid = $1::uuid",
id_table
))
.bind(&identity_uuid)
@@ -116,8 +116,14 @@ pub async fn bind_identity(
let uuid_clean = identity_uuid.replace('-', "");
// Sync identity JSON file
if let Err(e) = crate::core::identity::storage::save_identity_file_by_pool(&db, &uuid_clean).await {
tracing::warn!("[bind] Failed to sync identity file for {}: {}", uuid_clean, e);
if let Err(e) =
crate::core::identity::storage::save_identity_file_by_pool(&db, &uuid_clean).await
{
tracing::warn!(
"[bind] Failed to sync identity file for {}: {}",
uuid_clean,
e
);
}
Ok(Json(ApiResponse {
@@ -189,8 +195,15 @@ pub async fn unbind_identity(
.ok()
.flatten();
if let Some(identity_uuid) = uuid {
if let Err(e) = crate::core::identity::storage::save_identity_file_by_pool(&db, &identity_uuid).await {
tracing::warn!("[unbind] Failed to sync identity file for {}: {}", identity_uuid, e);
if let Err(e) =
crate::core::identity::storage::save_identity_file_by_pool(&db, &identity_uuid)
.await
{
tracing::warn!(
"[unbind] Failed to sync identity file for {}: {}",
identity_uuid,
e
);
}
}
}
@@ -221,7 +234,7 @@ pub async fn merge_identities(
// Get IDs for both identities
let from_row: Option<(i64, String)> = sqlx::query_as(&format!(
"SELECT id, COALESCE(real_name, actor_name) AS name FROM {} WHERE uuid = $1::uuid",
"SELECT id, name FROM {} WHERE uuid = $1::uuid",
id_table
))
.bind(&identity_uuid)
@@ -239,7 +252,7 @@ pub async fn merge_identities(
))?;
let into_row: Option<(i64, String)> = sqlx::query_as(&format!(
"SELECT id, COALESCE(real_name, actor_name) AS name FROM {} WHERE uuid = $1::uuid",
"SELECT id, name FROM {} WHERE uuid = $1::uuid",
id_table
))
.bind(&req.into_uuid)
@@ -299,8 +312,14 @@ pub async fn merge_identities(
// Sync target identity JSON
let into_uuid_clean = req.into_uuid.replace('-', "");
if let Err(e) = crate::core::identity::storage::save_identity_file_by_pool(&db, &into_uuid_clean).await {
tracing::warn!("[merge] Failed to sync target identity file for {}: {}", into_uuid_clean, e);
if let Err(e) =
crate::core::identity::storage::save_identity_file_by_pool(&db, &into_uuid_clean).await
{
tracing::warn!(
"[merge] Failed to sync target identity file for {}: {}",
into_uuid_clean,
e
);
}
// Delete source identity JSON if not keeping history
@@ -339,6 +358,106 @@ pub struct ListIdentitiesParams {
pub offset: Option<i32>,
}
#[derive(Debug, Serialize)]
pub struct IdentityTraceInfo {
pub file_uuid: String,
pub trace_id: i32,
pub frame_count: i64,
pub first_frame: i32,
pub last_frame: i32,
pub first_sec: f64,
pub last_sec: f64,
pub avg_confidence: f64,
}
#[derive(Debug, Serialize)]
pub struct IdentityTracesResponse {
pub success: bool,
pub identity_uuid: String,
pub name: String,
pub total_traces: usize,
pub total_faces: i64,
pub traces: Vec<IdentityTraceInfo>,
}
pub async fn get_identity_traces(
State(state): State<crate::api::server::AppState>,
Path(identity_uuid): Path<String>,
) -> Result<Json<IdentityTracesResponse>, (StatusCode, String)> {
let id_table = crate::core::db::schema::table_name("identities");
let fd_table = crate::core::db::schema::table_name("face_detections");
// Get identity name
let identity: Option<(i32, String)> = sqlx::query_as(&format!(
"SELECT id, name FROM {} WHERE uuid = $1::uuid",
id_table
))
.bind(&identity_uuid)
.fetch_optional(state.db.pool())
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let (identity_id, name) =
identity.ok_or((StatusCode::NOT_FOUND, "Identity not found".to_string()))?;
// Get all traces for this identity across all files
let rows: Vec<(String, i32, i64, i32, i32, f64, f64, f64)> = sqlx::query_as(&format!(
r#"SELECT fd.file_uuid::text, fd.trace_id,
COUNT(*)::bigint AS frame_count,
MIN(fd.frame_number)::int AS first_frame,
MAX(fd.frame_number)::int AS last_frame,
ROUND(MIN(fd.frame_number)::numeric / 25.0, 1)::float8 AS first_sec,
ROUND(MAX(fd.frame_number)::numeric / 25.0, 1)::float8 AS last_sec,
ROUND(AVG(fd.confidence)::numeric, 4)::float8 AS avg_confidence
FROM {} fd
WHERE fd.identity_id = $1
GROUP BY fd.file_uuid, fd.trace_id
ORDER BY fd.file_uuid, fd.trace_id"#,
fd_table
))
.bind(identity_id)
.fetch_all(state.db.pool())
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let total_traces = rows.len();
let total_faces: i64 = rows.iter().map(|r| r.2).sum();
let traces: Vec<IdentityTraceInfo> = rows
.into_iter()
.map(
|(
file_uuid,
trace_id,
frame_count,
first_frame,
last_frame,
first_sec,
last_sec,
avg_confidence,
)| IdentityTraceInfo {
file_uuid,
trace_id,
frame_count,
first_frame,
last_frame,
first_sec,
last_sec,
avg_confidence,
},
)
.collect();
Ok(Json(IdentityTracesResponse {
success: true,
identity_uuid,
name,
total_traces,
total_faces,
traces,
}))
}
pub fn identity_binding_routes() -> Router<crate::api::server::AppState> {
Router::new()
.route("/api/v1/identity/:identity_uuid/bind", post(bind_identity))
@@ -350,4 +469,8 @@ pub fn identity_binding_routes() -> Router<crate::api::server::AppState> {
"/api/v1/identity/:identity_uuid/mergeinto",
post(merge_identities),
)
.route(
"/api/v1/identity/:identity_uuid/traces",
get(get_identity_traces),
)
}