heldar_kernel/routes/
anr.rs1use axum::extract::{Path, Query, State};
12use axum::routing::{get, post};
13use axum::{Json, Router};
14use serde::Deserialize;
15use serde_json::json;
16
17use crate::auth::{self, Principal};
18use crate::error::{AppError, AppResult};
19use crate::models::RecordingGap;
20use crate::routes::cameras::load_camera;
21use crate::state::AppState;
22
23pub fn router() -> Router<AppState> {
24 Router::new()
25 .route("/api/v1/cameras/{id}/recording-gaps", get(list_gaps))
26 .route(
27 "/api/v1/cameras/{id}/recording-gaps/{gap_id}/retry",
28 post(retry_gap),
29 )
30}
31
32#[derive(Debug, Deserialize)]
33struct GapQuery {
34 state: Option<String>,
36 limit: Option<i64>,
37}
38
39async fn list_gaps(
41 State(st): State<AppState>,
42 Path(id): Path<String>,
43 principal: Principal,
44 Query(q): Query<GapQuery>,
45) -> AppResult<Json<Vec<RecordingGap>>> {
46 principal.require(principal.can_view(), "view recording gaps")?;
47 let _ = load_camera(&st.pool, &id).await?;
48 let limit = q.limit.unwrap_or(500).clamp(1, 5000);
49 let rows =
50 match q.state.as_deref() {
51 Some(state) => {
52 sqlx::query_as::<_, RecordingGap>(
53 "SELECT * FROM recording_gaps WHERE camera_id = ? AND fill_state = ?
54 ORDER BY gap_start DESC LIMIT ?",
55 )
56 .bind(&id)
57 .bind(state)
58 .bind(limit)
59 .fetch_all(&st.pool)
60 .await?
61 }
62 None => sqlx::query_as::<_, RecordingGap>(
63 "SELECT * FROM recording_gaps WHERE camera_id = ? ORDER BY gap_start DESC LIMIT ?",
64 )
65 .bind(&id)
66 .bind(limit)
67 .fetch_all(&st.pool)
68 .await?,
69 };
70 Ok(Json(rows))
71}
72
73async fn retry_gap(
75 State(st): State<AppState>,
76 Path((id, gap_id)): Path<(String, String)>,
77 principal: Principal,
78) -> AppResult<Json<RecordingGap>> {
79 principal.require(principal.can_manage_registry(), "retry recording-gap fill")?;
80 let _ = load_camera(&st.pool, &id).await?;
81 let res = sqlx::query(
82 "UPDATE recording_gaps
83 SET fill_state = 'pending', fill_attempts = 0, last_attempt_at = NULL, filled_at = NULL
84 WHERE id = ? AND camera_id = ?",
85 )
86 .bind(&gap_id)
87 .bind(&id)
88 .execute(&st.pool)
89 .await?;
90 if res.rows_affected() == 0 {
91 return Err(AppError::NotFound(format!(
92 "recording gap {gap_id} not found"
93 )));
94 }
95 auth::audit(
96 &st.pool,
97 &principal,
98 "anr_retry_gap",
99 "recording_gap",
100 &gap_id,
101 json!({ "camera_id": id }),
102 )
103 .await;
104 let gap = sqlx::query_as::<_, RecordingGap>("SELECT * FROM recording_gaps WHERE id = ?")
105 .bind(&gap_id)
106 .fetch_one(&st.pool)
107 .await?;
108 Ok(Json(gap))
109}