Central Verification Dashboard
This commit is contained in:
426
server/src/models.rs
Normal file
426
server/src/models.rs
Normal 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>,
|
||||
}
|
||||
Reference in New Issue
Block a user