508 lines
15 KiB
Rust
508 lines
15 KiB
Rust
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,
|
||
// Road type label ('' = unassigned) — injected into the export/claim JSON.
|
||
pub road_type: String,
|
||
// GPS pipeline: pending | ok | none | error (+ measured track length / last error).
|
||
pub gps_status: String,
|
||
pub gps_len_m: Option<f64>,
|
||
pub gps_error: Option<String>,
|
||
// Data quality: raw fix count + biggest dropout — the table compares the count
|
||
// against the video duration (≈1 fix/s) and flags mismatches.
|
||
pub gps_sample_count: Option<i32>,
|
||
pub gps_max_gap_s: Option<f64>,
|
||
// Real container duration from the scan (raw NAS videos have no fps/frame_count).
|
||
pub duration_s: Option<f64>,
|
||
// Client-calibrated chainage + the immutable GPS-measured (sr) chainage, meters.
|
||
pub chainage_start_m: Option<f64>,
|
||
pub chainage_end_m: Option<f64>,
|
||
pub sr_chainage_start_m: Option<f64>,
|
||
pub sr_chainage_end_m: Option<f64>,
|
||
pub route_seq: Option<i32>,
|
||
// "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,
|
||
// 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,
|
||
}
|
||
|
||
/// One row of `GET /api/projects/:id/map` — a video as the map sees it. `track` is
|
||
/// the (transport-downsampled) effective polyline: corrected when kept, else raw;
|
||
/// None until the GPS sweep has scanned the video (or when it has no metadata).
|
||
#[derive(Serialize, sqlx::FromRow)]
|
||
pub struct MapVideo {
|
||
pub id: i32,
|
||
pub file_name: String,
|
||
pub rel_path: String,
|
||
pub status: String,
|
||
pub workflow_status: String,
|
||
pub ignored: bool,
|
||
pub hand_raised: bool,
|
||
pub assigned_to: Option<String>,
|
||
pub claimed_by: Option<String>,
|
||
pub annotation_count: i32,
|
||
pub road_type: String,
|
||
pub gps_status: String,
|
||
pub gps_len_m: Option<f64>,
|
||
pub gps_error: Option<String>,
|
||
pub chainage_start_m: Option<f64>,
|
||
pub chainage_end_m: Option<f64>,
|
||
pub sr_chainage_start_m: Option<f64>,
|
||
pub sr_chainage_end_m: Option<f64>,
|
||
pub route_seq: Option<i32>,
|
||
pub remark_count: i64,
|
||
pub track: Option<Value>,
|
||
pub corrected: bool,
|
||
// Who/what produced the correction ('auto' or a username) + its summary note.
|
||
pub gps_corrected_by: Option<String>,
|
||
pub gps_corrected_note: Option<String>,
|
||
// Data quality from the raw scan: fix count + biggest dropout between fixes.
|
||
pub gps_sample_count: Option<i32>,
|
||
pub gps_max_gap_s: Option<f64>,
|
||
}
|
||
|
||
/// `GET /api/projects/:id/road_types` — a label + how many videos carry it.
|
||
#[derive(Serialize, sqlx::FromRow)]
|
||
pub struct RoadTypeRow {
|
||
pub name: String,
|
||
pub video_count: i64,
|
||
}
|
||
|
||
/// `POST /api/projects/:id/road_types {names}` — replace the label set (admin).
|
||
#[derive(Deserialize)]
|
||
pub struct RoadTypesRequest {
|
||
pub names: Vec<String>,
|
||
}
|
||
|
||
/// `POST /api/projects/:id/road_type_assign {video_ids, road_type}` — bulk-assign.
|
||
#[derive(Deserialize)]
|
||
pub struct RoadTypeAssignRequest {
|
||
pub video_ids: Vec<i32>,
|
||
pub road_type: String, // '' = clear
|
||
}
|
||
|
||
/// `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,
|
||
/// Annotations added per minute of work (annotations ÷ total_time). This is the
|
||
/// ranking metric: "most annotations in the least time". 0 when no time/annotations.
|
||
pub annotations_per_min: f64,
|
||
/// 1-based rank by annotations-per-minute (highest first); 0 = not yet ranked
|
||
/// (no annotations or no recorded time).
|
||
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>,
|
||
}
|