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}