detailed data for verifications
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
api, exportProjectUrl, exportVideoUrl, fmtDuration, fmtWhen, setAuthToken,
|
api, exportProjectUrl, exportVideoUrl, fmtDuration, fmtWhen, passDeltas, setAuthToken,
|
||||||
type Activity, type Member, type ProjectSummary, type Stats, type UserRow, type VideoRow,
|
type Activity, type Member, type ProjectSummary, type Stats, type UserRow, type VideoRow,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import { RemarkCell } from "./RemarkCell";
|
import { RemarkCell } from "./RemarkCell";
|
||||||
@@ -505,15 +505,27 @@ export function App() {
|
|||||||
{v.annotation_count > 0
|
{v.annotation_count > 0
|
||||||
? <a href={exportVideoUrl(v.id)} download title="Download this video's annotations (JSON)">{v.annotation_count} ⬇</a>
|
? <a href={exportVideoUrl(v.id)} download title="Download this video's annotations (JSON)">{v.annotation_count} ⬇</a>
|
||||||
: 0}
|
: 0}
|
||||||
{v.imported_count > 0 && (
|
{(v.imported_count > 0 || (v.passes?.length ?? 0) > 0) && (() => {
|
||||||
<div className="imported-line" title="annotations present when verification started (imported baseline)">
|
const deltas = passDeltas(v.imported_count, v.passes ?? []);
|
||||||
<span className="dim">imported {v.imported_count}</span>
|
return (
|
||||||
{v.annotation_count < v.imported_count &&
|
<div className="imported-line" title="imported baseline → per-pass changes (verify / re-verify)">
|
||||||
<span className="ann-del" title={`${v.imported_count - v.annotation_count} deleted by the user`}> · −{v.imported_count - v.annotation_count}</span>}
|
<span className="dim">imported {v.imported_count}</span>
|
||||||
{v.annotation_count > v.imported_count &&
|
{deltas.length > 0
|
||||||
<span className="ann-add" title={`${v.annotation_count - v.imported_count} added by the user`}> · +{v.annotation_count - v.imported_count}</span>}
|
? deltas.map((d, i) => (
|
||||||
</div>
|
<span key={i} className={d.delta >= 0 ? "ann-add" : "ann-del"}
|
||||||
)}
|
title={`${d.label} by ${d.username} → ${d.total} total`}>
|
||||||
|
{" · "}{d.label} {d.delta >= 0 ? "+" : ""}{d.delta}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
: <>
|
||||||
|
{v.annotation_count < v.imported_count &&
|
||||||
|
<span className="ann-del" title={`${v.imported_count - v.annotation_count} deleted by the user`}> · −{v.imported_count - v.annotation_count}</span>}
|
||||||
|
{v.annotation_count > v.imported_count &&
|
||||||
|
<span className="ann-add" title={`${v.annotation_count - v.imported_count} added by the user`}> · +{v.annotation_count - v.imported_count}</span>}
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td>{v.review_count > 0
|
<td>{v.review_count > 0
|
||||||
? <span className="review-flag" title={`${v.review_count} annotation(s) flagged for review`}>🚩 {v.review_count}</span>
|
? <span className="review-flag" title={`${v.review_count} annotation(s) flagged for review`}>🚩 {v.review_count}</span>
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ export interface Remark {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One verification pass (a push). `count` is the annotation total after that pass;
|
||||||
|
* null for legacy pushes logged before we recorded it. */
|
||||||
|
export interface Pass {
|
||||||
|
username: string;
|
||||||
|
at: string;
|
||||||
|
count: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VideoRow {
|
export interface VideoRow {
|
||||||
id: number;
|
id: number;
|
||||||
file_name: string;
|
file_name: string;
|
||||||
@@ -75,6 +83,7 @@ export interface VideoRow {
|
|||||||
ignored: boolean;
|
ignored: boolean;
|
||||||
ignored_by: string | null;
|
ignored_by: string | null;
|
||||||
remarks: Remark[];
|
remarks: Remark[];
|
||||||
|
passes: Pass[]; // per-pass annotation-count history (push order)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LeaderRow {
|
export interface LeaderRow {
|
||||||
@@ -281,6 +290,25 @@ export const api = {
|
|||||||
post<{ folders: number }>(`/projects/${projectId}/folders`, { folders }, token),
|
post<{ folders: number }>(`/projects/${projectId}/folders`, { folders }, token),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Per-pass annotation deltas from the imported baseline: verify +Δ, re-verify +Δ, …
|
||||||
|
* Skips legacy passes that have no recorded count. The first pass is "verify",
|
||||||
|
* subsequent ones "re-verify". `total` is the running annotation count after the pass. */
|
||||||
|
export function passDeltas(
|
||||||
|
importedCount: number,
|
||||||
|
passes: Pass[],
|
||||||
|
): { label: string; delta: number; username: string; total: number }[] {
|
||||||
|
const out: { label: string; delta: number; username: string; total: number }[] = [];
|
||||||
|
let prev = importedCount;
|
||||||
|
let n = 0;
|
||||||
|
for (const p of passes ?? []) {
|
||||||
|
if (p.count == null) continue;
|
||||||
|
n += 1;
|
||||||
|
out.push({ label: n === 1 ? "verify" : "re-verify", delta: p.count - prev, username: p.username, total: p.count });
|
||||||
|
prev = p.count;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** Human-readable byte size. */
|
/** Human-readable byte size. */
|
||||||
export function fmtBytes(n: number): string {
|
export function fmtBytes(n: number): string {
|
||||||
if (!n || n <= 0) return "0 B";
|
if (!n || n <= 0) return "0 B";
|
||||||
|
|||||||
@@ -549,7 +549,14 @@ async fn list_videos(State(s): State<AppState>, user: AuthUser, AxPath(id): AxPa
|
|||||||
hand_raised, hand_raised_by, hand_raised_at, ignored, ignored_by,
|
hand_raised, hand_raised_by, hand_raised_at, ignored, ignored_by,
|
||||||
COALESCE((SELECT json_agg(json_build_object('username', r.username, 'body', r.body, 'created_at', r.created_at)
|
COALESCE((SELECT json_agg(json_build_object('username', r.username, 'body', r.body, 'created_at', r.created_at)
|
||||||
ORDER BY r.created_at)
|
ORDER BY r.created_at)
|
||||||
FROM video_remarks r WHERE r.video_id = videos.id), '[]'::json) AS remarks
|
FROM video_remarks r WHERE r.video_id = videos.id), '[]'::json) AS remarks,
|
||||||
|
-- per-pass annotation-count history (each push, in order): lets the UI
|
||||||
|
-- show imported baseline → verify Δ → re-verify Δ. count is null for
|
||||||
|
-- legacy pushes logged before we recorded it.
|
||||||
|
COALESCE((SELECT json_agg(json_build_object(
|
||||||
|
'username', e.username, 'at', e.at,
|
||||||
|
'count', (e.meta->>'annotation_count')::int) ORDER BY e.at)
|
||||||
|
FROM video_events e WHERE e.video_id = videos.id AND e.event='push'), '[]'::json) AS passes
|
||||||
FROM videos WHERE project_id=$1 {FOLDER_FILTER} ORDER BY file_name"#);
|
FROM videos WHERE project_id=$1 {FOLDER_FILTER} ORDER BY file_name"#);
|
||||||
let rows = sqlx::query_as::<_, VideoRow>(&q)
|
let rows = sqlx::query_as::<_, VideoRow>(&q)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
|
|||||||
@@ -157,6 +157,10 @@ pub struct VideoRow {
|
|||||||
pub ignored_by: Option<String>,
|
pub ignored_by: Option<String>,
|
||||||
// Remark thread (json array of {username, body, created_at}); '[]' when none.
|
// Remark thread (json array of {username, body, created_at}); '[]' when none.
|
||||||
pub remarks: serde_json::Value,
|
pub remarks: serde_json::Value,
|
||||||
|
// Per-pass annotation-count history (json array of {username, at, count}, one per
|
||||||
|
// push in order); '[]' when never pushed. Drives the imported→verify→re-verify
|
||||||
|
// count breakdown in the UI.
|
||||||
|
pub passes: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `POST /api/videos/:id/hand {raised}` — raise/lower the hand on a video.
|
/// `POST /api/videos/:id/hand {raised}` — raise/lower the hand on a video.
|
||||||
|
|||||||
@@ -282,7 +282,11 @@ pub async fn push(
|
|||||||
verify_time_ms = COALESCE(videos.verify_time_ms, 0) + $3,
|
verify_time_ms = COALESCE(videos.verify_time_ms, 0) + $3,
|
||||||
annotation_count=$4, raw_json=$5,
|
annotation_count=$4, raw_json=$5,
|
||||||
primary_annotator=$6, annotation_time_ms=$7, annotated_at=now(),
|
primary_annotator=$6, annotation_time_ms=$7, annotated_at=now(),
|
||||||
imported_count = COALESCE(NULLIF($8, 0), videos.imported_count),
|
-- imported_count is the ORIGINAL auto-imported baseline (set at ingest).
|
||||||
|
-- Keep it fixed once set: verification/re-verification must NOT fold added
|
||||||
|
-- annotations into it (otherwise "imported 200" would drift to 210). Only
|
||||||
|
-- establish it from the claim-time count if it was never set.
|
||||||
|
imported_count = COALESCE(NULLIF(videos.imported_count, 0), $8),
|
||||||
claimed_by=NULL, claimed_at=NULL, lease_expires_at=NULL
|
claimed_by=NULL, claimed_at=NULL, lease_expires_at=NULL
|
||||||
WHERE id=$1"#,
|
WHERE id=$1"#,
|
||||||
)
|
)
|
||||||
@@ -299,7 +303,16 @@ pub async fn push(
|
|||||||
.map_err(err)?;
|
.map_err(err)?;
|
||||||
tx.commit().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;
|
// Record this pass's resulting annotation count so the dashboard can show a
|
||||||
|
// per-pass breakdown (imported baseline → verify Δ → re-verify Δ …), not just a net.
|
||||||
|
log_event(
|
||||||
|
&s.db,
|
||||||
|
id,
|
||||||
|
&user.username,
|
||||||
|
"push",
|
||||||
|
json!({ "verify_time_ms": verify_time_ms, "annotation_count": ann_count }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Mark project activity + auto-resume its timer (a verify/re-verify restarts it).
|
// Mark project activity + auto-resume its timer (a verify/re-verify restarts it).
|
||||||
let _ = sqlx::query(
|
let _ = sqlx::query(
|
||||||
|
|||||||
Reference in New Issue
Block a user