heldar_kernel/routes/
outbox.rs1use axum::extract::{Query, State};
11use axum::routing::get;
12use axum::{Json, Router};
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use serde_json::json;
16
17use crate::auth::{self, Principal};
18use crate::error::AppResult;
19use crate::state::AppState;
20
21pub fn router() -> Router<AppState> {
22 Router::new()
23 .route("/api/v1/outbox", get(list_outbox))
24 .route("/api/v1/site", get(site_info))
25}
26
27#[derive(Debug, Serialize, sqlx::FromRow)]
29struct OutboxEntry {
30 seq: i64,
31 topic: String,
32 camera_id: Option<String>,
33 site_id: Option<String>,
34 frame_id: Option<String>,
35 task_type: Option<String>,
36 detection_count: i64,
37 created_at: DateTime<Utc>,
38}
39
40#[derive(Debug, Serialize)]
42struct OutboxPage {
43 entries: Vec<OutboxEntry>,
44 next_seq: Option<i64>,
46 count: usize,
47}
48
49#[derive(Debug, Deserialize)]
50struct OutboxQuery {
51 since_seq: Option<i64>,
53 limit: Option<i64>,
55}
56
57async fn list_outbox(
59 State(st): State<AppState>,
60 principal: Principal,
61 Query(q): Query<OutboxQuery>,
62) -> AppResult<Json<OutboxPage>> {
63 principal.require(principal.can_admin(), "read the fleet outbox")?;
64 let since = q.since_seq.unwrap_or(0).max(0);
65 let limit = q.limit.unwrap_or(100).clamp(1, 1000);
66 let entries = sqlx::query_as::<_, OutboxEntry>(
67 "SELECT seq, topic, camera_id, site_id, frame_id, task_type, detection_count, created_at
68 FROM outbox
69 WHERE seq > ?
70 ORDER BY seq ASC
71 LIMIT ?",
72 )
73 .bind(since)
74 .bind(limit)
75 .fetch_all(&st.pool)
76 .await?;
77
78 let next_seq = entries.last().map(|e| e.seq);
79 let count = entries.len();
80 auth::audit(
81 &st.pool,
82 &principal,
83 "read_outbox",
84 "outbox",
85 &format!("since:{since}"),
86 json!({ "since_seq": since, "limit": limit, "returned": count }),
87 )
88 .await;
89 Ok(Json(OutboxPage {
90 entries,
91 next_seq,
92 count,
93 }))
94}
95
96#[derive(Debug, Serialize)]
98struct SiteInfo {
99 site_id: Option<String>,
100 name: &'static str,
101 version: &'static str,
102 started_at: DateTime<Utc>,
103}
104
105async fn site_info(State(st): State<AppState>) -> Json<SiteInfo> {
106 Json(SiteInfo {
107 site_id: st.cfg.site_id.clone(),
108 name: "Heldar Core",
109 version: env!("CARGO_PKG_VERSION"),
110 started_at: st.started_at,
111 })
112}