1use std::collections::HashMap;
9
10use crate::state::app_state::{AccessLogEntry, AppState};
11use crate::state::client::{ClientConnectionAttempt, ClientStatus};
12use crate::state::server::{ClosedConnectionSummary, ServerStatus};
13use crate::state::{AccessLogOwner, ClientId, ServerId};
14
15#[derive(Debug, Clone)]
16pub struct ConnRow {
17 pub id: u32,
18 pub remote_addr: String,
19 pub bytes_received: u64,
20 pub bytes_sent: u64,
21 pub active: bool,
22 pub can_message: bool,
25}
26
27#[derive(Debug, Clone)]
28pub struct ServerRow {
29 pub id: ServerId,
30 pub protocol: String,
31 pub port: u16,
32 pub local_addr: Option<String>,
33 pub status: ServerStatus,
34 pub instruction: String,
35 pub memory_len: usize,
36 pub startup_params: Option<serde_json::Value>,
37 pub routing: Option<crate::scripting::EventHandlerConfig>,
38 pub conns: Vec<ConnRow>,
39 pub recent: Vec<ClosedConnectionSummary>,
40 pub requests: Vec<AccessLogEntry>,
41 pub task_count: usize,
42 pub uptime_secs: u64,
44 pub client_counterpart: Option<String>,
47 pub request_only: Option<String>,
51 pub intercepts: Vec<crate::state::intercepts::InterceptView>,
53}
54
55#[derive(Debug, Clone)]
56pub struct ClientRow {
57 pub id: ClientId,
58 pub protocol: String,
59 pub remote_addr: String,
60 pub status: ClientStatus,
61 pub instruction: String,
62 pub memory_len: usize,
63 pub startup_params: Option<serde_json::Value>,
64 pub routing: Option<crate::scripting::EventHandlerConfig>,
65 pub connection: Option<ConnRow>,
66 pub history: Vec<ClientConnectionAttempt>,
67 pub requests: Vec<AccessLogEntry>,
68 pub task_count: usize,
69 pub uptime_secs: u64,
71 pub send_state: SendState,
75 pub send_actions: Vec<SendVerb>,
81 pub intercepts: Vec<crate::state::intercepts::InterceptView>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct SendVerb {
88 pub name: String,
89 pub description: String,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum SendState {
94 Ready,
96 NotConnected,
98 ProtocolUnsupported,
101}
102
103#[derive(Debug, Clone, Default)]
104pub struct RailSnapshot {
105 pub servers: Vec<ServerRow>,
106 pub clients: Vec<ClientRow>,
107 pub pipe_count: usize,
108 pub active_conversations: usize,
109 pub llm_activity: Vec<crate::state::llm_activity::LlmActivity>,
110 pub total_input_tokens: u64,
111 pub total_output_tokens: u64,
112 pub total_llm_calls: u64,
113}
114
115const REQUESTS_PER_BAND: usize = 100;
117
118pub fn is_initiable_action(name: &str) -> bool {
125 match name {
126 "wait_for_more" => false,
129 "disconnect" => false,
133 _ => true,
134 }
135}
136
137pub async fn build_snapshot(state: &AppState) -> RailSnapshot {
138 let servers = state.get_all_servers().await;
139 let clients = state.get_all_clients().await;
140 let tasks = state.get_all_tasks().await;
141 let llm_activity = state.llm_activity.snapshot();
142 let (total_input_tokens, total_output_tokens, total_llm_calls) = state.get_llm_stats().await;
143 let pipe_count = state.list_pipes().await.len();
144
145 let mut server_intercepts: HashMap<u32, Vec<crate::state::intercepts::InterceptView>> =
147 HashMap::new();
148 let mut client_intercepts: HashMap<u32, Vec<crate::state::intercepts::InterceptView>> =
149 HashMap::new();
150 for view in state.list_intercepts().await {
151 match view.owner {
152 crate::state::intercepts::InterceptOwner::Server(id) => {
153 server_intercepts.entry(id.as_u32()).or_default().push(view)
154 }
155 crate::state::intercepts::InterceptOwner::Client(id) => {
156 client_intercepts.entry(id.as_u32()).or_default().push(view)
157 }
158 }
159 }
160
161 let mut server_requests: HashMap<u32, Vec<AccessLogEntry>> = HashMap::new();
163 let mut client_requests: HashMap<u32, Vec<AccessLogEntry>> = HashMap::new();
164 for entry in state.list_access_logs(None).await {
165 match entry.owner() {
166 Some(AccessLogOwner::Server(id)) => {
167 let bucket = server_requests.entry(id).or_default();
168 if bucket.len() < REQUESTS_PER_BAND {
169 bucket.push(entry);
170 }
171 }
172 Some(AccessLogOwner::Client(id)) => {
173 let bucket = client_requests.entry(id).or_default();
174 if bucket.len() < REQUESTS_PER_BAND {
175 bucket.push(entry);
176 }
177 }
178 None => {}
179 }
180 }
181
182 let mut server_rows = Vec::with_capacity(servers.len());
183 for server in servers {
184 let mut conns: Vec<ConnRow> = Vec::with_capacity(server.connections.len());
185 for c in server.connections.values() {
186 conns.push(ConnRow {
187 id: c.id.as_u32(),
188 remote_addr: c.remote_addr.to_string(),
189 bytes_received: c.bytes_received,
190 bytes_sent: c.bytes_sent,
191 active: c.status == crate::state::server::ConnectionStatus::Active,
192 can_message: state.has_peer_handle(server.id, c.id.as_u32()).await,
193 });
194 }
195 conns.sort_by_key(|c| c.id);
196
197 let task_count = tasks
198 .iter()
199 .filter(|t| match t.scope {
200 crate::state::task::TaskScope::Server(sid) => sid == server.id,
201 crate::state::task::TaskScope::Connection(sid, _) => sid == server.id,
202 _ => false,
203 })
204 .count();
205
206 server_rows.push(ServerRow {
207 id: server.id,
208 protocol: server.protocol_name.clone(),
209 port: server.port,
210 local_addr: server.local_addr.map(|a| a.to_string()),
211 status: server.status.clone(),
212 instruction: server.instruction.clone(),
213 memory_len: server.memory.len(),
214 startup_params: server.startup_params.clone(),
215 routing: server.event_handler_config.clone(),
216 recent: server.recent_connections.iter().cloned().collect(),
217 requests: server_requests
218 .remove(&server.id.as_u32())
219 .unwrap_or_default(),
220 conns,
221 task_count,
222 uptime_secs: server.created_at.elapsed().as_secs(),
223 client_counterpart: crate::protocol::compiled_client_protocol_for_server(
224 &server.protocol_name,
225 ),
226 request_only: crate::protocol::server_registry::registry()
227 .request_only_reason(&server.protocol_name)
228 .map(str::to_string),
229 intercepts: server_intercepts
230 .remove(&server.id.as_u32())
231 .unwrap_or_default(),
232 });
233 }
234 server_rows.sort_by_key(|s| s.id.as_u32());
235
236 let mut client_rows = Vec::with_capacity(clients.len());
237 for client in clients {
238 let connected = client.status == ClientStatus::Connected;
239 let send_state = match (connected, state.has_client_handle(client.id).await) {
240 (true, true) => SendState::Ready,
241 (true, false) => SendState::ProtocolUnsupported,
242 (false, _) => SendState::NotConnected,
243 };
244 let task_count = tasks
245 .iter()
246 .filter(|t| matches!(t.scope, crate::state::task::TaskScope::Client(cid) if cid == client.id))
247 .count();
248 client_rows.push(ClientRow {
249 id: client.id,
250 protocol: client.protocol_name.clone(),
251 remote_addr: client.remote_addr.clone(),
252 status: client.status.clone(),
253 instruction: client.instruction.clone(),
254 memory_len: client.memory.len(),
255 startup_params: client.startup_params.clone(),
256 routing: client.event_handler_config.clone(),
257 connection: client.connection.as_ref().map(|c| ConnRow {
258 id: c.id.as_u32(),
259 remote_addr: c.remote_addr.clone(),
260 bytes_received: c.bytes_received,
261 bytes_sent: c.bytes_sent,
262 active: c.status == ClientStatus::Connected,
263 can_message: false,
264 }),
265 history: client.connection_history.iter().cloned().collect(),
266 requests: client_requests
267 .remove(&client.id.as_u32())
268 .unwrap_or_default(),
269 task_count,
270 uptime_secs: client.created_at.elapsed().as_secs(),
271 send_state,
272 send_actions: crate::tui::modal::composer::ComposerModel::vocabulary(
273 &client.protocol_name,
274 state,
275 )
276 .into_iter()
277 .filter(|action| is_initiable_action(&action.name))
278 .map(|action| SendVerb {
279 name: action.name,
280 description: action.description,
281 })
282 .collect(),
283 intercepts: client_intercepts
284 .remove(&client.id.as_u32())
285 .unwrap_or_default(),
286 });
287 }
288 client_rows.sort_by_key(|c| c.id.as_u32());
289
290 RailSnapshot {
291 servers: server_rows,
292 clients: client_rows,
293 pipe_count,
294 active_conversations: llm_activity.len(),
295 llm_activity,
296 total_input_tokens,
297 total_output_tokens,
298 total_llm_calls,
299 }
300}