Central Verification Dashboard

This commit is contained in:
2026-06-16 14:32:21 +05:30
commit 7cb85d5ebc
37 changed files with 9665 additions and 0 deletions

426
server/src/models.rs Normal file
View File

@@ -0,0 +1,426 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
// ---- Parsed from the sibling export-JSON (video-annotator export format) ----
#[derive(Deserialize, Default)]
pub struct ExportDoc {
#[serde(default)]
pub video: Option<ExportVideo>,
#[serde(default)]
pub fixed_annotations: Vec<FixedAnn>,
#[serde(default)]
pub range_annotations: Vec<RangeAnn>,
}
#[derive(Deserialize, Default)]
pub struct ExportVideo {
#[serde(default)]
pub file_name: String,
#[serde(default)]
pub width: Option<i64>,
#[serde(default)]
pub height: Option<i64>,
#[serde(default)]
pub fps: Option<f64>,
#[serde(default)]
pub frame_count: Option<i64>,
#[serde(default)]
pub annotation_time_ms: i64,
}
#[derive(Deserialize)]
pub struct FixedAnn {
#[serde(default)]
pub label: String,
#[serde(default)]
pub side: String,
#[serde(default)]
pub frame_number: i64,
#[serde(default, rename = "type")]
pub shape_type: String,
#[serde(default)]
pub vertices: Value,
#[serde(default)]
pub annotated_by: String,
#[serde(default)]
pub remark: String,
#[serde(default)]
pub review_status: String,
#[serde(default)]
pub subclass: String,
#[serde(default)]
pub created_at: String,
}
#[derive(Deserialize)]
pub struct RangeAnn {
#[serde(default)]
pub label: String,
#[serde(default)]
pub side: String,
#[serde(default)]
pub start_frame: i64,
#[serde(default)]
pub start_vertices: Value,
#[serde(default)]
pub end_frame: Option<i64>,
#[serde(default)]
pub end_vertices: Option<Value>,
#[serde(default, rename = "type")]
pub shape_type: String,
#[serde(default)]
pub annotated_by: String,
#[serde(default)]
pub remark: String,
#[serde(default)]
pub review_status: String,
#[serde(default)]
pub subclass: String,
#[serde(default)]
pub created_at: String,
}
// ---- API response shapes ----
#[derive(Serialize)]
pub struct ProjectSummary {
pub id: i32,
pub name: String,
pub source_path: String,
pub source_kind: String,
pub client_id: Option<i32>,
pub client_name: String,
pub total: i64,
pub pending: i64,
pub annotated: i64,
pub verified: i64,
}
/// A client owning one or more projects (admin-created from the dashboard).
#[derive(Serialize)]
pub struct ClientRow {
pub id: i32,
pub name: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub project_count: i64,
}
/// Admin: `POST /api/clients` — create a client.
#[derive(Deserialize)]
pub struct NewClientRequest {
pub name: String,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct VideoRow {
pub id: i32,
pub file_name: String,
pub rel_path: String,
pub has_json: bool,
pub width: Option<i32>,
pub height: Option<i32>,
pub fps: Option<f64>,
pub frame_count: Option<i32>,
pub annotation_count: i32,
// Baseline count this video started a verification pass with: the NAS-ingested
// count, or (after a push) the count the worker had at claim time. Compared with
// annotation_count it shows how many the worker added/deleted.
pub imported_count: i32,
pub annotation_time_ms: i64,
pub primary_annotator: String,
pub status: String,
pub annotated_at: Option<chrono::DateTime<chrono::Utc>>,
// Phase 3: claim/verify dimension (orthogonal to `status`).
pub claimed_by: Option<String>,
pub claimed_at: Option<chrono::DateTime<chrono::Utc>>,
pub lease_expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub completed_by: Option<String>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
pub verify_time_ms: i64,
// Annotations still flagged for review (review_status='flagged') on this video.
pub review_count: i64,
// How many times pushed (1 = verified, ≥2 = re-verified) + the distinct verifiers
// ("ravi, john"), both derived from the push event log.
pub verify_count: i64,
pub verifiers: String,
// Admin assignment (advisory owner) + the derived lifecycle label for the UI badge:
// pending | annotated | assigned | in-progress | verified | re-verified.
pub assigned_to: Option<String>,
pub workflow_status: String,
// "Raise hand" signal — anyone may raise (to start a conversation) or lower it.
pub hand_raised: bool,
pub hand_raised_by: Option<String>,
pub hand_raised_at: Option<chrono::DateTime<chrono::Utc>>,
// "Ignore" flag — admin-only; the UI crosses out ignored rows.
pub ignored: bool,
pub ignored_by: Option<String>,
// Remark thread (json array of {username, body, created_at}); '[]' when none.
pub remarks: serde_json::Value,
}
/// `POST /api/videos/:id/hand {raised}` — raise/lower the hand on a video.
#[derive(Deserialize)]
pub struct HandRequest {
pub raised: bool,
}
/// `POST /api/videos/:id/ignore {ignored}` — admin-only ignore toggle.
#[derive(Deserialize)]
pub struct IgnoreRequest {
pub ignored: bool,
}
#[derive(Deserialize)]
pub struct AssignRequest {
pub video_ids: Vec<i32>,
pub assignee: String, // empty = unassign
}
/// A user picked for a project (project_members joined with users).
#[derive(Serialize, sqlx::FromRow)]
pub struct MemberRow {
pub username: String,
pub display_name: String,
pub role: String,
pub added_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize)]
pub struct AddMemberRequest {
pub username: String,
}
/// One line in a video's remark thread.
#[derive(Serialize, sqlx::FromRow)]
pub struct Remark {
pub username: String,
pub body: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize)]
pub struct NewRemarkRequest {
pub body: String,
}
#[derive(Serialize)]
pub struct Overview {
pub total: i64,
pub pending: i64,
pub annotated: i64,
pub verified: i64,
pub pct_done: f64,
}
#[derive(Serialize)]
pub struct LeaderRow {
pub user: String,
pub videos: i64,
pub annotations: i64,
pub total_time_ms: i64,
pub avg_time_ms: i64,
}
#[derive(Serialize)]
pub struct Stats {
pub overview: Overview,
pub leaderboard: Vec<LeaderRow>,
// Phase 3: verification leaderboard (by `completed_by`).
pub verifiers: Vec<VerifierRow>,
// Project time, verification pace + worker-based ETA.
pub timeline: ProjectTime,
}
/// Aggregate project time metrics + a worker-parallelised ETA for the remaining work.
#[derive(Serialize)]
pub struct ProjectTime {
/// Users picked for the project (the parallelism factor for the ETA).
pub active_workers: i64,
/// Average effort per verified video (annotation + verify time).
pub avg_video_ms: i64,
/// Total time spent by all users across all videos (annotation + verify).
pub total_spent_ms: i64,
/// Wall-clock from project inception to last completion (or now if ongoing).
pub elapsed_ms: i64,
/// Whether every video is verified.
pub all_verified: bool,
/// Estimated remaining work time = remaining × avg_video ÷ active_workers.
pub eta_ms: Option<i64>,
/// Whether the project timer is currently running (accumulating).
pub time_running: bool,
/// Last verify/re-verify activity (drives the 72h auto-stop).
pub last_activity_at: Option<chrono::DateTime<chrono::Utc>>,
/// Verification pace: push (verify) events in the recent window, and the window
/// length in days. videos/day = recent_verified ÷ recent_window_days.
pub recent_verified: i64,
pub recent_window_days: i64,
}
/// `POST /api/projects/:id/timer {action}` — start | stop | reset (admin only).
#[derive(Deserialize)]
pub struct TimerRequest {
pub action: String,
}
/// `POST /api/projects/:id/folders {folders}` — set the folders to consider (admin).
#[derive(Deserialize)]
pub struct FoldersRequest {
pub folders: Vec<String>,
}
/// `GET /api/projects/:id/folders` — a folder in the project + its video count +
/// whether it's currently included in verification.
#[derive(Serialize, sqlx::FromRow)]
pub struct FolderRow {
pub folder: String,
pub video_count: i64,
pub included: bool,
}
#[derive(Serialize)]
pub struct VerifierRow {
pub user: String,
pub videos: i64,
pub total_time_ms: i64,
pub avg_time_ms: i64,
/// Total annotations this user authored (annotated_by) in the project.
pub annotations: i64,
/// 1-based rank by completion speed (fastest avg first); 0 = no completions yet.
pub rank: i64,
}
// ---- Phase 3: auth + worker pull/push shapes ----
/// `GET /api/auth/whoami` — identifies the bearer.
#[derive(Serialize)]
pub struct WhoAmI {
pub username: String,
pub role: String,
}
/// `POST /api/videos/:id/claim` response — everything the desktop needs to verify.
#[derive(Serialize)]
pub struct ClaimResponse {
pub video_id: i32,
pub file_name: String,
pub rel_path: String,
pub width: Option<i32>,
pub height: Option<i32>,
pub fps: Option<f64>,
pub frame_count: Option<i32>,
pub lease_expires_at: chrono::DateTime<chrono::Utc>,
pub download_url: String,
/// Full preloaded export doc (`{video, fixed_annotations[], range_annotations[]}`)
/// for the desktop to import via its existing import path.
pub annotations: Value,
}
/// Admin: `POST /api/users` — provision a worker/admin.
#[derive(Deserialize)]
pub struct NewUserRequest {
pub username: String,
#[serde(default)]
pub display_name: String,
#[serde(default = "default_role")]
pub role: String,
}
fn default_role() -> String {
"ml_support".into()
}
/// Admin: `POST /api/users/:id/update` — edit a user's display name + role.
#[derive(Deserialize)]
pub struct UpdateUserRequest {
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub role: String,
}
/// Admin user-provision response — the **only** time the clear token is shown.
#[derive(Serialize)]
pub struct NewUserResponse {
pub username: String,
pub display_name: String,
pub role: String,
pub token: String,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct UserRow {
pub id: i32,
pub username: String,
pub display_name: String,
pub role: String,
pub active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// `POST /api/projects` — create a project under a client (any authenticated user).
#[derive(Deserialize)]
pub struct NewProjectRequest {
pub client_id: i32,
pub name: String,
pub source_path: String,
/// 'local' (default) or 'nfs'.
#[serde(default = "default_source_kind")]
pub source_kind: String,
}
fn default_source_kind() -> String {
"local".into()
}
/// `POST /api/projects/:id/source` — change a project's sync directory (any authenticated user).
#[derive(Deserialize)]
pub struct UpdateSourceRequest {
pub source_path: String,
#[serde(default = "default_source_kind")]
pub source_kind: String,
}
/// `GET /api/admin/storage` — DB size + row counts (admin only).
#[derive(Serialize)]
pub struct StorageInfo {
pub db_bytes: i64,
pub videos: i64,
pub annotations: i64,
pub video_events: i64,
pub audit_events: i64,
/// Retention cap applied nightly to the event/audit logs.
pub max_log_rows: i64,
}
/// `GET /api/audit` — an org-level audit row.
#[derive(Serialize, sqlx::FromRow)]
pub struct AuditRow {
pub at: chrono::DateTime<chrono::Utc>,
pub username: String,
pub action: String,
pub detail: Value,
}
/// `GET /api/projects/:id/activity` — live claims + recent events.
#[derive(Serialize)]
pub struct Activity {
pub active_claims: Vec<ActiveClaim>,
pub recent_events: Vec<EventRow>,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct ActiveClaim {
pub video_id: i32,
pub file_name: String,
pub claimed_by: String,
pub claimed_at: chrono::DateTime<chrono::Utc>,
pub lease_expires_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct EventRow {
pub video_id: i32,
pub file_name: String,
pub username: String,
pub event: String,
pub at: chrono::DateTime<chrono::Utc>,
}