Skip to main content

heldar_kernel/
config.rs

1use std::env;
2use std::path::PathBuf;
3
4/// Runtime configuration, loaded from environment (see `.env.example`).
5#[derive(Clone, Debug)]
6pub struct Config {
7    pub database_url: String,
8    pub data_dir: PathBuf,
9    pub recordings_dir: PathBuf,
10    pub clips_dir: PathBuf,
11    pub snapshots_dir: PathBuf,
12    pub frames_dir: PathBuf,
13    pub ffmpeg_bin: String,
14    pub ffprobe_bin: String,
15    pub mediamtx_api_url: String,
16    pub mediamtx_hls_base: String,
17    pub mediamtx_rtsp_base: String,
18    pub mediamtx_webrtc_base: String,
19    /// Max SQLite pool connections. Tunable per deployment: more absorbs bursts of concurrent
20    /// requests (WAL serves reads concurrently; writes still serialize), at the cost of memory.
21    pub db_max_connections: u32,
22    pub recorder_enabled: bool,
23    pub default_segment_seconds: i64,
24    pub default_retention_hours: i64,
25    pub indexer_interval_s: u64,
26    pub health_interval_s: u64,
27    pub retention_interval_s: u64,
28    pub api_host: String,
29    pub api_port: u16,
30    pub cors_origins: Vec<String>,
31    /// Soft cap on total recording footprint; oldest unlocked segments are pruned above this.
32    pub max_recordings_bytes: u64,
33    /// Hard floor on free disk space; when free space drops below this, oldest unlocked segments
34    /// are pruned regardless of age/size policy (protects the host from a full disk).
35    pub min_free_disk_bytes: u64,
36    /// Optional webhook URL that receives warning/critical events as JSON (alerting).
37    pub alert_webhook_url: Option<String>,
38    /// How often the alert notifier polls for new events to deliver.
39    pub notifier_interval_s: u64,
40    /// Master switch for AI frame sampling (Stage 2). Cameras still need an enabled AI task.
41    pub ai_enabled: bool,
42    /// Global frame-sampling budget (frames/sec summed across all cameras); per-camera fps is
43    /// reduced proportionally above this so adding AI cameras degrades fps instead of overloading.
44    pub ai_max_total_fps: f64,
45    pub default_ai_fps: f64,
46    pub default_ai_width: i64,
47    /// How long detection rows are kept before the retention sweeper prunes them.
48    pub detection_retention_hours: i64,
49    // ---- Auth / RBAC (kernel platform feature) ----
50    /// Master switch for authentication + RBAC. When false, the API is open (dev/single-tenant
51    /// LAN appliance default) and a synthetic admin principal is used. When true, the auth/admin
52    /// surface requires a valid bearer token (session or API key) and enforces roles.
53    pub auth_enabled: bool,
54    /// Lifetime of an issued login session token.
55    pub session_ttl_hours: i64,
56    /// Add `Secure` to the session cookie (require HTTPS). Default false for HTTP LAN/overlay
57    /// appliances; set true when the deployment is served over TLS.
58    pub auth_cookie_secure: bool,
59    /// Optional first-run admin bootstrap (only used when no users exist yet).
60    pub bootstrap_admin_user: Option<String>,
61    pub bootstrap_admin_password: Option<String>,
62    /// How long kernel audit-log + generic-event rows are kept before retention prunes them.
63    pub audit_retention_days: i64,
64    // ---- Remote-access overlay (kernel platform feature; see docs/REMOTE-ACCESS.md) ----
65    /// Whether this deployment is reached through a WireGuard overlay (Tailscale / NetBird /
66    /// wireguard) running as an external daemon on the host. The kernel does not manage the
67    /// overlay; it only reports whether the configured interface is present + up so the dashboard
68    /// can surface remote-access health. When false, the deployment is LAN-only.
69    pub overlay_enabled: bool,
70    /// Label for the overlay in use: `tailscale` | `netbird` | `wireguard` | `none`.
71    pub overlay_kind: String,
72    /// The overlay's network interface to probe (e.g. `tailscale0`, `wt0`, `wg0`).
73    pub overlay_iface: Option<String>,
74}
75
76fn var(key: &str) -> Option<String> {
77    env::var(key).ok().filter(|s| !s.trim().is_empty())
78}
79
80fn var_or(key: &str, default: &str) -> String {
81    var(key).unwrap_or_else(|| default.to_string())
82}
83
84fn parse_or<T: std::str::FromStr>(key: &str, default: T) -> T {
85    var(key).and_then(|v| v.parse().ok()).unwrap_or(default)
86}
87
88fn parse_bool(key: &str, default: bool) -> bool {
89    match var(key) {
90        Some(v) => matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"),
91        None => default,
92    }
93}
94
95impl Config {
96    pub fn from_env() -> Self {
97        let data_dir = PathBuf::from(var_or("HELDAR_DATA_DIR", "./data"));
98        let recordings_dir = var("HELDAR_RECORDINGS_DIR")
99            .map(PathBuf::from)
100            .unwrap_or_else(|| data_dir.join("recordings"));
101        let clips_dir = var("HELDAR_CLIPS_DIR")
102            .map(PathBuf::from)
103            .unwrap_or_else(|| data_dir.join("clips"));
104        let snapshots_dir = var("HELDAR_SNAPSHOTS_DIR")
105            .map(PathBuf::from)
106            .unwrap_or_else(|| data_dir.join("snapshots"));
107        let frames_dir = var("HELDAR_FRAMES_DIR")
108            .map(PathBuf::from)
109            .unwrap_or_else(|| data_dir.join("frames"));
110
111        let cors_origins = var_or("HELDAR_CORS_ORIGINS", "http://localhost:5173")
112            .split(',')
113            .map(|s| s.trim().to_string())
114            .filter(|s| !s.is_empty())
115            .collect();
116
117        let max_recordings_gb: f64 = parse_or("HELDAR_MAX_RECORDINGS_GB", 20.0);
118        let min_free_disk_gb: f64 = parse_or("HELDAR_MIN_FREE_DISK_GB", 5.0);
119
120        Config {
121            database_url: var_or("HELDAR_DATABASE_URL", "sqlite://./data/heldar.db"),
122            data_dir,
123            recordings_dir,
124            clips_dir,
125            snapshots_dir,
126            frames_dir,
127            ffmpeg_bin: var_or("HELDAR_FFMPEG_BIN", "ffmpeg"),
128            ffprobe_bin: var_or("HELDAR_FFPROBE_BIN", "ffprobe"),
129            mediamtx_api_url: var_or("HELDAR_MEDIAMTX_API_URL", "http://127.0.0.1:9997"),
130            mediamtx_hls_base: var_or("HELDAR_MEDIAMTX_HLS_BASE", "http://127.0.0.1:8888"),
131            mediamtx_rtsp_base: var_or("HELDAR_MEDIAMTX_RTSP_BASE", "rtsp://127.0.0.1:8554"),
132            mediamtx_webrtc_base: var_or("HELDAR_MEDIAMTX_WEBRTC_BASE", "http://127.0.0.1:8889"),
133            db_max_connections: parse_or::<u32>("HELDAR_DB_MAX_CONNECTIONS", 16).clamp(2, 256),
134            recorder_enabled: parse_bool("HELDAR_RECORDER_ENABLED", true),
135            default_segment_seconds: parse_or("HELDAR_DEFAULT_SEGMENT_SECONDS", 60),
136            default_retention_hours: parse_or("HELDAR_DEFAULT_RETENTION_HOURS", 24),
137            indexer_interval_s: parse_or("HELDAR_INDEXER_INTERVAL_S", 10),
138            health_interval_s: parse_or("HELDAR_HEALTH_INTERVAL_S", 15),
139            retention_interval_s: parse_or("HELDAR_RETENTION_INTERVAL_S", 300),
140            api_host: var_or("HELDAR_API_HOST", "0.0.0.0"),
141            api_port: parse_or("HELDAR_API_PORT", 8000),
142            cors_origins,
143            max_recordings_bytes: (max_recordings_gb * 1024.0 * 1024.0 * 1024.0) as u64,
144            min_free_disk_bytes: (min_free_disk_gb * 1024.0 * 1024.0 * 1024.0) as u64,
145            alert_webhook_url: var("HELDAR_ALERT_WEBHOOK_URL"),
146            notifier_interval_s: parse_or("HELDAR_NOTIFIER_INTERVAL_S", 15),
147            ai_enabled: parse_bool("HELDAR_AI_ENABLED", true),
148            ai_max_total_fps: parse_or("HELDAR_AI_MAX_TOTAL_FPS", 40.0),
149            default_ai_fps: parse_or("HELDAR_DEFAULT_AI_FPS", 5.0),
150            default_ai_width: parse_or("HELDAR_DEFAULT_AI_WIDTH", 1280),
151            detection_retention_hours: parse_or("HELDAR_DETECTION_RETENTION_HOURS", 168),
152            auth_enabled: parse_bool("HELDAR_AUTH_ENABLED", false),
153            session_ttl_hours: parse_or("HELDAR_SESSION_TTL_HOURS", 12),
154            auth_cookie_secure: parse_bool("HELDAR_AUTH_COOKIE_SECURE", false),
155            bootstrap_admin_user: var("HELDAR_BOOTSTRAP_ADMIN_USER"),
156            bootstrap_admin_password: var("HELDAR_BOOTSTRAP_ADMIN_PASSWORD"),
157            audit_retention_days: parse_or("HELDAR_AUDIT_RETENTION_DAYS", 365),
158            overlay_enabled: parse_bool("HELDAR_OVERLAY_ENABLED", false),
159            overlay_kind: var_or("HELDAR_OVERLAY_KIND", "none"),
160            overlay_iface: var("HELDAR_OVERLAY_IFACE"),
161        }
162    }
163
164    /// Directory where a camera's segments are stored.
165    pub fn camera_recordings_dir(&self, camera_id: &str) -> PathBuf {
166        self.recordings_dir.join(camera_id)
167    }
168
169    /// Directory where a camera's sampled AI frames are written.
170    pub fn camera_frames_dir(&self, camera_id: &str) -> PathBuf {
171        self.frames_dir.join(camera_id)
172    }
173}