use axum::extract::{Query, State};
use axum::routing::get;
use axum::{Json, Router};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::auth::{self, Principal};
use crate::error::AppResult;
use crate::state::AppState;
pub fn router() -> Router<AppState> {
Router::new()
.route("/api/v1/outbox", get(list_outbox))
.route("/api/v1/site", get(site_info))
}
#[derive(Debug, Serialize, sqlx::FromRow)]
struct OutboxEntry {
seq: i64,
topic: String,
camera_id: Option<String>,
site_id: Option<String>,
frame_id: Option<String>,
task_type: Option<String>,
detection_count: i64,
created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize)]
struct OutboxPage {
entries: Vec<OutboxEntry>,
next_seq: Option<i64>,
count: usize,
}
#[derive(Debug, Deserialize)]
struct OutboxQuery {
since_seq: Option<i64>,
limit: Option<i64>,
}
async fn list_outbox(
State(st): State<AppState>,
principal: Principal,
Query(q): Query<OutboxQuery>,
) -> AppResult<Json<OutboxPage>> {
principal.require(principal.can_admin(), "read the fleet outbox")?;
let since = q.since_seq.unwrap_or(0).max(0);
let limit = q.limit.unwrap_or(100).clamp(1, 1000);
let entries = sqlx::query_as::<_, OutboxEntry>(
"SELECT seq, topic, camera_id, site_id, frame_id, task_type, detection_count, created_at
FROM outbox
WHERE seq > ?
ORDER BY seq ASC
LIMIT ?",
)
.bind(since)
.bind(limit)
.fetch_all(&st.pool)
.await?;
let next_seq = entries.last().map(|e| e.seq);
let count = entries.len();
auth::audit(
&st.pool,
&principal,
"read_outbox",
"outbox",
&format!("since:{since}"),
json!({ "since_seq": since, "limit": limit, "returned": count }),
)
.await;
Ok(Json(OutboxPage {
entries,
next_seq,
count,
}))
}
#[derive(Debug, Serialize)]
struct SiteInfo {
site_id: Option<String>,
name: &'static str,
version: &'static str,
started_at: DateTime<Utc>,
}
async fn site_info(State(st): State<AppState>) -> Json<SiteInfo> {
Json(SiteInfo {
site_id: st.cfg.site_id.clone(),
name: "Heldar Core",
version: env!("CARGO_PKG_VERSION"),
started_at: st.started_at,
})
}