Skip to main content

orch/
webhook_status.rs

1//! Webhook server health state, persisted to `~/.orch/webhook_status.json`.
2//!
3//! The engine writes this file during startup and each health-check cycle.
4//! The `orch webhook status` CLI reads it so operators can inspect webhook
5//! health without tailing logs.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11/// Persisted webhook health state.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct WebhookStatus {
14    /// Whether webhooks are enabled in config.
15    pub configured: bool,
16    /// Listening port (None when disabled or not yet bound).
17    pub port: Option<u16>,
18    /// Whether the last health-check ping succeeded.
19    pub healthy: bool,
20    /// True when the engine has fallen back to polling due to startup failure.
21    pub fallback_mode: bool,
22    /// UTC timestamp of the last health-check attempt.
23    pub last_check_utc: Option<DateTime<Utc>>,
24    /// Human-readable reason for the last failure (cleared on recovery).
25    pub last_failure_reason: Option<String>,
26    /// How many bind attempts were made at startup.
27    pub startup_attempts: u32,
28}
29
30impl Default for WebhookStatus {
31    fn default() -> Self {
32        Self {
33            configured: false,
34            port: None,
35            healthy: false,
36            fallback_mode: true,
37            last_check_utc: None,
38            last_failure_reason: None,
39            startup_attempts: 0,
40        }
41    }
42}
43
44/// Path to `~/.orch/webhook_status.json`.
45pub fn status_path() -> anyhow::Result<PathBuf> {
46    Ok(crate::home::orch_home()?.join("webhook_status.json"))
47}
48
49impl WebhookStatus {
50    /// Persist the status to disk. Non-fatal: logs a warning on write failure.
51    pub async fn save(&self) {
52        match status_path().and_then(|p| {
53            serde_json::to_string_pretty(self)
54                .map(|j| (p, j))
55                .map_err(anyhow::Error::from)
56        }) {
57            Ok((p, json)) => {
58                if let Err(e) = tokio::fs::write(&p, json).await {
59                    tracing::warn!(error = ?e, "failed to persist webhook status");
60                }
61            }
62            Err(e) => tracing::warn!(?e, "failed to persist webhook status"),
63        }
64    }
65
66    /// Load the last-known status from disk. Returns `None` if the file does
67    /// not exist or cannot be parsed.
68    pub fn load() -> Option<Self> {
69        let path = status_path().ok()?;
70        let json = std::fs::read_to_string(path).ok()?;
71        serde_json::from_str(&json).ok()
72    }
73}