1use std::io::Write;
4
5use serde::Deserialize;
6
7use crate::cli::{CompactionCommand, OutputFormat, ServerCommand};
8use crate::client::http::{ClientError, HttpClient};
9use crate::error::{CliError, Result};
10use crate::models::{Column, Row};
11use crate::output::server as server_output;
12use crate::output::table::TableFormatter;
13use crate::output::Formatter;
14use crate::tui::renderer::render_output;
15
16#[derive(Debug, Deserialize)]
17struct ServerStatusResponse {
18 version: Option<String>,
19 uptime_secs: Option<u64>,
20 connections: Option<u64>,
21 queries_per_second: Option<f64>,
22 cluster: Option<ServerClusterStatus>,
23}
24
25#[derive(Debug, Deserialize)]
26struct ServerMetricsResponse {
27 qps: Option<f64>,
28 avg_latency_ms: Option<f64>,
29 p99_latency_ms: Option<f64>,
30 memory_usage_mb: Option<u64>,
31 active_connections: Option<u64>,
32}
33
34#[derive(Debug, Deserialize)]
35struct ServerHealthResponse {
36 status: Option<String>,
37 message: Option<String>,
38 degraded: Option<bool>,
39 cluster: Option<ServerClusterStatus>,
40}
41
42#[derive(Debug, Deserialize)]
43struct ServerCompactionResponse {
44 success: Option<bool>,
45 message: Option<String>,
46}
47
48#[derive(Debug, Deserialize)]
49struct ServerClusterOperationResponse {
50 action: Option<String>,
51 cluster: Option<ServerClusterStatus>,
52}
53
54#[derive(Debug, Deserialize)]
55struct ServerClusterStatus {
56 schema_version: Option<u32>,
57 mode: Option<String>,
58 identity: Option<ServerClusterIdentity>,
59 routing_capabilities: Option<ServerRoutingCapabilities>,
60 degraded: Option<bool>,
61 diagnostics: Option<Vec<ServerClusterDiagnostic>>,
62}
63
64#[derive(Debug, Deserialize)]
65struct ServerClusterIdentity {
66 node_id: Option<String>,
67 lifecycle_state: Option<String>,
68}
69
70#[derive(Debug, Deserialize)]
71struct ServerRoutingCapabilities {
72 local_only: Option<bool>,
73 future_distributed_execution_required: Option<bool>,
74 scatter_gather_simulated: Option<bool>,
75}
76
77#[derive(Debug, Deserialize)]
78struct ServerClusterDiagnostic {
79 code: Option<String>,
80}
81
82pub async fn execute_remote<W: Write>(
84 client: &HttpClient,
85 cmd: &ServerCommand,
86 writer: &mut W,
87 quiet: bool,
88) -> Result<()> {
89 match cmd {
90 ServerCommand::Status => {
91 let response: ServerStatusResponse = client
92 .get_json("api/admin/status")
93 .await
94 .map_err(map_client_error)?;
95 if quiet {
96 return Ok(());
97 }
98 render_table(
99 writer,
100 server_output::status_columns(),
101 vec![status_row_from_response(&response)],
102 )
103 }
104 ServerCommand::Metrics => {
105 let response: ServerMetricsResponse = client
106 .get_json("api/admin/metrics")
107 .await
108 .map_err(map_client_error)?;
109 if quiet {
110 return Ok(());
111 }
112 render_table(
113 writer,
114 server_output::metrics_columns(),
115 vec![server_output::metrics_row(
116 response.qps,
117 response.avg_latency_ms,
118 response.p99_latency_ms,
119 response.memory_usage_mb,
120 response.active_connections,
121 )],
122 )
123 }
124 ServerCommand::Health => {
125 let response: ServerHealthResponse = client
126 .get_json("api/admin/health")
127 .await
128 .map_err(map_client_error)?;
129 if quiet {
130 return Ok(());
131 }
132 render_table(
133 writer,
134 server_output::health_columns(),
135 vec![health_row_from_response(&response)],
136 )
137 }
138 ServerCommand::Join => {
139 let response = execute_cluster_operation(client, "join").await?;
140 if quiet {
141 return Ok(());
142 }
143 render_cluster_operation(writer, &response)
144 }
145 ServerCommand::Leave => {
146 let response = execute_cluster_operation(client, "leave").await?;
147 if quiet {
148 return Ok(());
149 }
150 render_cluster_operation(writer, &response)
151 }
152 ServerCommand::Compaction { command } => match command {
153 CompactionCommand::Trigger => {
154 let request = serde_json::json!({});
155 let response: ServerCompactionResponse = client
156 .post_json("api/admin/compaction", &request)
157 .await
158 .map_err(map_client_error)?;
159 if quiet {
160 return Ok(());
161 }
162 render_table(
163 writer,
164 server_output::compaction_columns(),
165 vec![server_output::compaction_row(
166 response.success,
167 response.message.as_deref(),
168 )],
169 )
170 }
171 },
172 }
173}
174
175pub async fn execute_remote_tui(
176 client: &HttpClient,
177 cmd: &ServerCommand,
178 quiet: bool,
179 connection_label: impl Into<String>,
180 output_format: OutputFormat,
181 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + '_>>,
182) -> Result<()> {
183 match cmd {
184 ServerCommand::Status => {
185 let response: ServerStatusResponse = client
186 .get_json("api/admin/status")
187 .await
188 .map_err(map_client_error)?;
189 if quiet {
190 return Ok(());
191 }
192 render_output(
193 server_output::status_columns(),
194 vec![status_row_from_response(&response)],
195 connection_label,
196 Some(server_command_context(cmd)),
197 true,
198 None,
199 output_format,
200 admin_launcher,
201 )
202 }
203 ServerCommand::Metrics => {
204 let response: ServerMetricsResponse = client
205 .get_json("api/admin/metrics")
206 .await
207 .map_err(map_client_error)?;
208 if quiet {
209 return Ok(());
210 }
211 render_output(
212 server_output::metrics_columns(),
213 vec![server_output::metrics_row(
214 response.qps,
215 response.avg_latency_ms,
216 response.p99_latency_ms,
217 response.memory_usage_mb,
218 response.active_connections,
219 )],
220 connection_label,
221 Some(server_command_context(cmd)),
222 true,
223 None,
224 output_format,
225 admin_launcher,
226 )
227 }
228 ServerCommand::Health => {
229 let response: ServerHealthResponse = client
230 .get_json("api/admin/health")
231 .await
232 .map_err(map_client_error)?;
233 if quiet {
234 return Ok(());
235 }
236 render_output(
237 server_output::health_columns(),
238 vec![health_row_from_response(&response)],
239 connection_label,
240 Some(server_command_context(cmd)),
241 true,
242 None,
243 output_format,
244 admin_launcher,
245 )
246 }
247 ServerCommand::Join => {
248 let response = execute_cluster_operation(client, "join").await?;
249 if quiet {
250 return Ok(());
251 }
252 render_output(
253 server_output::cluster_operation_columns(),
254 vec![cluster_operation_row(&response)],
255 connection_label,
256 Some(server_command_context(cmd)),
257 true,
258 None,
259 output_format,
260 admin_launcher,
261 )
262 }
263 ServerCommand::Leave => {
264 let response = execute_cluster_operation(client, "leave").await?;
265 if quiet {
266 return Ok(());
267 }
268 render_output(
269 server_output::cluster_operation_columns(),
270 vec![cluster_operation_row(&response)],
271 connection_label,
272 Some(server_command_context(cmd)),
273 true,
274 None,
275 output_format,
276 admin_launcher,
277 )
278 }
279 ServerCommand::Compaction { command } => match command {
280 CompactionCommand::Trigger => {
281 let request = serde_json::json!({});
282 let response: ServerCompactionResponse = client
283 .post_json("api/admin/compaction", &request)
284 .await
285 .map_err(map_client_error)?;
286 if quiet {
287 return Ok(());
288 }
289 render_output(
290 server_output::compaction_columns(),
291 vec![server_output::compaction_row(
292 response.success,
293 response.message.as_deref(),
294 )],
295 connection_label,
296 Some(server_command_context(cmd)),
297 true,
298 None,
299 output_format,
300 admin_launcher,
301 )
302 }
303 },
304 }
305}
306
307fn server_command_context(cmd: &ServerCommand) -> String {
308 match cmd {
309 ServerCommand::Status => "server status".to_string(),
310 ServerCommand::Metrics => "server metrics".to_string(),
311 ServerCommand::Health => "server health".to_string(),
312 ServerCommand::Join => "server join".to_string(),
313 ServerCommand::Leave => "server leave".to_string(),
314 ServerCommand::Compaction { .. } => "server compaction trigger".to_string(),
315 }
316}
317
318fn render_table<W: Write>(writer: &mut W, columns: Vec<Column>, rows: Vec<Row>) -> Result<()> {
319 let mut formatter = TableFormatter::new();
320 formatter.write_header(writer, &columns)?;
321 for row in rows {
322 formatter.write_row(writer, &row)?;
323 }
324 formatter.write_footer(writer)
325}
326
327fn status_row_from_response(response: &ServerStatusResponse) -> Row {
328 let cluster = ClusterDisplayFields::from(response.cluster.as_ref());
329 server_output::status_row(server_output::StatusRowFields {
330 version: response.version.as_deref(),
331 uptime_secs: response.uptime_secs,
332 connections: response.connections,
333 qps: response.queries_per_second,
334 cluster_schema_version: cluster.schema_version,
335 cluster_mode: cluster.mode,
336 node_id: cluster.node_id,
337 lifecycle_state: cluster.lifecycle_state,
338 degraded: cluster.degraded,
339 local_only: cluster.local_only,
340 future_distributed: cluster.future_distributed,
341 scatter_gather: cluster.scatter_gather,
342 diagnostics: cluster.diagnostics.as_deref(),
343 })
344}
345
346fn health_row_from_response(response: &ServerHealthResponse) -> Row {
347 let cluster = ClusterDisplayFields::from(response.cluster.as_ref());
348 server_output::health_row(
349 response.status.as_deref(),
350 response.message.as_deref(),
351 response.degraded.or(cluster.degraded),
352 cluster.mode,
353 cluster.node_id,
354 )
355}
356
357async fn execute_cluster_operation(
358 client: &HttpClient,
359 action: &str,
360) -> Result<ServerClusterOperationResponse> {
361 let request = serde_json::json!({});
362 let path = format!("api/admin/cluster/{action}");
363 client
364 .post_json(&path, &request)
365 .await
366 .map_err(map_client_error)
367}
368
369fn render_cluster_operation<W: Write>(
370 writer: &mut W,
371 response: &ServerClusterOperationResponse,
372) -> Result<()> {
373 render_table(
374 writer,
375 server_output::cluster_operation_columns(),
376 vec![cluster_operation_row(response)],
377 )
378}
379
380fn cluster_operation_row(response: &ServerClusterOperationResponse) -> Row {
381 let cluster = response.cluster.as_ref();
382 let identity = cluster.and_then(|cluster| cluster.identity.as_ref());
383 server_output::cluster_operation_row(
384 response.action.as_deref(),
385 cluster.and_then(|cluster| cluster.mode.as_deref()),
386 identity.and_then(|identity| identity.node_id.as_deref()),
387 identity.and_then(|identity| identity.lifecycle_state.as_deref()),
388 cluster.and_then(|cluster| cluster.degraded),
389 )
390}
391
392struct ClusterDisplayFields<'a> {
393 mode: Option<&'a str>,
394 schema_version: Option<u32>,
395 node_id: Option<&'a str>,
396 lifecycle_state: Option<&'a str>,
397 degraded: Option<bool>,
398 local_only: Option<bool>,
399 future_distributed: Option<bool>,
400 scatter_gather: Option<bool>,
401 diagnostics: Option<String>,
402}
403
404impl<'a> From<Option<&'a ServerClusterStatus>> for ClusterDisplayFields<'a> {
405 fn from(cluster: Option<&'a ServerClusterStatus>) -> Self {
406 let identity = cluster.and_then(|cluster| cluster.identity.as_ref());
407 let routing = cluster.and_then(|cluster| cluster.routing_capabilities.as_ref());
408 Self {
409 mode: cluster.and_then(|cluster| cluster.mode.as_deref()),
410 schema_version: cluster.and_then(|cluster| cluster.schema_version),
411 node_id: identity.and_then(|identity| identity.node_id.as_deref()),
412 lifecycle_state: identity.and_then(|identity| identity.lifecycle_state.as_deref()),
413 degraded: cluster.and_then(|cluster| cluster.degraded),
414 local_only: routing.and_then(|routing| routing.local_only),
415 future_distributed: routing
416 .and_then(|routing| routing.future_distributed_execution_required),
417 scatter_gather: routing.and_then(|routing| routing.scatter_gather_simulated),
418 diagnostics: cluster.and_then(diagnostic_codes),
419 }
420 }
421}
422
423fn diagnostic_codes(cluster: &ServerClusterStatus) -> Option<String> {
424 let diagnostics = cluster.diagnostics.as_ref()?;
425 let codes = diagnostics
426 .iter()
427 .filter_map(|diagnostic| diagnostic.code.as_deref())
428 .collect::<Vec<_>>();
429 if codes.is_empty() {
430 None
431 } else {
432 Some(codes.join(","))
433 }
434}
435
436fn map_client_error(err: ClientError) -> CliError {
437 match err {
438 ClientError::Request { source, .. } => {
439 CliError::ServerConnection(format!("request failed: {source}"))
440 }
441 ClientError::InvalidUrl(message) => CliError::InvalidArgument(message),
442 ClientError::Build(message) => CliError::InvalidArgument(message),
443 ClientError::Auth(err) => CliError::InvalidArgument(err.to_string()),
444 ClientError::HttpStatus { status, body } => {
445 CliError::InvalidArgument(format!("Server error: HTTP {} - {}", status.as_u16(), body))
446 }
447 }
448}