heldar_kernel/config.rs
1use std::path::PathBuf;
2
3use crate::env::{parse_bool, parse_or, var, var_or};
4
5/// Runtime configuration, loaded from environment (see `.env.example`).
6#[derive(Clone, Debug)]
7pub struct Config {
8 pub database_url: String,
9 pub data_dir: PathBuf,
10 pub recordings_dir: PathBuf,
11 pub clips_dir: PathBuf,
12 pub snapshots_dir: PathBuf,
13 pub frames_dir: PathBuf,
14 /// Directory where segment-spanning HLS playback sessions are generated (one subdir per session).
15 pub playback_dir: PathBuf,
16 pub ffmpeg_bin: String,
17 pub ffprobe_bin: String,
18 pub mediamtx_api_url: String,
19 pub mediamtx_hls_base: String,
20 pub mediamtx_rtsp_base: String,
21 pub mediamtx_webrtc_base: String,
22 /// Max SQLite pool connections. Tunable per deployment: more absorbs bursts of concurrent
23 /// requests (WAL serves reads concurrently; writes still serialize), at the cost of memory.
24 pub db_max_connections: u32,
25 pub recorder_enabled: bool,
26 /// Optional second recordings root for dual/mirror recording. When set, cameras with
27 /// `mirror_enabled` get a SECOND ffmpeg pipeline writing byte-identical segments here (a redundant
28 /// DVR copy on a separate volume). Empty/unset disables mirror recording entirely.
29 pub mirror_recordings_dir: Option<PathBuf>,
30 /// Master switch for ANR (Automatic Network Replenishment) edge re-fill: re-fetch missed footage
31 /// from a camera's onboard storage to fill recording gaps. Cameras still need `anr_enabled`.
32 pub anr_enabled: bool,
33 /// How often the ANR loop scans for pending gaps to fill (seconds).
34 pub anr_interval_s: u64,
35 /// Ignore gaps older than this many hours (most cameras only retain recent onboard footage).
36 pub anr_max_gap_hours: i64,
37 /// Give up on a gap after this many fill attempts (marked `failed`).
38 pub anr_max_attempts: i64,
39 pub default_segment_seconds: i64,
40 pub default_retention_hours: i64,
41 /// Default per-camera storage quota (bytes) applied when a camera is created without an explicit
42 /// `storage_quota_bytes`. 0 means no default quota (the camera's quota is stored as NULL).
43 pub default_camera_quota_bytes: u64,
44 /// Default audio-recording toggle applied when a camera is created without an explicit
45 /// `record_audio`. When false (default) the recorder drops audio (video only).
46 pub default_record_audio: bool,
47 /// Default pre-roll seconds applied when a camera is created without an explicit
48 /// `pre_roll_seconds` (event / scheduled_event recording). Clamped to 0..300 in handlers.
49 pub default_pre_roll_seconds: i64,
50 /// Default post-roll seconds (the trigger recording window) applied when a camera is created
51 /// without an explicit `post_roll_seconds`. Clamped to 0..3600 in handlers.
52 pub default_post_roll_seconds: i64,
53 pub indexer_interval_s: u64,
54 pub health_interval_s: u64,
55 pub retention_interval_s: u64,
56 pub api_host: String,
57 pub api_port: u16,
58 pub cors_origins: Vec<String>,
59 /// Soft cap on total recording footprint; oldest unlocked segments are pruned above this.
60 pub max_recordings_bytes: u64,
61 /// Hard floor on free disk space; when free space drops below this, oldest unlocked segments
62 /// are pruned regardless of age/size policy (protects the host from a full disk).
63 pub min_free_disk_bytes: u64,
64 /// How often the alert notifier polls for new events to deliver.
65 pub notifier_interval_s: u64,
66 /// Master switch for AI frame sampling (Stage 2). Cameras still need an enabled AI task.
67 pub ai_enabled: bool,
68 /// Global frame-sampling budget (frames/sec summed across all cameras); per-camera fps is
69 /// reduced proportionally above this so adding AI cameras degrades fps instead of overloading.
70 pub ai_max_total_fps: f64,
71 pub default_ai_fps: f64,
72 pub default_ai_width: i64,
73 /// How long detection rows are kept before the retention sweeper prunes them.
74 pub detection_retention_hours: i64,
75 // ---- Scheduled interval snapshots ----
76 /// Master switch for the background snapshot scheduler (interval live-frame captures).
77 pub snapshot_scheduler_enabled: bool,
78 /// How often the scheduler ticks to look for due schedules (seconds).
79 pub snapshot_scheduler_interval_s: u64,
80 /// How long captured snapshots are kept before the retention sweeper prunes them. 0 = no pruning.
81 pub snapshot_retention_hours: i64,
82 // ---- Per-camera recording schedule (time-of-day windows) ----
83 /// How often the schedule watcher ticks to open/close recording windows for `scheduled` /
84 /// `scheduled_event` cameras (seconds). Windows are evaluated against the SERVER's LOCAL timezone.
85 pub schedule_check_interval_s: u64,
86 // ---- Segment-spanning HLS playback sessions (kernel platform feature) ----
87 /// How long a generated playback session (its HLS dir + the segment read-locks it holds) is
88 /// retained before the cleanup sweeper removes the dir and releases its locks. Server time.
89 pub playback_session_ttl_minutes: i64,
90 /// Maximum playback session span (seconds); a longer requested range is rejected (HTTP 400).
91 pub max_playback_seconds: f64,
92 // ---- Auth / RBAC (kernel platform feature) ----
93 /// Master switch for authentication + RBAC. When false, the API is open (dev/single-tenant
94 /// LAN appliance default) and a synthetic admin principal is used. When true, the auth/admin
95 /// surface requires a valid bearer token (session or API key) and enforces roles.
96 pub auth_enabled: bool,
97 /// Lifetime of an issued login session token.
98 pub session_ttl_hours: i64,
99 /// Add `Secure` to the session cookie (require HTTPS). Default false for HTTP LAN/overlay
100 /// appliances; set true when the deployment is served over TLS.
101 pub auth_cookie_secure: bool,
102 /// Optional first-run admin bootstrap (only used when no users exist yet).
103 pub bootstrap_admin_user: Option<String>,
104 pub bootstrap_admin_password: Option<String>,
105 /// How long kernel audit-log + generic-event rows are kept before retention prunes them.
106 pub audit_retention_days: i64,
107 // ---- Remote-access overlay (kernel platform feature; see docs/REMOTE-ACCESS.md) ----
108 /// Whether this deployment is reached through a WireGuard overlay (Tailscale / NetBird /
109 /// wireguard) running as an external daemon on the host. The kernel does not manage the
110 /// overlay; it only reports whether the configured interface is present + up so the dashboard
111 /// can surface remote-access health. When false, the deployment is LAN-only.
112 pub overlay_enabled: bool,
113 /// Label for the overlay in use: `tailscale` | `netbird` | `wireguard` | `none`.
114 pub overlay_kind: String,
115 /// The overlay's network interface to probe (e.g. `tailscale0`, `wt0`, `wg0`).
116 pub overlay_iface: Option<String>,
117 // ---- Backup subsystem (kernel platform feature) ----
118 /// Path to the `rclone` binary used for sftp/ftp/s3 remote backups. Local/NAS-mount backups use
119 /// std fs copy and never need it; remote backups degrade to a clear job error when it is missing.
120 pub rclone_bin: String,
121 /// Master switch for the background backup scheduler (scheduled policy jobs). On-demand archive
122 /// export still works when this is false.
123 pub backup_enabled: bool,
124 /// How often the backup scheduler ticks to look for due policies (seconds).
125 pub backup_scheduler_interval_s: u64,
126 /// Hard timeout for a single backup job's transfer (seconds); a job exceeding it is marked error.
127 pub backup_job_timeout_s: u64,
128 /// Maximum number of backup jobs running concurrently (a tokio Semaphore bounds the scheduler +
129 /// manual triggers).
130 pub backup_max_concurrent_jobs: usize,
131 /// Where on-demand archive (.zip) exports are written; also served at `/media/archives`.
132 pub archive_dir: PathBuf,
133 /// Maximum total source footprint (sum of segment sizes) for a single archive export; a larger
134 /// selection is rejected (HTTP 400).
135 pub archive_max_bytes: u64,
136 /// How long archive exports + finished backup-job rows are kept before retention prunes them.
137 pub archive_retention_hours: i64,
138 // ---- ONVIF (kernel platform feature; Profile S MVP) ----
139 /// How long the WS-Discovery probe listens for ProbeMatch replies (milliseconds).
140 pub onvif_discovery_timeout_ms: u64,
141 /// Per-request timeout for an ONVIF SOAP call (GetDeviceInformation, PTZ, etc.) in milliseconds.
142 pub onvif_request_timeout_ms: u64,
143 /// Per-request timeout for a HikVision ISAPI camera-config call (HTTP Digest) in milliseconds.
144 pub isapi_request_timeout_ms: u64,
145 // ---- Disk / array health (HA ops; see docs/HA.md) ----
146 /// Run periodic SMART self-assessment checks (`smartctl -H`) inside the health loop. Off by
147 /// default; needs `smartmontools` on PATH. Missing binary degrades to a one-time log + skip.
148 pub smart_check_enabled: bool,
149 /// Block devices to query when SMART checks are enabled (e.g. `/dev/sda,/dev/sdb`).
150 pub smart_devices: Vec<String>,
151 /// Watch `/proc/mdstat` (Linux md/RAID) and emit `raid_degraded` when an array shows a down member.
152 pub mdstat_check_enabled: bool,
153 /// Cadence of the disk-health (SMART/RAID) check inside the health loop (seconds).
154 pub smart_check_interval_s: u64,
155 // ---- Readiness HA probe (see docs/HA.md) ----
156 /// When > 0, `/readyz` also requires at least this percent of enabled cameras to be actively
157 /// recording (503 `insufficient_recorders` otherwise). 0 (default) keeps DB-connectivity-only.
158 pub readyz_min_recording_percent: f64,
159 // ---- Live preview transcode (HEVC->H.264) hardware acceleration ----
160 /// Encoder engine for the live preview transcode path: `software` (libx264, default), `vaapi`,
161 /// or `nvenc`. Unknown values warn and fall back to software.
162 pub live_transcode_engine: String,
163 /// VAAPI render node used when `live_transcode_engine = vaapi`.
164 pub vaapi_device: String,
165 // ---- Fleet / multi-site identity ----
166 /// Optional site identifier stamped onto outbox rows and surfaced at `GET /api/v1/site` for the
167 /// edge->cloud fleet uplink. Empty/unset = a single unnamed site.
168 pub site_id: Option<String>,
169 /// Control-plane base URL for edge-side self-registration (`HELDAR_CP_URL`). Unset (default) = this
170 /// node never phones home; the fleet is opt-in. When set together with `site_id` and
171 /// `public_base_url`, the node POSTs its identity to the control plane on boot + on a heartbeat, so
172 /// the control plane drains it without any static config or restart.
173 pub cp_url: Option<String>,
174 /// This node's externally reachable base URL, as the control plane should address it
175 /// (`HELDAR_PUBLIC_BASE_URL`, e.g. its overlay/WireGuard address). Required for self-registration —
176 /// the node cannot infer it (it binds `0.0.0.0`). Unset → self-registration parks.
177 pub public_base_url: Option<String>,
178 /// Bearer credential the control plane presents when draining this node's outbox
179 /// (`HELDAR_CP_TOKEN`). Empty (default) when this node runs with auth disabled (the LAN default);
180 /// when auth is enabled, set it to a valid API key the control plane may use.
181 pub cp_token: String,
182 /// Heartbeat cadence (seconds) for re-registration with the control plane
183 /// (`HELDAR_CP_REGISTER_INTERVAL_S`). Re-registration is idempotent, so the heartbeat also
184 /// re-teaches a control plane that restarted or lost its registry.
185 pub cp_register_interval_s: u64,
186 /// Optional mTLS material for talking to the control plane: this node's client cert + key (to
187 /// present when registering) and the CA that signed the control plane's server cert (to verify
188 /// it). Required as a set when the control plane enforces mTLS; unset = plain HTTP to the control
189 /// plane (the LAN/overlay default).
190 pub cp_tls: Option<CpTlsCfg>,
191 // ---- Plugin registry / store (Phase C) ----
192 /// Master switch for the plugin store's remote-registry fetching. When false, the store shows only
193 /// the bundled open catalog + locally installed plugins (fully offline). The bundled catalog is
194 /// always available regardless.
195 pub registry_enabled: bool,
196 /// Remote signed-catalog URLs to fetch (comma-separated). Default EMPTY — no phone-home; an
197 /// operator (or the proprietary build) sets the official Straits-AI registry here to populate the
198 /// proprietary/community shelves.
199 pub registry_urls: Vec<String>,
200 /// How often the background loop refreshes remote registries (seconds).
201 pub registry_refresh_s: u64,
202 /// Per-fetch timeout for a remote catalog (seconds).
203 pub registry_fetch_timeout_s: u64,
204 /// Operator-pinned extra trust anchors, `key_id:base64pubkey` comma-separated, added to the
205 /// compile-time pinned keys (for private registries).
206 pub registry_trusted_keys: Vec<(String, String)>,
207 /// When true, surface a remote registry's entries even if its signature does not verify (badged
208 /// unverified). Default false — fail closed.
209 pub registry_allow_unverified: bool,
210 /// When true, allow remote registry URLs that resolve to private/link-local addresses (default
211 /// false; SSRF guard for the admin-configured fetch).
212 pub registry_allow_private: bool,
213 // ---- Embedded dashboard (single-binary SPA serving) ----
214 /// Directory holding the built React dashboard (`apps/web/dist`), served as a static SPA
215 /// fallback so the whole product is one binary at one URL. Resolved from `HELDAR_WEB_DIR`; when
216 /// unset it falls back to `apps/web/dist` relative to the binary CWD. `None` when neither path
217 /// exists — the server then runs API-only (no dashboard).
218 pub web_dir: Option<PathBuf>,
219 // ---- Email / SMTP notifier (the off-by-default `smtp` feature) ----
220 /// SMTP relay host. Unset = email notifications disabled (the notifier parks).
221 pub smtp_host: Option<String>,
222 pub smtp_port: u16,
223 pub smtp_username: Option<String>,
224 pub smtp_password: Option<String>,
225 /// Envelope/From address (e.g. `heldar@site.example`). Required to send.
226 pub smtp_from: Option<String>,
227 /// `starttls` (587, default) | `implicit` (465) | `none`.
228 pub smtp_tls: String,
229 /// Recipient addresses that receive matching-event emails.
230 pub smtp_recipients: Vec<String>,
231 /// Severity floor for emailed events: `info` | `warning` (default) | `critical`.
232 pub smtp_min_severity: String,
233 /// How often the notifier polls for new events to email (seconds).
234 pub smtp_interval_s: u64,
235}
236
237/// mTLS material the edge presents to / uses to verify the control plane.
238#[derive(Clone, Debug)]
239pub struct CpTlsCfg {
240 /// PEM path: this node's client certificate (CN must equal `site_id`).
241 pub client_cert: PathBuf,
242 /// PEM path: the private key for the client certificate.
243 pub client_key: PathBuf,
244 /// PEM path: the CA that signed the control plane's server certificate.
245 pub server_ca: PathBuf,
246}
247
248/// Read the control-plane mTLS material from the environment. All-or-none: a partial set is a
249/// misconfiguration, so warn and disable mTLS (the heartbeat will then fail loudly against an https
250/// control plane, which is the visible signal to fix the config).
251fn cp_tls_from_env() -> Option<CpTlsCfg> {
252 match (
253 var("HELDAR_CP_TLS_CLIENT_CERT"),
254 var("HELDAR_CP_TLS_CLIENT_KEY"),
255 var("HELDAR_CP_TLS_CA"),
256 ) {
257 (None, None, None) => None,
258 (Some(client_cert), Some(client_key), Some(server_ca)) => Some(CpTlsCfg {
259 client_cert: client_cert.into(),
260 client_key: client_key.into(),
261 server_ca: server_ca.into(),
262 }),
263 _ => {
264 tracing::warn!(
265 "control-plane mTLS needs all of HELDAR_CP_TLS_CLIENT_CERT, HELDAR_CP_TLS_CLIENT_KEY, HELDAR_CP_TLS_CA; ignoring partial config"
266 );
267 None
268 }
269 }
270}
271
272impl Config {
273 pub fn from_env() -> Self {
274 let data_dir = PathBuf::from(var_or("HELDAR_DATA_DIR", "./data"));
275 let recordings_dir = var("HELDAR_RECORDINGS_DIR")
276 .map(PathBuf::from)
277 .unwrap_or_else(|| data_dir.join("recordings"));
278 let clips_dir = var("HELDAR_CLIPS_DIR")
279 .map(PathBuf::from)
280 .unwrap_or_else(|| data_dir.join("clips"));
281 let snapshots_dir = var("HELDAR_SNAPSHOTS_DIR")
282 .map(PathBuf::from)
283 .unwrap_or_else(|| data_dir.join("snapshots"));
284 let frames_dir = var("HELDAR_FRAMES_DIR")
285 .map(PathBuf::from)
286 .unwrap_or_else(|| data_dir.join("frames"));
287 let playback_dir = var("HELDAR_PLAYBACK_DIR")
288 .map(PathBuf::from)
289 .unwrap_or_else(|| data_dir.join("playback"));
290 let archive_dir = var("HELDAR_ARCHIVE_DIR")
291 .map(PathBuf::from)
292 .unwrap_or_else(|| data_dir.join("archives"));
293
294 let cors_origins = var_or("HELDAR_CORS_ORIGINS", "http://localhost:5173")
295 .split(',')
296 .map(|s| s.trim().to_string())
297 .filter(|s| !s.is_empty())
298 .collect();
299
300 // Embedded dashboard: explicit HELDAR_WEB_DIR wins; otherwise try `apps/web/dist` relative
301 // to the binary CWD. Only `Some` when the directory actually exists (else API-only).
302 let web_dir = var("HELDAR_WEB_DIR")
303 .map(PathBuf::from)
304 .unwrap_or_else(|| PathBuf::from("apps/web/dist"));
305 let web_dir = if web_dir.is_dir() {
306 Some(web_dir)
307 } else {
308 None
309 };
310
311 let max_recordings_gb: f64 = parse_or("HELDAR_MAX_RECORDINGS_GB", 20.0);
312 let min_free_disk_gb: f64 = parse_or("HELDAR_MIN_FREE_DISK_GB", 5.0);
313 let default_camera_quota_gb: f64 = parse_or("HELDAR_DEFAULT_CAMERA_QUOTA_GB", 0.0);
314
315 Config {
316 database_url: var_or("HELDAR_DATABASE_URL", "sqlite://./data/heldar.db"),
317 data_dir,
318 recordings_dir,
319 clips_dir,
320 snapshots_dir,
321 frames_dir,
322 playback_dir,
323 ffmpeg_bin: var_or("HELDAR_FFMPEG_BIN", "ffmpeg"),
324 ffprobe_bin: var_or("HELDAR_FFPROBE_BIN", "ffprobe"),
325 mediamtx_api_url: var_or("HELDAR_MEDIAMTX_API_URL", "http://127.0.0.1:9997"),
326 mediamtx_hls_base: var_or("HELDAR_MEDIAMTX_HLS_BASE", "http://127.0.0.1:8888"),
327 mediamtx_rtsp_base: var_or("HELDAR_MEDIAMTX_RTSP_BASE", "rtsp://127.0.0.1:8554"),
328 mediamtx_webrtc_base: var_or("HELDAR_MEDIAMTX_WEBRTC_BASE", "http://127.0.0.1:8889"),
329 db_max_connections: parse_or::<u32>("HELDAR_DB_MAX_CONNECTIONS", 16).clamp(2, 256),
330 recorder_enabled: parse_bool("HELDAR_RECORDER_ENABLED", true),
331 mirror_recordings_dir: var("HELDAR_MIRROR_RECORDINGS_DIR").map(PathBuf::from),
332 anr_enabled: parse_bool("HELDAR_ANR_ENABLED", false),
333 anr_interval_s: parse_or("HELDAR_ANR_INTERVAL_S", 300),
334 anr_max_gap_hours: parse_or("HELDAR_ANR_MAX_GAP_HOURS", 24),
335 anr_max_attempts: parse_or("HELDAR_ANR_MAX_ATTEMPTS", 3),
336 default_segment_seconds: parse_or("HELDAR_DEFAULT_SEGMENT_SECONDS", 60),
337 default_retention_hours: parse_or("HELDAR_DEFAULT_RETENTION_HOURS", 24),
338 default_camera_quota_bytes: (default_camera_quota_gb * 1024.0 * 1024.0 * 1024.0) as u64,
339 default_record_audio: parse_bool("HELDAR_DEFAULT_RECORD_AUDIO", false),
340 default_pre_roll_seconds: parse_or("HELDAR_DEFAULT_PRE_ROLL_SECONDS", 10),
341 default_post_roll_seconds: parse_or("HELDAR_DEFAULT_POST_ROLL_SECONDS", 30),
342 indexer_interval_s: parse_or("HELDAR_INDEXER_INTERVAL_S", 10),
343 health_interval_s: parse_or("HELDAR_HEALTH_INTERVAL_S", 15),
344 retention_interval_s: parse_or("HELDAR_RETENTION_INTERVAL_S", 300),
345 api_host: var_or("HELDAR_API_HOST", "0.0.0.0"),
346 api_port: parse_or("HELDAR_API_PORT", 8000),
347 cors_origins,
348 max_recordings_bytes: (max_recordings_gb * 1024.0 * 1024.0 * 1024.0) as u64,
349 min_free_disk_bytes: (min_free_disk_gb * 1024.0 * 1024.0 * 1024.0) as u64,
350 notifier_interval_s: parse_or("HELDAR_NOTIFIER_INTERVAL_S", 15),
351 ai_enabled: parse_bool("HELDAR_AI_ENABLED", true),
352 ai_max_total_fps: parse_or("HELDAR_AI_MAX_TOTAL_FPS", 40.0),
353 default_ai_fps: parse_or("HELDAR_DEFAULT_AI_FPS", 5.0),
354 default_ai_width: parse_or("HELDAR_DEFAULT_AI_WIDTH", 1280),
355 detection_retention_hours: parse_or("HELDAR_DETECTION_RETENTION_HOURS", 168),
356 snapshot_scheduler_enabled: parse_bool("HELDAR_SNAPSHOT_SCHEDULER_ENABLED", true),
357 snapshot_scheduler_interval_s: parse_or("HELDAR_SNAPSHOT_SCHEDULER_INTERVAL_S", 60),
358 snapshot_retention_hours: parse_or("HELDAR_SNAPSHOT_RETENTION_HOURS", 168),
359 schedule_check_interval_s: parse_or("HELDAR_SCHEDULE_CHECK_INTERVAL_S", 30),
360 playback_session_ttl_minutes: parse_or("HELDAR_PLAYBACK_SESSION_TTL_MINUTES", 60),
361 max_playback_seconds: parse_or("HELDAR_MAX_PLAYBACK_SECONDS", 7200.0),
362 auth_enabled: parse_bool("HELDAR_AUTH_ENABLED", false),
363 session_ttl_hours: parse_or("HELDAR_SESSION_TTL_HOURS", 12),
364 auth_cookie_secure: parse_bool("HELDAR_AUTH_COOKIE_SECURE", false),
365 bootstrap_admin_user: var("HELDAR_BOOTSTRAP_ADMIN_USER"),
366 bootstrap_admin_password: var("HELDAR_BOOTSTRAP_ADMIN_PASSWORD"),
367 audit_retention_days: parse_or("HELDAR_AUDIT_RETENTION_DAYS", 365),
368 overlay_enabled: parse_bool("HELDAR_OVERLAY_ENABLED", false),
369 overlay_kind: var_or("HELDAR_OVERLAY_KIND", "none"),
370 overlay_iface: var("HELDAR_OVERLAY_IFACE"),
371 rclone_bin: var_or("HELDAR_RCLONE_BIN", "rclone"),
372 backup_enabled: parse_bool("HELDAR_BACKUP_ENABLED", true),
373 backup_scheduler_interval_s: parse_or("HELDAR_BACKUP_SCHEDULER_INTERVAL_S", 60),
374 backup_job_timeout_s: parse_or("HELDAR_BACKUP_JOB_TIMEOUT_S", 3600),
375 backup_max_concurrent_jobs: parse_or::<usize>("HELDAR_BACKUP_MAX_CONCURRENT_JOBS", 2)
376 .max(1),
377 archive_dir,
378 archive_max_bytes: parse_or("HELDAR_ARCHIVE_MAX_BYTES", 10_737_418_240u64),
379 archive_retention_hours: parse_or("HELDAR_ARCHIVE_RETENTION_HOURS", 48),
380 onvif_discovery_timeout_ms: parse_or("HELDAR_ONVIF_DISCOVERY_TIMEOUT_MS", 2000),
381 onvif_request_timeout_ms: parse_or("HELDAR_ONVIF_REQUEST_TIMEOUT_MS", 5000),
382 isapi_request_timeout_ms: parse_or("HELDAR_ISAPI_REQUEST_TIMEOUT_MS", 8000),
383 smart_check_enabled: parse_bool("HELDAR_SMART_CHECK_ENABLED", false),
384 smart_devices: var("HELDAR_SMART_DEVICES")
385 .map(|v| {
386 v.split(',')
387 .map(|s| s.trim().to_string())
388 .filter(|s| !s.is_empty())
389 .collect()
390 })
391 .unwrap_or_default(),
392 mdstat_check_enabled: parse_bool("HELDAR_MDSTAT_CHECK_ENABLED", false),
393 smart_check_interval_s: parse_or("HELDAR_SMART_CHECK_INTERVAL_S", 300),
394 readyz_min_recording_percent: parse_or("HELDAR_READYZ_MIN_RECORDING_PERCENT", 0.0),
395 live_transcode_engine: var_or("HELDAR_LIVE_TRANSCODE_ENGINE", "software"),
396 vaapi_device: var_or("HELDAR_VAAPI_DEVICE", "/dev/dri/renderD128"),
397 site_id: var("HELDAR_SITE_ID"),
398 cp_url: var("HELDAR_CP_URL"),
399 public_base_url: var("HELDAR_PUBLIC_BASE_URL"),
400 cp_token: var_or("HELDAR_CP_TOKEN", ""),
401 cp_register_interval_s: parse_or("HELDAR_CP_REGISTER_INTERVAL_S", 300),
402 cp_tls: cp_tls_from_env(),
403 registry_enabled: parse_bool("HELDAR_REGISTRY_ENABLED", true),
404 registry_urls: var_or("HELDAR_REGISTRY_URLS", "")
405 .split(',')
406 .map(|s| s.trim().to_string())
407 .filter(|s| !s.is_empty())
408 .collect(),
409 registry_refresh_s: parse_or("HELDAR_REGISTRY_REFRESH_S", 900),
410 registry_fetch_timeout_s: parse_or("HELDAR_REGISTRY_FETCH_TIMEOUT_S", 10),
411 registry_trusted_keys: var_or("HELDAR_REGISTRY_TRUSTED_KEYS", "")
412 .split(',')
413 .filter_map(|s| {
414 let s = s.trim();
415 s.split_once(':')
416 .map(|(id, key)| (id.trim().to_string(), key.trim().to_string()))
417 .filter(|(id, key)| !id.is_empty() && !key.is_empty())
418 })
419 .collect(),
420 registry_allow_unverified: parse_bool("HELDAR_REGISTRY_ALLOW_UNVERIFIED", false),
421 registry_allow_private: parse_bool("HELDAR_REGISTRY_ALLOW_PRIVATE", false),
422 web_dir,
423 smtp_host: var("HELDAR_SMTP_HOST"),
424 smtp_port: parse_or("HELDAR_SMTP_PORT", 587u16),
425 smtp_username: var("HELDAR_SMTP_USERNAME"),
426 smtp_password: var("HELDAR_SMTP_PASSWORD"),
427 smtp_from: var("HELDAR_SMTP_FROM"),
428 smtp_tls: var_or("HELDAR_SMTP_TLS", "starttls"),
429 smtp_recipients: var_or("HELDAR_SMTP_RECIPIENTS", "")
430 .split(',')
431 .map(|s| s.trim().to_string())
432 .filter(|s| !s.is_empty())
433 .collect(),
434 smtp_min_severity: var_or("HELDAR_SMTP_MIN_SEVERITY", "warning"),
435 smtp_interval_s: parse_or("HELDAR_SMTP_INTERVAL_S", 30),
436 }
437 }
438
439 /// Directory where a camera's segments are stored.
440 pub fn camera_recordings_dir(&self, camera_id: &str) -> PathBuf {
441 self.recordings_dir.join(camera_id)
442 }
443
444 /// Directory where a camera's sampled AI frames are written.
445 pub fn camera_frames_dir(&self, camera_id: &str) -> PathBuf {
446 self.frames_dir.join(camera_id)
447 }
448}