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}
23
24#[derive(Debug, Deserialize)]
25struct ServerMetricsResponse {
26 qps: Option<f64>,
27 avg_latency_ms: Option<f64>,
28 p99_latency_ms: Option<f64>,
29 memory_usage_mb: Option<u64>,
30 active_connections: Option<u64>,
31}
32
33#[derive(Debug, Deserialize)]
34struct ServerHealthResponse {
35 status: Option<String>,
36 message: Option<String>,
37}
38
39#[derive(Debug, Deserialize)]
40struct ServerCompactionResponse {
41 success: Option<bool>,
42 message: Option<String>,
43}
44
45pub async fn execute_remote<W: Write>(
47 client: &HttpClient,
48 cmd: &ServerCommand,
49 writer: &mut W,
50 quiet: bool,
51) -> Result<()> {
52 match cmd {
53 ServerCommand::Status => {
54 let response: ServerStatusResponse = client
55 .get_json("api/admin/status")
56 .await
57 .map_err(map_client_error)?;
58 if quiet {
59 return Ok(());
60 }
61 render_table(
62 writer,
63 server_output::status_columns(),
64 vec![server_output::status_row(
65 response.version.as_deref(),
66 response.uptime_secs,
67 response.connections,
68 response.queries_per_second,
69 )],
70 )
71 }
72 ServerCommand::Metrics => {
73 let response: ServerMetricsResponse = client
74 .get_json("api/admin/metrics")
75 .await
76 .map_err(map_client_error)?;
77 if quiet {
78 return Ok(());
79 }
80 render_table(
81 writer,
82 server_output::metrics_columns(),
83 vec![server_output::metrics_row(
84 response.qps,
85 response.avg_latency_ms,
86 response.p99_latency_ms,
87 response.memory_usage_mb,
88 response.active_connections,
89 )],
90 )
91 }
92 ServerCommand::Health => {
93 let response: ServerHealthResponse = client
94 .get_json("api/admin/health")
95 .await
96 .map_err(map_client_error)?;
97 if quiet {
98 return Ok(());
99 }
100 render_table(
101 writer,
102 server_output::health_columns(),
103 vec![server_output::health_row(
104 response.status.as_deref(),
105 response.message.as_deref(),
106 )],
107 )
108 }
109 ServerCommand::Compaction { command } => match command {
110 CompactionCommand::Trigger => {
111 let request = serde_json::json!({});
112 let response: ServerCompactionResponse = client
113 .post_json("api/admin/compaction", &request)
114 .await
115 .map_err(map_client_error)?;
116 if quiet {
117 return Ok(());
118 }
119 render_table(
120 writer,
121 server_output::compaction_columns(),
122 vec![server_output::compaction_row(
123 response.success,
124 response.message.as_deref(),
125 )],
126 )
127 }
128 },
129 }
130}
131
132pub async fn execute_remote_tui(
133 client: &HttpClient,
134 cmd: &ServerCommand,
135 quiet: bool,
136 connection_label: impl Into<String>,
137 output_format: OutputFormat,
138 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + '_>>,
139) -> Result<()> {
140 match cmd {
141 ServerCommand::Status => {
142 let response: ServerStatusResponse = client
143 .get_json("api/admin/status")
144 .await
145 .map_err(map_client_error)?;
146 if quiet {
147 return Ok(());
148 }
149 render_output(
150 server_output::status_columns(),
151 vec![server_output::status_row(
152 response.version.as_deref(),
153 response.uptime_secs,
154 response.connections,
155 response.queries_per_second,
156 )],
157 connection_label,
158 Some(server_command_context(cmd)),
159 true,
160 None,
161 output_format,
162 admin_launcher,
163 )
164 }
165 ServerCommand::Metrics => {
166 let response: ServerMetricsResponse = client
167 .get_json("api/admin/metrics")
168 .await
169 .map_err(map_client_error)?;
170 if quiet {
171 return Ok(());
172 }
173 render_output(
174 server_output::metrics_columns(),
175 vec![server_output::metrics_row(
176 response.qps,
177 response.avg_latency_ms,
178 response.p99_latency_ms,
179 response.memory_usage_mb,
180 response.active_connections,
181 )],
182 connection_label,
183 Some(server_command_context(cmd)),
184 true,
185 None,
186 output_format,
187 admin_launcher,
188 )
189 }
190 ServerCommand::Health => {
191 let response: ServerHealthResponse = client
192 .get_json("api/admin/health")
193 .await
194 .map_err(map_client_error)?;
195 if quiet {
196 return Ok(());
197 }
198 render_output(
199 server_output::health_columns(),
200 vec![server_output::health_row(
201 response.status.as_deref(),
202 response.message.as_deref(),
203 )],
204 connection_label,
205 Some(server_command_context(cmd)),
206 true,
207 None,
208 output_format,
209 admin_launcher,
210 )
211 }
212 ServerCommand::Compaction { command } => match command {
213 CompactionCommand::Trigger => {
214 let request = serde_json::json!({});
215 let response: ServerCompactionResponse = client
216 .post_json("api/admin/compaction", &request)
217 .await
218 .map_err(map_client_error)?;
219 if quiet {
220 return Ok(());
221 }
222 render_output(
223 server_output::compaction_columns(),
224 vec![server_output::compaction_row(
225 response.success,
226 response.message.as_deref(),
227 )],
228 connection_label,
229 Some(server_command_context(cmd)),
230 true,
231 None,
232 output_format,
233 admin_launcher,
234 )
235 }
236 },
237 }
238}
239
240fn server_command_context(cmd: &ServerCommand) -> String {
241 match cmd {
242 ServerCommand::Status => "server status".to_string(),
243 ServerCommand::Metrics => "server metrics".to_string(),
244 ServerCommand::Health => "server health".to_string(),
245 ServerCommand::Compaction { .. } => "server compaction trigger".to_string(),
246 }
247}
248
249fn render_table<W: Write>(writer: &mut W, columns: Vec<Column>, rows: Vec<Row>) -> Result<()> {
250 let mut formatter = TableFormatter::new();
251 formatter.write_header(writer, &columns)?;
252 for row in rows {
253 formatter.write_row(writer, &row)?;
254 }
255 formatter.write_footer(writer)
256}
257
258fn map_client_error(err: ClientError) -> CliError {
259 match err {
260 ClientError::Request { source, .. } => {
261 CliError::ServerConnection(format!("request failed: {source}"))
262 }
263 ClientError::InvalidUrl(message) => CliError::InvalidArgument(message),
264 ClientError::Build(message) => CliError::InvalidArgument(message),
265 ClientError::Auth(err) => CliError::InvalidArgument(err.to_string()),
266 ClientError::HttpStatus { status, body } => {
267 CliError::InvalidArgument(format!("Server error: HTTP {} - {}", status.as_u16(), body))
268 }
269 }
270}