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}