feat: trace-level matching, health watcher/worker status, timezone config
This commit is contained in:
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user