Skip to main content

heldar_kernel/routes/
system.rs

1use axum::extract::State;
2use axum::http::StatusCode;
3use axum::response::{IntoResponse, Response};
4use axum::routing::get;
5use axum::{Json, Router};
6use chrono::{DateTime, Utc};
7use serde::Serialize;
8use serde_json::{json, Value};
9
10use crate::error::AppResult;
11use crate::services::remote_access::{self, OverlayStatus};
12use crate::services::storage::{self, StorageReport};
13use crate::state::AppState;
14
15pub fn router() -> Router<AppState> {
16    Router::new()
17        .route("/healthz", get(healthz))
18        .route("/readyz", get(readyz))
19        .route("/api/v1/system", get(system_info))
20}
21
22/// Liveness: the process is up.
23async fn healthz() -> Json<Value> {
24    Json(json!({ "status": "ok" }))
25}
26
27/// Readiness: the database is reachable (returns 503 otherwise).
28async fn readyz(State(st): State<AppState>) -> Response {
29    match sqlx::query_scalar::<_, i64>("SELECT 1")
30        .fetch_one(&st.pool)
31        .await
32    {
33        Ok(_) => (StatusCode::OK, Json(json!({ "ready": true }))).into_response(),
34        Err(e) => {
35            tracing::error!(error = %e, "readyz: database not reachable");
36            (
37                StatusCode::SERVICE_UNAVAILABLE,
38                Json(json!({ "ready": false, "reason": "database" })),
39            )
40                .into_response()
41        }
42    }
43}
44
45#[derive(Debug, Serialize)]
46struct SystemInfo {
47    name: &'static str,
48    version: &'static str,
49    started_at: DateTime<Utc>,
50    uptime_seconds: i64,
51    recorder_enabled: bool,
52    cameras_total: i64,
53    cameras_recording: i64,
54    active_recorders: usize,
55    segments_total: i64,
56    recordings_bytes: i64,
57    recordings_gb: f64,
58    max_recordings_gb: f64,
59    storage: StorageReport,
60    remote_access: OverlayStatus,
61}
62
63async fn system_info(State(st): State<AppState>) -> AppResult<Json<SystemInfo>> {
64    let cameras_total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM cameras")
65        .fetch_one(&st.pool)
66        .await?;
67    let cameras_recording: i64 =
68        sqlx::query_scalar("SELECT COUNT(*) FROM camera_status WHERE state = 'recording'")
69            .fetch_one(&st.pool)
70            .await?;
71    let segments_total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM segments")
72        .fetch_one(&st.pool)
73        .await?;
74    let recordings_bytes: i64 =
75        sqlx::query_scalar("SELECT COALESCE(SUM(size_bytes), 0) FROM segments")
76            .fetch_one(&st.pool)
77            .await?;
78    let active_recorders = st.recorder.active_ids().await.len();
79    let storage = storage::storage_report(&st.pool, &st.cfg).await?;
80
81    Ok(Json(SystemInfo {
82        name: "Heldar Core",
83        version: env!("CARGO_PKG_VERSION"),
84        started_at: st.started_at,
85        uptime_seconds: (Utc::now() - st.started_at).num_seconds(),
86        recorder_enabled: st.cfg.recorder_enabled,
87        cameras_total,
88        cameras_recording,
89        active_recorders,
90        segments_total,
91        recordings_bytes,
92        recordings_gb: recordings_bytes as f64 / 1024.0 / 1024.0 / 1024.0,
93        max_recordings_gb: st.cfg.max_recordings_bytes as f64 / 1024.0 / 1024.0 / 1024.0,
94        storage,
95        remote_access: remote_access::status(&st.cfg),
96    }))
97}