Implement WakeOnLAN functionality with simplified configuration
All checks were successful
Build and Release / build-and-release (push) Successful in 2m32s
All checks were successful
Build and Release / build-and-release (push) Successful in 2m32s
- Add Status::Offline enum variant for disconnected hosts
- All configured hosts now always visible showing offline status when disconnected
- Add WakeOnLAN support using wake-on-lan Rust crate
- Implement w key binding to wake offline hosts with MAC addresses
- Simplify configuration to single [hosts] section with MAC addresses only
- Change critical status icon from ◯ to ! for better visibility
- Add proper MAC address parsing and error handling
- Silent WakeOnLAN operation with logging for success/failure
Configuration format:
[hosts]
hostname = { mac_address = "AA:BB:CC:DD:EE:FF" }
This commit is contained in:
@@ -9,6 +9,7 @@ use ratatui::{
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
use wake_on_lan::MagicPacket;
|
||||
|
||||
pub mod theme;
|
||||
pub mod widgets;
|
||||
@@ -93,15 +94,25 @@ pub struct TuiApp {
|
||||
|
||||
impl TuiApp {
|
||||
pub fn new(config: DashboardConfig) -> Self {
|
||||
Self {
|
||||
let mut app = Self {
|
||||
host_widgets: HashMap::new(),
|
||||
current_host: None,
|
||||
available_hosts: Vec::new(),
|
||||
available_hosts: config.hosts.keys().cloned().collect(),
|
||||
host_index: 0,
|
||||
should_quit: false,
|
||||
user_navigated_away: false,
|
||||
config,
|
||||
};
|
||||
|
||||
// Sort predefined hosts
|
||||
app.available_hosts.sort();
|
||||
|
||||
// Initialize with first host if available
|
||||
if !app.available_hosts.is_empty() {
|
||||
app.current_host = Some(app.available_hosts[0].clone());
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
/// Get or create host widgets for the given hostname
|
||||
@@ -186,21 +197,28 @@ impl TuiApp {
|
||||
}
|
||||
|
||||
/// Update available hosts with localhost prioritization
|
||||
pub fn update_hosts(&mut self, hosts: Vec<String>) {
|
||||
// Sort hosts alphabetically
|
||||
let mut sorted_hosts = hosts.clone();
|
||||
pub fn update_hosts(&mut self, discovered_hosts: Vec<String>) {
|
||||
// Start with configured hosts (always visible)
|
||||
let mut all_hosts: Vec<String> = self.config.hosts.keys().cloned().collect();
|
||||
|
||||
// Add any discovered hosts that aren't already configured
|
||||
for host in discovered_hosts {
|
||||
if !all_hosts.contains(&host) {
|
||||
all_hosts.push(host);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep hosts that have pending transitions even if they're offline
|
||||
for (hostname, host_widgets) in &self.host_widgets {
|
||||
if !host_widgets.pending_service_transitions.is_empty() {
|
||||
if !sorted_hosts.contains(hostname) {
|
||||
sorted_hosts.push(hostname.clone());
|
||||
if !all_hosts.contains(hostname) {
|
||||
all_hosts.push(hostname.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sorted_hosts.sort();
|
||||
self.available_hosts = sorted_hosts;
|
||||
all_hosts.sort();
|
||||
self.available_hosts = all_hosts;
|
||||
|
||||
// Get the current hostname (localhost) for auto-selection
|
||||
let localhost = gethostname::gethostname().to_string_lossy().to_string();
|
||||
@@ -331,6 +349,33 @@ impl TuiApp {
|
||||
return Ok(Some(UiCommand::TriggerBackup { hostname }));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('w') => {
|
||||
// Wake on LAN for offline hosts
|
||||
if let Some(hostname) = self.current_host.clone() {
|
||||
// Check if host has MAC address configured
|
||||
if let Some(host_details) = self.config.hosts.get(&hostname) {
|
||||
if let Some(mac_address) = &host_details.mac_address {
|
||||
// Parse MAC address and send WoL packet
|
||||
let mac_bytes = Self::parse_mac_address(mac_address);
|
||||
match mac_bytes {
|
||||
Ok(mac) => {
|
||||
match MagicPacket::new(&mac).send() {
|
||||
Ok(_) => {
|
||||
info!("WakeOnLAN packet sent successfully to {} ({})", hostname, mac_address);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to send WakeOnLAN packet to {}: {}", hostname, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("Invalid MAC address format for {}: {}", hostname, mac_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
// Tab cycles to next host
|
||||
self.navigate_host(1);
|
||||
@@ -674,7 +719,7 @@ impl TuiApp {
|
||||
let metrics = metric_store.get_metrics_for_host(hostname);
|
||||
|
||||
if metrics.is_empty() {
|
||||
return Status::Unknown;
|
||||
return Status::Offline;
|
||||
}
|
||||
|
||||
// First check if we have the aggregated host status summary from the agent
|
||||
@@ -694,7 +739,8 @@ impl TuiApp {
|
||||
Status::Warning => has_warning = true,
|
||||
Status::Pending => has_pending = true,
|
||||
Status::Ok => ok_count += 1,
|
||||
Status::Unknown => {} // Ignore unknown for aggregation
|
||||
Status::Unknown => {}, // Ignore unknown for aggregation
|
||||
Status::Offline => {}, // Ignore offline for aggregation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,6 +781,7 @@ impl TuiApp {
|
||||
shortcuts.push("s/S: Start/Stop".to_string());
|
||||
shortcuts.push("J: Logs".to_string());
|
||||
shortcuts.push("L: Custom".to_string());
|
||||
shortcuts.push("w: Wake".to_string());
|
||||
|
||||
// Always show quit
|
||||
shortcuts.push("q: Quit".to_string());
|
||||
@@ -773,5 +820,20 @@ impl TuiApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse MAC address string (e.g., "AA:BB:CC:DD:EE:FF") to [u8; 6]
|
||||
fn parse_mac_address(mac_str: &str) -> Result<[u8; 6], &'static str> {
|
||||
let parts: Vec<&str> = mac_str.split(':').collect();
|
||||
if parts.len() != 6 {
|
||||
return Err("MAC address must have 6 parts separated by colons");
|
||||
}
|
||||
|
||||
let mut mac = [0u8; 6];
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
match u8::from_str_radix(part, 16) {
|
||||
Ok(byte) => mac[i] = byte,
|
||||
Err(_) => return Err("Invalid hexadecimal byte in MAC address"),
|
||||
}
|
||||
}
|
||||
Ok(mac)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user