| 1 |
use clap::Args; |
| 2 |
use anyhow::{Context, Result}; |
| 3 |
use humansize::{format_size, BINARY}; |
| 4 |
use std::time::Duration; |
| 5 |
use tracing::info; |
| 6 |
|
| 7 |
use crate::config::Config; |
| 8 |
use crate::client::ZephyrClient; |
| 9 |
use super::Command; |
| 10 |
|
| 11 |
#[derive(Debug, Args)] |
| 12 |
pub struct StatusCommand { |
| 13 |
/// Show detailed status information |
| 14 |
#[arg(short, long)] |
| 15 |
detailed: bool, |
| 16 |
|
| 17 |
/// Output format (table, json) |
| 18 |
#[arg(long, default_value = "table")] |
| 19 |
format: String, |
| 20 |
|
| 21 |
/// Refresh interval in seconds (0 for single check) |
| 22 |
#[arg(short, long, default_value = "0")] |
| 23 |
watch: u64, |
| 24 |
} |
| 25 |
|
| 26 |
#[async_trait::async_trait] |
| 27 |
impl Command for StatusCommand { |
| 28 |
async fn execute(&self, config: &Config) -> Result<()> { |
| 29 |
info!("Getting ZephyrFS node status"); |
| 30 |
|
| 31 |
if self.watch > 0 { |
| 32 |
self.watch_status(config).await |
| 33 |
} else { |
| 34 |
self.show_status(config).await |
| 35 |
} |
| 36 |
} |
| 37 |
} |
| 38 |
|
| 39 |
impl StatusCommand { |
| 40 |
async fn show_status(&self, config: &Config) -> Result<()> { |
| 41 |
let client = ZephyrClient::new(config); |
| 42 |
|
| 43 |
// Get node status |
| 44 |
let node_info = client.get_node_status().await |
| 45 |
.context("Failed to get node status. Is the node running?")?; |
| 46 |
|
| 47 |
match self.format.as_str() { |
| 48 |
"table" => self.print_table_status(&node_info, config), |
| 49 |
"json" => self.print_json_status(&node_info)?, |
| 50 |
_ => anyhow::bail!("Invalid format: {}. Valid options: table, json", self.format), |
| 51 |
} |
| 52 |
|
| 53 |
Ok(()) |
| 54 |
} |
| 55 |
|
| 56 |
async fn watch_status(&self, config: &Config) -> Result<()> { |
| 57 |
println!("Watching node status (refresh every {} seconds)...", self.watch); |
| 58 |
println!("Press Ctrl+C to stop\n"); |
| 59 |
|
| 60 |
loop { |
| 61 |
// Clear screen |
| 62 |
print!("\x1B[2J\x1B[1;1H"); |
| 63 |
|
| 64 |
match self.show_status(config).await { |
| 65 |
Ok(()) => {}, |
| 66 |
Err(e) => println!("Error getting status: {}", e), |
| 67 |
} |
| 68 |
|
| 69 |
tokio::time::sleep(Duration::from_secs(self.watch)).await; |
| 70 |
} |
| 71 |
} |
| 72 |
|
| 73 |
fn print_table_status(&self, node_info: &crate::client::NodeInfo, config: &Config) { |
| 74 |
println!("ZephyrFS Node Status"); |
| 75 |
println!("{}", "=".repeat(50)); |
| 76 |
println!(); |
| 77 |
|
| 78 |
// Basic info |
| 79 |
println!("Node Information:"); |
| 80 |
println!(" ID: {}", node_info.id); |
| 81 |
println!(" Name: {}", node_info.name); |
| 82 |
println!(" Status: {}", self.format_status(&node_info.status)); |
| 83 |
println!(); |
| 84 |
|
| 85 |
// Network info |
| 86 |
println!("Network:"); |
| 87 |
println!(" Connected peers: {}", node_info.peers_connected); |
| 88 |
println!(" Listen port: {}", config.node.listen_port); |
| 89 |
println!(" Coordinator: {}", config.coordinator.endpoint); |
| 90 |
println!(); |
| 91 |
|
| 92 |
// Storage info |
| 93 |
let used_gb = node_info.storage_used_gb; |
| 94 |
let available_gb = node_info.storage_available_gb; |
| 95 |
let total_gb = used_gb + available_gb; |
| 96 |
let usage_percent = if total_gb > 0.0 { (used_gb / total_gb) * 100.0 } else { 0.0 }; |
| 97 |
|
| 98 |
println!("Storage:"); |
| 99 |
println!(" Used: {} ({:.1}%)", format_size((used_gb * 1e9) as u64, BINARY), usage_percent); |
| 100 |
println!(" Available: {}", format_size((available_gb * 1e9) as u64, BINARY)); |
| 101 |
println!(" Total allocated: {}", format_size((total_gb * 1e9) as u64, BINARY)); |
| 102 |
println!(" Max allocation: {} GB", config.storage.max_storage_gb); |
| 103 |
println!(); |
| 104 |
|
| 105 |
// Runtime info |
| 106 |
let uptime = Duration::from_secs(node_info.uptime_seconds); |
| 107 |
println!("Runtime:"); |
| 108 |
println!(" Uptime: {}", self.format_duration(uptime)); |
| 109 |
println!(" Data directory: {:?}", config.node.data_dir); |
| 110 |
|
| 111 |
if self.detailed { |
| 112 |
println!(); |
| 113 |
println!("Configuration:"); |
| 114 |
println!(" Chunk size: {} MB", config.storage.chunk_size_mb); |
| 115 |
println!(" Replication factor: {}", config.storage.replication_factor); |
| 116 |
println!(" Max connections: {}", config.network.max_connections); |
| 117 |
println!(" Connection timeout: {}s", config.network.connection_timeout); |
| 118 |
} |
| 119 |
} |
| 120 |
|
| 121 |
fn print_json_status(&self, node_info: &crate::client::NodeInfo) -> Result<()> { |
| 122 |
let json = serde_json::to_string_pretty(node_info) |
| 123 |
.context("Failed to serialize node info to JSON")?; |
| 124 |
println!("{}", json); |
| 125 |
Ok(()) |
| 126 |
} |
| 127 |
|
| 128 |
fn format_status(&self, status: &str) -> String { |
| 129 |
match status.to_lowercase().as_str() { |
| 130 |
"running" => "🟢 Running".to_string(), |
| 131 |
"starting" => "🟡 Starting".to_string(), |
| 132 |
"stopping" => "🟡 Stopping".to_string(), |
| 133 |
"stopped" => "🔴 Stopped".to_string(), |
| 134 |
"error" => "❌ Error".to_string(), |
| 135 |
_ => format!("❓ {}", status), |
| 136 |
} |
| 137 |
} |
| 138 |
|
| 139 |
fn format_duration(&self, duration: Duration) -> String { |
| 140 |
let secs = duration.as_secs(); |
| 141 |
|
| 142 |
if secs < 60 { |
| 143 |
format!("{}s", secs) |
| 144 |
} else if secs < 3600 { |
| 145 |
format!("{}m {}s", secs / 60, secs % 60) |
| 146 |
} else if secs < 86400 { |
| 147 |
let hours = secs / 3600; |
| 148 |
let mins = (secs % 3600) / 60; |
| 149 |
format!("{}h {}m", hours, mins) |
| 150 |
} else { |
| 151 |
let days = secs / 86400; |
| 152 |
let hours = (secs % 86400) / 3600; |
| 153 |
format!("{}d {}h", days, hours) |
| 154 |
} |
| 155 |
} |
| 156 |
} |