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, #[serde(default)] pub fixed_annotations: Vec, #[serde(default)] pub range_annotations: Vec, } #[derive(Deserialize, Default)] pub struct ExportVideo { #[serde(default)] pub file_name: String, #[serde(default)] pub width: Option, #[serde(default)] pub height: Option, #[serde(default)] pub fps: Option, #[serde(default)] pub frame_count: Option, #[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, #[serde(default)] pub end_vertices: Option, #[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, 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, 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, pub height: Option, pub fps: Option, pub frame_count: Option, 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>, // Phase 3: claim/verify dimension (orthogonal to `status`). pub claimed_by: Option, pub claimed_at: Option>, pub lease_expires_at: Option>, pub completed_by: Option, pub completed_at: Option>, 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, 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, pub gps_error: Option, // 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, pub gps_max_gap_s: Option, // Real container duration from the scan (raw NAS videos have no fps/frame_count). pub duration_s: Option, // Client-calibrated chainage + the immutable GPS-measured (sr) chainage, meters. pub chainage_start_m: Option, pub chainage_end_m: Option, pub sr_chainage_start_m: Option, pub sr_chainage_end_m: Option, pub route_seq: Option, // "Raise hand" signal — anyone may raise (to start a conversation) or lower it. pub hand_raised: bool, pub hand_raised_by: Option, pub hand_raised_at: Option>, // "Ignore" flag — admin-only; the UI crosses out ignored rows. pub ignored: bool, pub ignored_by: Option, // 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, pub claimed_by: Option, pub annotation_count: i32, pub road_type: String, pub gps_status: String, pub gps_len_m: Option, pub gps_error: Option, pub chainage_start_m: Option, pub chainage_end_m: Option, pub sr_chainage_start_m: Option, pub sr_chainage_end_m: Option, pub route_seq: Option, pub remark_count: i64, pub track: Option, pub corrected: bool, // Who/what produced the correction ('auto' or a username) + its summary note. pub gps_corrected_by: Option, pub gps_corrected_note: Option, // Data quality from the raw scan: fix count + biggest dropout between fixes. pub gps_sample_count: Option, pub gps_max_gap_s: Option, } /// `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, } /// `POST /api/projects/:id/road_type_assign {video_ids, road_type}` — bulk-assign. #[derive(Deserialize)] pub struct RoadTypeAssignRequest { pub video_ids: Vec, 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, 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, } #[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, } #[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, // Phase 3: verification leaderboard (by `completed_by`). pub verifiers: Vec, // 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, /// 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>, /// 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, } /// `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, pub height: Option, pub fps: Option, pub frame_count: Option, pub lease_expires_at: chrono::DateTime, 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, } /// `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, 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, pub recent_events: Vec, } #[derive(Serialize, sqlx::FromRow)] pub struct ActiveClaim { pub video_id: i32, pub file_name: String, pub claimed_by: String, pub claimed_at: chrono::DateTime, pub lease_expires_at: chrono::DateTime, } #[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, }