Skip to main content

alopex_cli/commands/
server.rs

1//! Server management commands.
2
3use 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
45/// Execute a server management command against a remote server.
46pub 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}