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