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, cloud sync, request counters. Updated by
5//! background tasks (health checks, request handlers, tunnel callbacks) and
6//! 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).
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
59    /// MCP upstream connection health.
60    pub mcp_status: ConnectionStatus,
61    /// Optional warning about MCP upstream (e.g., "Server requires auth").
62    pub mcp_warning: Option<String>,
63    /// Tunnel connection health.
64    pub tunnel_status: ConnectionStatus,
65
66    /// Cloud sync endpoint URL (None if cloud not configured).
67    pub cloud_endpoint: Option<String>,
68    /// Last cloud sync status.
69    pub cloud_sync: Option<CloudSyncStatus>,
70
71    /// When this proxy instance started.
72    pub started_at: Instant,
73    /// Total number of requests handled.
74    pub request_count: u64,
75}
76
77impl ProxyHealth {
78    pub fn new() -> Self {
79        Self {
80            proxy_url: String::new(),
81            tunnel_url: String::new(),
82            mcp_upstream: String::new(),
83            mcp_status: ConnectionStatus::Unknown,
84            mcp_warning: None,
85            tunnel_status: ConnectionStatus::Disconnected,
86            cloud_endpoint: None,
87            cloud_sync: None,
88            started_at: Instant::now(),
89            request_count: 0,
90        }
91    }
92
93    /// Mark the MCP upstream as confirmed connected (clear any warning).
94    pub fn confirm_mcp_connected(&mut self) {
95        self.mcp_status = ConnectionStatus::Connected;
96        self.mcp_warning = None;
97    }
98
99    /// Increment request counter.
100    pub fn record_request(&mut self) {
101        self.request_count += 1;
102    }
103
104    /// Human-readable uptime string.
105    pub fn uptime(&self) -> String {
106        let secs = self.started_at.elapsed().as_secs();
107        if secs < 60 {
108            format!("{secs}s")
109        } else if secs < 3600 {
110            format!("{}m {}s", secs / 60, secs % 60)
111        } else {
112            format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
113        }
114    }
115}
116
117impl Default for ProxyHealth {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123/// Thread-safe shared proxy health. Prefer [`lock_health`] over `.lock().unwrap()`
124/// to handle poisoning without panic.
125pub type SharedProxyHealth = Arc<Mutex<ProxyHealth>>;
126
127/// Create a new shared proxy health container.
128pub fn new_shared_health() -> SharedProxyHealth {
129    Arc::new(Mutex::new(ProxyHealth::new()))
130}
131
132/// Lock the shared health, recovering from poison if a thread panicked.
133pub fn lock_health(health: &SharedProxyHealth) -> std::sync::MutexGuard<'_, ProxyHealth> {
134    health.lock().unwrap_or_else(|e| e.into_inner())
135}