Skip to main content

heldar_kernel/
models.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use sqlx::types::Json;
5use sqlx::FromRow;
6
7use crate::camera_url;
8
9/// Camera row as stored. `password` is never serialized to clients; use [`CameraView`] for output.
10#[derive(Debug, Clone, FromRow)]
11pub struct Camera {
12    pub id: String,
13    pub site_id: Option<String>,
14    pub name: String,
15    pub vendor: String,
16    pub model: Option<String>,
17    pub address: Option<String>,
18    pub rtsp_port: i64,
19    pub username: Option<String>,
20    pub password: Option<String>,
21    pub main_stream_url: Option<String>,
22    pub sub_stream_url: Option<String>,
23    pub record_stream: String,
24    pub codec: Option<String>,
25    pub resolution_main: Option<String>,
26    pub resolution_sub: Option<String>,
27    pub fps_main: Option<i64>,
28    pub fps_sub: Option<i64>,
29    pub capabilities: Json<Value>,
30    pub record_enabled: bool,
31    pub segment_seconds: i64,
32    pub retention_hours: i64,
33    pub enabled: bool,
34    pub created_at: DateTime<Utc>,
35    pub updated_at: DateTime<Utc>,
36}
37
38impl Camera {
39    /// Whether the recorder should be running a process for this camera.
40    pub fn should_record(&self) -> bool {
41        self.enabled && self.record_enabled
42    }
43}
44
45/// Client-facing camera representation: credentials stripped, stream URLs masked.
46#[derive(Debug, Clone, Serialize)]
47pub struct CameraView {
48    pub id: String,
49    pub site_id: Option<String>,
50    pub name: String,
51    pub vendor: String,
52    pub model: Option<String>,
53    pub address: Option<String>,
54    pub rtsp_port: i64,
55    pub username: Option<String>,
56    pub has_password: bool,
57    pub record_stream: String,
58    /// Effective RTSP URL for the recorded stream, with credentials masked.
59    pub record_url_masked: Option<String>,
60    pub codec: Option<String>,
61    pub resolution_main: Option<String>,
62    pub resolution_sub: Option<String>,
63    pub fps_main: Option<i64>,
64    pub fps_sub: Option<i64>,
65    pub capabilities: Value,
66    pub record_enabled: bool,
67    pub segment_seconds: i64,
68    pub retention_hours: i64,
69    pub enabled: bool,
70    pub created_at: DateTime<Utc>,
71    pub updated_at: DateTime<Utc>,
72}
73
74impl From<Camera> for CameraView {
75    fn from(c: Camera) -> Self {
76        let record_url_masked = camera_url::record_url(&c).map(|u| camera_url::mask_url(&u));
77        CameraView {
78            id: c.id,
79            site_id: c.site_id,
80            name: c.name,
81            vendor: c.vendor,
82            model: c.model,
83            address: c.address,
84            rtsp_port: c.rtsp_port,
85            username: c.username,
86            has_password: c
87                .password
88                .as_deref()
89                .map(|p| !p.is_empty())
90                .unwrap_or(false),
91            record_stream: c.record_stream,
92            record_url_masked,
93            codec: c.codec,
94            resolution_main: c.resolution_main,
95            resolution_sub: c.resolution_sub,
96            fps_main: c.fps_main,
97            fps_sub: c.fps_sub,
98            capabilities: c.capabilities.0,
99            record_enabled: c.record_enabled,
100            segment_seconds: c.segment_seconds,
101            retention_hours: c.retention_hours,
102            enabled: c.enabled,
103            created_at: c.created_at,
104            updated_at: c.updated_at,
105        }
106    }
107}
108
109/// Payload to create a camera. `id` may be omitted (slug auto-derived from name).
110#[derive(Debug, Deserialize)]
111pub struct CameraCreate {
112    pub id: Option<String>,
113    pub name: String,
114    pub site_id: Option<String>,
115    #[serde(default = "default_vendor")]
116    pub vendor: String,
117    pub model: Option<String>,
118    pub address: Option<String>,
119    pub rtsp_port: Option<i64>,
120    pub username: Option<String>,
121    pub password: Option<String>,
122    pub main_stream_url: Option<String>,
123    pub sub_stream_url: Option<String>,
124    pub record_stream: Option<String>,
125    pub capabilities: Option<Value>,
126    pub record_enabled: Option<bool>,
127    pub segment_seconds: Option<i64>,
128    pub retention_hours: Option<i64>,
129    pub enabled: Option<bool>,
130}
131
132fn default_vendor() -> String {
133    "generic".to_string()
134}
135
136/// Partial update; only present fields are changed.
137#[derive(Debug, Deserialize, Default)]
138pub struct CameraUpdate {
139    pub name: Option<String>,
140    pub site_id: Option<String>,
141    pub vendor: Option<String>,
142    pub model: Option<String>,
143    pub address: Option<String>,
144    pub rtsp_port: Option<i64>,
145    pub username: Option<String>,
146    pub password: Option<String>,
147    pub main_stream_url: Option<String>,
148    pub sub_stream_url: Option<String>,
149    pub record_stream: Option<String>,
150    pub capabilities: Option<Value>,
151    pub record_enabled: Option<bool>,
152    pub segment_seconds: Option<i64>,
153    pub retention_hours: Option<i64>,
154    pub enabled: Option<bool>,
155}
156
157#[derive(Debug, Clone, Serialize, FromRow)]
158pub struct Segment {
159    pub id: String,
160    pub camera_id: String,
161    pub path: String,
162    pub start_time: DateTime<Utc>,
163    pub end_time: DateTime<Utc>,
164    pub duration_s: f64,
165    pub codec: Option<String>,
166    pub width: Option<i64>,
167    pub height: Option<i64>,
168    pub size_bytes: i64,
169    pub container: String,
170    pub locked: bool,
171    pub incident_id: Option<String>,
172    pub created_at: DateTime<Utc>,
173}
174
175#[derive(Debug, Clone, Serialize, FromRow)]
176pub struct CameraStatus {
177    pub camera_id: String,
178    pub state: String,
179    pub last_segment_at: Option<DateTime<Utc>>,
180    pub last_started_at: Option<DateTime<Utc>>,
181    pub reconnect_count: i64,
182    pub segments_written: i64,
183    pub fps_observed: Option<f64>,
184    pub bitrate_kbps: Option<f64>,
185    pub last_error: Option<String>,
186    pub recorder_pid: Option<i64>,
187    pub updated_at: DateTime<Utc>,
188}
189
190#[derive(Debug, Clone, Serialize, FromRow)]
191pub struct Event {
192    pub id: String,
193    pub camera_id: Option<String>,
194    pub site_id: Option<String>,
195    pub event_type: String,
196    pub severity: String,
197    pub timestamp: DateTime<Utc>,
198    pub payload: Json<Value>,
199    pub created_at: DateTime<Utc>,
200}
201
202// ---- Stage 2: AI frame sampling ----
203
204/// A perception task to run on a camera (consumed by AI workers).
205#[derive(Debug, Clone, Serialize, FromRow)]
206pub struct AiTask {
207    pub id: String,
208    pub camera_id: String,
209    pub task_type: String,
210    pub enabled: bool,
211    pub stream_profile: String,
212    pub fps: f64,
213    pub width: i64,
214    pub config: Json<Value>,
215    pub created_at: DateTime<Utc>,
216    pub updated_at: DateTime<Utc>,
217}
218
219#[derive(Debug, Deserialize)]
220pub struct AiTaskCreate {
221    pub task_type: String,
222    pub stream_profile: Option<String>,
223    pub fps: Option<f64>,
224    pub width: Option<i64>,
225    pub config: Option<Value>,
226    pub enabled: Option<bool>,
227}
228
229#[derive(Debug, Deserialize, Default)]
230pub struct AiTaskUpdate {
231    pub task_type: Option<String>,
232    pub stream_profile: Option<String>,
233    pub fps: Option<f64>,
234    pub width: Option<i64>,
235    pub config: Option<Value>,
236    pub enabled: Option<bool>,
237}
238
239/// A detection result posted by an AI worker.
240#[derive(Debug, Clone, Serialize, FromRow)]
241pub struct Detection {
242    pub id: String,
243    pub camera_id: String,
244    pub task_type: String,
245    pub timestamp: DateTime<Utc>,
246    pub label: Option<String>,
247    pub confidence: Option<f64>,
248    pub bbox: Option<Json<Value>>,
249    pub track_id: Option<String>,
250    pub attributes: Json<Value>,
251    /// Worker-supplied per-camera frame id this detection belongs to (idempotency / batch grouping).
252    pub frame_id: Option<String>,
253    pub created_at: DateTime<Utc>,
254}
255
256/// One detection inside an ingest request.
257#[derive(Debug, Deserialize)]
258pub struct DetectionIngest {
259    pub label: Option<String>,
260    pub confidence: Option<f64>,
261    pub bbox: Option<Value>,
262    pub track_id: Option<String>,
263    pub attributes: Option<Value>,
264}
265
266/// Optional event an AI worker can raise alongside its detections.
267#[derive(Debug, Deserialize)]
268pub struct IngestEvent {
269    pub event_type: String,
270    pub severity: Option<String>,
271    pub payload: Option<Value>,
272}
273
274/// Payload an AI worker POSTs to ingest detections (and optionally an event) for a camera.
275#[derive(Debug, Deserialize)]
276pub struct AiIngest {
277    pub camera_id: String,
278    pub task_type: String,
279    pub timestamp: Option<String>,
280    /// Optional per-camera monotonic frame id. When present, ingest is idempotent on
281    /// (camera_id, frame_id): a duplicate redelivery is a no-op (no double-insert, no re-fire of
282    /// consumer side effects). Omit it (e.g. the dependency-light client) to accept every batch.
283    pub frame_id: Option<String>,
284    #[serde(default)]
285    pub detections: Vec<DetectionIngest>,
286    pub event: Option<IngestEvent>,
287}
288
289// ---- Stage 3: zones + zone events ----
290
291/// A polygon region on a camera; tracked detections crossing it raise enter/exit/dwell events.
292#[derive(Debug, Clone, Serialize, FromRow)]
293pub struct Zone {
294    pub id: String,
295    pub camera_id: String,
296    pub name: String,
297    pub kind: String,
298    /// JSON array of [x, y] vertices, normalized 0..1.
299    pub polygon: Json<Value>,
300    pub dwell_seconds: f64,
301    /// JSON array of detection labels that count toward this zone (empty = all labels).
302    pub labels: Json<Value>,
303    pub severity: String,
304    pub config: Json<Value>,
305    pub enabled: bool,
306    pub created_at: DateTime<Utc>,
307    pub updated_at: DateTime<Utc>,
308}
309
310#[derive(Debug, Deserialize)]
311pub struct ZoneCreate {
312    pub name: String,
313    pub kind: Option<String>,
314    pub polygon: Value,
315    pub dwell_seconds: Option<f64>,
316    pub labels: Option<Value>,
317    pub severity: Option<String>,
318    pub config: Option<Value>,
319    pub enabled: Option<bool>,
320}
321
322#[derive(Debug, Deserialize, Default)]
323pub struct ZoneUpdate {
324    pub name: Option<String>,
325    pub kind: Option<String>,
326    pub polygon: Option<Value>,
327    pub dwell_seconds: Option<f64>,
328    pub labels: Option<Value>,
329    pub severity: Option<String>,
330    pub config: Option<Value>,
331    pub enabled: Option<bool>,
332}
333
334#[derive(Debug, Clone, Serialize, FromRow)]
335pub struct ZoneEvent {
336    pub id: String,
337    pub camera_id: String,
338    pub zone_id: String,
339    pub zone_name: String,
340    pub track_id: Option<String>,
341    pub event_type: String,
342    pub label: Option<String>,
343    pub timestamp: DateTime<Utc>,
344    pub dwell_seconds: Option<f64>,
345    pub evidence_path: Option<String>,
346    pub created_at: DateTime<Utc>,
347}
348
349// ---- Stage 4: Campus Entry — RBAC ----
350
351/// Operator account. `password_hash` is never serialized; use [`UserView`] for output.
352#[derive(Debug, Clone, FromRow)]
353pub struct User {
354    pub id: String,
355    pub username: String,
356    pub password_hash: String,
357    pub role: String,
358    pub display_name: Option<String>,
359    pub active: bool,
360    pub created_at: DateTime<Utc>,
361    pub updated_at: DateTime<Utc>,
362}
363
364#[derive(Debug, Clone, Serialize)]
365pub struct UserView {
366    pub id: String,
367    pub username: String,
368    pub role: String,
369    pub display_name: Option<String>,
370    pub active: bool,
371    pub created_at: DateTime<Utc>,
372    pub updated_at: DateTime<Utc>,
373}
374
375impl From<User> for UserView {
376    fn from(u: User) -> Self {
377        UserView {
378            id: u.id,
379            username: u.username,
380            role: u.role,
381            display_name: u.display_name,
382            active: u.active,
383            created_at: u.created_at,
384            updated_at: u.updated_at,
385        }
386    }
387}
388
389#[derive(Debug, Deserialize)]
390pub struct UserCreate {
391    pub username: String,
392    pub password: String,
393    pub role: Option<String>,
394    pub display_name: Option<String>,
395    pub active: Option<bool>,
396}
397
398#[derive(Debug, Deserialize, Default)]
399pub struct UserUpdate {
400    pub password: Option<String>,
401    pub role: Option<String>,
402    pub display_name: Option<String>,
403    pub active: Option<bool>,
404}
405
406#[derive(Debug, Deserialize)]
407pub struct LoginRequest {
408    pub username: String,
409    pub password: String,
410}
411
412#[derive(Debug, Clone, FromRow)]
413pub struct ApiKey {
414    pub id: String,
415    pub name: String,
416    /// Mapped from the row for completeness; never exposed (see [`ApiKeyView`]).
417    pub key_hash: String,
418    pub key_prefix: String,
419    pub role: String,
420    pub active: bool,
421    pub last_used_at: Option<DateTime<Utc>>,
422    pub created_at: DateTime<Utc>,
423}
424
425#[derive(Debug, Clone, Serialize)]
426pub struct ApiKeyView {
427    pub id: String,
428    pub name: String,
429    pub key_prefix: String,
430    pub role: String,
431    pub active: bool,
432    pub last_used_at: Option<DateTime<Utc>>,
433    pub created_at: DateTime<Utc>,
434}
435
436impl From<ApiKey> for ApiKeyView {
437    fn from(k: ApiKey) -> Self {
438        ApiKeyView {
439            id: k.id,
440            name: k.name,
441            key_prefix: k.key_prefix,
442            role: k.role,
443            active: k.active,
444            last_used_at: k.last_used_at,
445            created_at: k.created_at,
446        }
447    }
448}
449
450#[derive(Debug, Deserialize)]
451pub struct ApiKeyCreate {
452    pub name: String,
453    pub role: Option<String>,
454}