minor bugs

This commit is contained in:
2026-06-23 20:17:32 +05:30
parent c81e503af0
commit bf019f86c3
6 changed files with 133 additions and 60 deletions

View File

@@ -218,20 +218,27 @@ pub async fn push(
AxPath(id): AxPath<i32>,
Json(body): Json<Value>,
) -> ApiResult<Value> {
// The current holder (or an admin) may push. A null holder (auto-released, not
// re-claimed) is tolerated; a different holder is rejected.
let claimed_by: Option<Option<String>> =
sqlx::query_scalar("SELECT claimed_by FROM videos WHERE id=$1")
// A non-admin pusher must currently hold an ACTIVE claim on this video. This blocks
// a stale/expired claim from overwriting newer authoritative server state: if the
// lease lapsed (auto-released, or re-claimed by someone else) the worker must
// re-claim before pushing — their local work is never lost, it just can't clobber a
// claim they no longer hold. Admins may always push.
let row: Option<(Option<String>, Option<chrono::DateTime<chrono::Utc>>)> =
sqlx::query_as("SELECT claimed_by, lease_expires_at FROM videos WHERE id=$1")
.bind(id)
.fetch_optional(&s.db)
.await
.map_err(err)?;
let claimed_by = claimed_by.ok_or((StatusCode::NOT_FOUND, "video not found".to_string()))?;
let (claimed_by, lease_expires_at) =
row.ok_or((StatusCode::NOT_FOUND, "video not found".to_string()))?;
if !user.is_admin() {
if let Some(holder) = claimed_by.as_deref() {
if holder != user.username {
return Err((StatusCode::CONFLICT, "claim is held by another worker".into()));
}
let holds_active_claim = claimed_by.as_deref() == Some(user.username.as_str())
&& lease_expires_at.map(|l| l > chrono::Utc::now()).unwrap_or(false);
if !holds_active_claim {
return Err((
StatusCode::CONFLICT,
"your claim has expired or is held by someone else — re-claim the video before pushing".into(),
));
}
}
@@ -243,9 +250,6 @@ pub async fn push(
let doc: ExportDoc = serde_json::from_value(body.clone())
.map_err(|e| (StatusCode::BAD_REQUEST, format!("invalid export doc: {e}")))?;
crate::ingest::replace_annotations(&s.db, id, &doc)
.await
.map_err(err)?;
let ann_count = (doc.fixed_annotations.len() + doc.range_annotations.len()) as i32;
// Recompute the video-level annotator fields from the pushed doc (same idea as
@@ -261,6 +265,15 @@ pub async fn push(
let primary_annotator = freq.into_iter().max_by_key(|(_, c)| *c).map(|(u, _)| u).unwrap_or_default();
let ann_time_ms = doc.video.as_ref().map(|v| v.annotation_time_ms).unwrap_or(0);
// Replace the annotation set AND flip the video to verified in ONE transaction:
// either the pushed annotations + the verified status both commit, or neither does.
// This makes a push all-or-nothing — a verifier can never end up "verified" with a
// half-written annotation set, and a failed push leaves the prior pass intact.
let mut tx = s.db.begin().await.map_err(err)?;
crate::ingest::replace_annotations(&mut tx, id, &doc)
.await
.map_err(err)?;
// Accumulate verify_time_ms across passes (re-verification adds up). The list
// of verifiers + the pass count are derived from video_events (each push logs one).
sqlx::query(
@@ -281,9 +294,10 @@ pub async fn push(
.bind(&primary_annotator)
.bind(ann_time_ms)
.bind(imported_count)
.execute(&s.db)
.execute(&mut *tx)
.await
.map_err(err)?;
tx.commit().await.map_err(err)?;
log_event(&s.db, id, &user.username, "push", json!({ "verify_time_ms": verify_time_ms })).await;