Skip to main content

mcpr_core/proxy/
health.rs

1//! Per-proxy connection health and display state.
2//!
3//! Tracks what one proxy instance is currently doing — MCP upstream health,
4//! tunnel connection status, widget discovery, cloud sync, request counters.
5//! Updated by background tasks (health checks, request handlers, tunnel
6//! callbacks) and read by the admin API, TUI, and status commands.
7//!
8//! Lives behind an `Arc<Mutex<_>>` because updates come from many callers and
9//! readers pull snapshots; contention is negligible (updates are infrequent).
10
11use std::sync::{Arc, Mutex};
12use std::time::Instant;
13
14/// Connection status for an upstream service (MCP, tunnel, widgets).
15#[derive(Clone, Copy, PartialEq)]
16pub enum ConnectionStatus {
17    /// Status not yet determined (initial state).
18    Unknown,
19    /// Service is unreachable or down.
20    Disconnected,
21    /// Connection attempt in progress (tunnel only).
22    Connecting,
23    /// Service is healthy and responding.
24    Connected,
25    /// Tunnel was forcibly disconnected by the relay (e.g., subdomain conflict).
26    Evicted,
27    /// Server is reachable but does not speak MCP protocol.
28    NotMcp,
29}
30
31impl ConnectionStatus {
32    pub fn label(&self) -> &'static str {
33        match self {
34            Self::Unknown => "Unknown",
35            Self::Disconnected => "Disconnected",
36            Self::Connecting => "Connecting…",
37            Self::Connected => "Connected",
38            Self::Evicted => "Evicted",
39            Self::NotMcp => "Not MCP",
40        }
41    }
42}
43
44/// Cloud sync status from the last flush attempt.
45pub enum CloudSyncStatus {
46    Ok { count: usize },
47    Failed { message: String },
48}
49
50/// Display + health state for one proxy instance.
51pub struct ProxyHealth {
52    /// Public URL where AI clients connect (e.g., http://localhost:3000 or tunnel URL).
53    pub proxy_url: String,
54    /// Tunnel public URL (empty if tunnel disabled).
55    pub tunnel_url: String,
56    /// Upstream MCP server URL from config.
57    pub mcp_upstream: String,
58    /// Widget source description ("URL", "path", or "(none)").
59    pub widgets: String,
60
61    /// MCP upstream connection health.
62    pub mcp_status: ConnectionStatus,
63    /// Optional warning about MCP upstream (e.g., "Server requires auth").
64    pub mcp_warning: Option<String>,
65    /// Tunnel connection health.
66    pub tunnel_status: ConnectionStatus,
67    /// Widget source connection health.
68    pub widgets_status: ConnectionStatus,
69    /// Number of discovered widgets.
70    pub widget_count: Option<usize>,
71    /// Names of discovered widgets.
72    pub widget_names: Vec<String>,
73
74    /// Cloud sync endpoint URL (None if cloud not configured).
75    pub cloud_endpoint: Option<String>,
76    /// Last cloud sync status.
77    pub cloud_sync: Option<CloudSyncStatus>,
78
79    /// When this proxy instance started.
80    pub started_at: Instant,
81    /// Total number of requests handled.
82    pub request_count: u64,
83}
84
85impl ProxyHealth {
86    pub fn new() -> Self {
87        Self {
88            proxy_url: String::new(),
89            tunnel_url: String::new(),
90            mcp_upstream: String::new(),
91            widgets: "(none)".into(),
92            mcp_status: ConnectionStatus::Unknown,
93            mcp_warning: None,
94            tunnel_status: ConnectionStatus::Disconnected,
95            widgets_status: ConnectionStatus::Unknown,
96            widget_count: None,
97            widget_names: Vec::new(),
98            cloud_endpoint: None,
99            cloud_sync: None,
100            started_at: Instant::now(),
101            request_count: 0,
102        }
103    }
104
105    /// Mark the MCP upstream as confirmed connected (clear any warning).
106    pub fn confirm_mcp_connected(&mut self) {
107        self.mcp_status = ConnectionStatus::Connected;
108        self.mcp_warning = None;
109    }
110
111    /// Increment request counter.
112    pub fn record_request(&mut self) {
113        self.request_count += 1;
114    }
115
116    /// Human-readable uptime string.
117    pub fn uptime(&self) -> String {
118        let secs = self.started_at.elapsed().as_secs();
119        if secs < 60 {
120            format!("{secs}s")
121        } else if secs < 3600 {
122            format!("{}m {}s", secs / 60, secs % 60)
123        } else {
124            format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
125        }
126    }
127}
128
129impl Default for ProxyHealth {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135/// Thread-safe shared proxy health. Prefer [`lock_health`] over `.lock().unwrap()`
136/// to handle poisoning without panic.
137pub type SharedProxyHealth = Arc<Mutex<ProxyHealth>>;
138
139/// Create a new shared proxy health container.
140pub fn new_shared_health() -> SharedProxyHealth {
141    Arc::new(Mutex::new(ProxyHealth::new()))
142}
143
144/// Lock the shared health, recovering from poison if a thread panicked.
145pub fn lock_health(health: &SharedProxyHealth) -> std::sync::MutexGuard<'_, ProxyHealth> {
146    health.lock().unwrap_or_else(|e| e.into_inner())
147}