diff --git a/src/faces.rs b/src/faces.rs index 10cd0b6..66ddfca 100644 --- a/src/faces.rs +++ b/src/faces.rs @@ -117,6 +117,35 @@ struct InsertFaceDetection { created_at: i64, } +/// Build a [`FaceWithPerson`] from a freshly-mutated row by resolving the +/// person name via [`FaceDao::get_person`]. Used by `create_face_handler` +/// and `update_face_handler` so PATCH/POST responses match the join shape +/// `/image/faces` returns — without this the carousel overlay's +/// optimistic-replace would clobber the rendered name (the bare +/// [`FaceDetectionRow`] doesn't carry it). +fn hydrate_face_with_person( + dao: &mut D, + ctx: &opentelemetry::Context, + row: FaceDetectionRow, +) -> anyhow::Result { + let person_name = match row.person_id { + Some(pid) => dao.get_person(ctx, pid)?.map(|p| p.name), + None => None, + }; + Ok(FaceWithPerson { + id: row.id, + bbox_x: row.bbox_x.unwrap_or(0.0), + bbox_y: row.bbox_y.unwrap_or(0.0), + bbox_w: row.bbox_w.unwrap_or(0.0), + bbox_h: row.bbox_h.unwrap_or(0.0), + confidence: row.confidence.unwrap_or(0.0), + source: row.source, + person_id: row.person_id, + person_name, + model_version: row.model_version, + }) +} + /// Face row decorated with its assigned person's name. Returned by /// `/image/faces` for the rendering side (carousel overlay, person chips). #[derive(Serialize, Debug, Clone)] @@ -2099,7 +2128,10 @@ async fn create_face_handler( "Created manual face id={} library={} hash={} person_id={:?}", row.id, row.library_id, row.content_hash, row.person_id ); - HttpResponse::Created().json(row) + match hydrate_face_with_person(&mut *dao, &span_context, row) { + Ok(joined) => HttpResponse::Created().json(joined), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } } async fn update_face_handler( @@ -2205,8 +2237,16 @@ async fn update_face_handler( } let mut dao = face_dao.lock().expect("face dao lock"); - match dao.update_face(&span_context, id, person_patch, bbox_patch, new_embedding) { - Ok(row) => HttpResponse::Ok().json(row), + let row = match dao.update_face(&span_context, id, person_patch, bbox_patch, new_embedding) { + Ok(r) => r, + Err(e) => return HttpResponse::InternalServerError().body(e.to_string()), + }; + // Hydrate person_name so the response shape matches GET /image/faces + // — the carousel overlay does an optimistic replace on this row, and + // a bare FaceDetectionRow with no person_name would visibly drop the + // VFD label off the bbox even though the assignment didn't change. + match hydrate_face_with_person(&mut *dao, &span_context, row) { + Ok(joined) => HttpResponse::Ok().json(joined), Err(e) => HttpResponse::InternalServerError().body(e.to_string()), } }