Remove transitional icons and improve service logs
All checks were successful
Build and Release / build-and-release (push) Successful in 1m31s

- Remove all transitional icon infrastructure (CommandType, pending transitions)
- Clean up ZMQ command system remnants after SSH migration
- Add real-time log streaming for service start operations
- Show final logs and status for service stop operations
- Fix compilation warnings by removing unused methods
- Simplify UI architecture with pure SSH-based service control
This commit is contained in:
Christoffer Martinsson 2025-11-18 16:40:14 +01:00
parent de252d27b9
commit 6693f3a05f
8 changed files with 60 additions and 266 deletions

6
Cargo.lock generated
View File

@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.73" version = "0.1.74"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -292,7 +292,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.73" version = "0.1.74"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -315,7 +315,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.73" version = "0.1.74"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.74" version = "0.1.75"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -90,14 +90,6 @@ impl UserStoppedServiceTracker {
tracker tracker
} }
/// Mark a service as user-stopped
pub fn mark_user_stopped(&mut self, service_name: &str) -> Result<()> {
info!("Marking service '{}' as user-stopped", service_name);
self.user_stopped_services.insert(service_name.to_string());
self.save_to_storage()?;
debug!("Service '{}' marked as user-stopped and saved to storage", service_name);
Ok(())
}
/// Clear user-stopped flag for a service (when user starts it) /// Clear user-stopped flag for a service (when user starts it)
pub fn clear_user_stopped(&mut self, service_name: &str) -> Result<()> { pub fn clear_user_stopped(&mut self, service_name: &str) -> Result<()> {

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.74" version = "0.1.75"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -12,7 +12,7 @@ use tracing::{debug, error, info, warn};
use crate::communication::{ZmqConsumer}; use crate::communication::{ZmqConsumer};
use crate::config::DashboardConfig; use crate::config::DashboardConfig;
use crate::metrics::MetricStore; use crate::metrics::MetricStore;
use crate::ui::{TuiApp, UiCommand}; use crate::ui::TuiApp;
pub struct Dashboard { pub struct Dashboard {
zmq_consumer: ZmqConsumer, zmq_consumer: ZmqConsumer,
@ -141,16 +141,10 @@ impl Dashboard {
match event::read() { match event::read() {
Ok(event) => { Ok(event) => {
if let Some(ref mut tui_app) = self.tui_app { if let Some(ref mut tui_app) = self.tui_app {
// Handle input and check for commands // Handle input
match tui_app.handle_input(event) { match tui_app.handle_input(event) {
Ok(Some(command)) => { Ok(_) => {
// Execute the command // Check if we should quit
if let Err(e) = self.execute_ui_command(command).await {
error!("Failed to execute UI command: {}", e);
}
}
Ok(None) => {
// No command, check if we should quit
if tui_app.should_quit() { if tui_app.should_quit() {
info!("Quit requested, exiting dashboard"); info!("Quit requested, exiting dashboard");
break; break;
@ -277,17 +271,6 @@ impl Dashboard {
Ok(()) Ok(())
} }
/// Execute a UI command by sending it to the appropriate agent
async fn execute_ui_command(&self, command: UiCommand) -> Result<()> {
match command {
UiCommand::TriggerBackup { hostname } => {
info!("Trigger backup requested for {}", hostname);
// TODO: Implement backup trigger command
info!("Backup trigger not yet implemented");
}
}
Ok(())
}
} }

View File

@ -16,24 +16,12 @@ pub mod widgets;
use crate::config::DashboardConfig; use crate::config::DashboardConfig;
use crate::metrics::MetricStore; use crate::metrics::MetricStore;
use cm_dashboard_shared::{Metric, Status}; use cm_dashboard_shared::Status;
use theme::{Components, Layout as ThemeLayout, Theme, Typography}; use theme::{Components, Layout as ThemeLayout, Theme, Typography};
use widgets::{BackupWidget, ServicesWidget, SystemWidget, Widget}; use widgets::{BackupWidget, ServicesWidget, SystemWidget, Widget};
/// Commands that can be triggered from the UI
#[derive(Debug, Clone)]
pub enum UiCommand {
TriggerBackup { hostname: String },
}
/// Types of commands for status tracking
#[derive(Debug, Clone)]
pub enum CommandType {
ServiceStart,
ServiceStop,
BackupTrigger,
}
/// Panel types for focus management /// Panel types for focus management
@ -52,8 +40,6 @@ pub struct HostWidgets {
pub backup_scroll_offset: usize, pub backup_scroll_offset: usize,
/// Last update time for this host /// Last update time for this host
pub last_update: Option<Instant>, pub last_update: Option<Instant>,
/// Pending service transitions for immediate visual feedback
pub pending_service_transitions: HashMap<String, (CommandType, String, Instant)>, // service_name -> (command_type, original_status, start_time)
} }
impl HostWidgets { impl HostWidgets {
@ -66,7 +52,6 @@ impl HostWidgets {
services_scroll_offset: 0, services_scroll_offset: 0,
backup_scroll_offset: 0, backup_scroll_offset: 0,
last_update: None, last_update: None,
pending_service_transitions: HashMap::new(),
} }
} }
} }
@ -159,8 +144,6 @@ impl TuiApp {
} }
} }
// Clear completed transitions first
self.clear_completed_transitions(&hostname, &service_metrics);
// Now get host widgets and update them // Now get host widgets and update them
let host_widgets = self.get_or_create_host_widgets(&hostname); let host_widgets = self.get_or_create_host_widgets(&hostname);
@ -196,14 +179,6 @@ impl TuiApp {
} }
} }
// 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 !all_hosts.contains(hostname) {
all_hosts.push(hostname.clone());
}
}
}
all_hosts.sort(); all_hosts.sort();
self.available_hosts = all_hosts; self.available_hosts = all_hosts;
@ -234,7 +209,7 @@ impl TuiApp {
} }
/// Handle keyboard input /// Handle keyboard input
pub fn handle_input(&mut self, event: Event) -> Result<Option<UiCommand>> { pub fn handle_input(&mut self, event: Event) -> Result<()> {
if let Event::Key(key) = event { if let Event::Key(key) = event {
match key.code { match key.code {
KeyCode::Char('q') => { KeyCode::Char('q') => {
@ -297,12 +272,9 @@ impl TuiApp {
KeyCode::Char('s') => { KeyCode::Char('s') => {
// Service start command via SSH with progress display // Service start command via SSH with progress display
if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) { if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) {
// Start transition tracking for visual feedback
self.start_command(&hostname, CommandType::ServiceStart, service_name.clone());
let connection_ip = self.get_connection_ip(&hostname); let connection_ip = self.get_connection_ip(&hostname);
let service_start_command = format!( let service_start_command = format!(
"bash -c 'cat << \"EOF\"\nService Start: {}.service\nTarget: {} ({})\n\nEOF\nssh -tt {}@{} \"sudo systemctl start {}.service && echo \\\"Service started successfully\\\" && sudo systemctl status {}.service --no-pager -l\"\necho\necho \"========================================\"\necho \"Operation completed. Press any key to close...\"\necho \"========================================\"\nread -n 1 -s\nexit'", "bash -c 'cat << \"EOF\"\nService Start: {}.service\nTarget: {} ({})\n\nEOF\nssh -tt {}@{} \"echo \\\"Starting service...\\\" && sudo systemctl start {}.service && echo \\\"Service started! Following logs (Ctrl+C to stop):\\\" && echo \\\"========================================\\\" && sudo journalctl -u {}.service -f --no-pager -n 20\"\necho\necho \"========================================\"\necho \"Operation completed. Press any key to close...\"\necho \"========================================\"\nread -n 1 -s\nexit'",
service_name, service_name,
hostname, hostname,
connection_ip, connection_ip,
@ -325,18 +297,16 @@ impl TuiApp {
KeyCode::Char('S') => { KeyCode::Char('S') => {
// Service stop command via SSH with progress display // Service stop command via SSH with progress display
if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) { if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) {
// Start transition tracking for visual feedback
self.start_command(&hostname, CommandType::ServiceStop, service_name.clone());
let connection_ip = self.get_connection_ip(&hostname); let connection_ip = self.get_connection_ip(&hostname);
let service_stop_command = format!( let service_stop_command = format!(
"bash -c 'cat << \"EOF\"\nService Stop: {}.service\nTarget: {} ({})\n\nEOF\nssh -tt {}@{} \"sudo systemctl stop {}.service && echo \\\"Service stopped successfully\\\" && sudo systemctl status {}.service --no-pager -l\"\necho\necho \"========================================\"\necho \"Operation completed. Press any key to close...\"\necho \"========================================\"\nread -n 1 -s\nexit'", "bash -c 'cat << \"EOF\"\nService Stop: {}.service\nTarget: {} ({})\n\nEOF\nssh -tt {}@{} \"echo \\\"Stopping service...\\\" && sudo systemctl stop {}.service && echo \\\"Service stopped! Final logs:\\\" && echo \\\"========================================\\\" && sudo journalctl -u {}.service --no-pager -n 10 && echo \\\"========================================\\\" && sudo systemctl status {}.service --no-pager -l\"\necho\necho \"========================================\"\necho \"Operation completed. Press any key to close...\"\necho \"========================================\"\nread -n 1 -s\nexit'",
service_name, service_name,
hostname, hostname,
connection_ip, connection_ip,
self.config.ssh.rebuild_user, self.config.ssh.rebuild_user,
connection_ip, connection_ip,
service_name, service_name,
service_name,
service_name service_name
); );
@ -397,13 +367,6 @@ impl TuiApp {
} }
} }
} }
KeyCode::Char('b') => {
// Trigger backup
if let Some(hostname) = self.current_host.clone() {
self.start_command(&hostname, CommandType::BackupTrigger, hostname.clone());
return Ok(Some(UiCommand::TriggerBackup { hostname }));
}
}
KeyCode::Char('w') => { KeyCode::Char('w') => {
// Wake on LAN for offline hosts // Wake on LAN for offline hosts
if let Some(hostname) = self.current_host.clone() { if let Some(hostname) = self.current_host.clone() {
@ -476,7 +439,7 @@ impl TuiApp {
_ => {} _ => {}
} }
} }
Ok(None) Ok(())
} }
/// Navigate between hosts /// Navigate between hosts
@ -530,86 +493,8 @@ impl TuiApp {
self.should_quit self.should_quit
} }
/// Get current service status for state-aware command validation
fn get_current_service_status(&self, hostname: &str, service_name: &str) -> Option<String> {
if let Some(host_widgets) = self.host_widgets.get(hostname) {
return host_widgets.services_widget.get_service_status(service_name);
}
None
}
/// Start command execution with immediate visual feedback
pub fn start_command(&mut self, hostname: &str, command_type: CommandType, target: String) -> bool {
// Get current service status to validate command
let current_status = self.get_current_service_status(hostname, &target);
// Validate if command makes sense for current state
let should_execute = match (&command_type, current_status.as_deref()) {
(CommandType::ServiceStart, Some("inactive") | Some("failed") | Some("dead")) => true,
(CommandType::ServiceStop, Some("active")) => true,
(CommandType::ServiceStart, Some("active")) => {
// Already running - don't execute
false
},
(CommandType::ServiceStop, Some("inactive") | Some("failed") | Some("dead")) => {
// Already stopped - don't execute
false
},
(_, None) => {
// Unknown service state - allow command to proceed
true
},
_ => true, // Default: allow other combinations
};
// ALWAYS store the pending transition for immediate visual feedback, even if we don't execute
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
host_widgets.pending_service_transitions.insert(
target.clone(),
(command_type, current_status.unwrap_or_else(|| "unknown".to_string()), Instant::now())
);
}
should_execute
}
/// Clear pending transitions when real status updates arrive or timeout
fn clear_completed_transitions(&mut self, hostname: &str, service_metrics: &[&Metric]) {
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
let mut completed_services = Vec::new();
// Check each pending transition to see if real status has changed
for (service_name, (command_type, original_status, _start_time)) in &host_widgets.pending_service_transitions {
// Look for status metric for this service
for metric in service_metrics {
if metric.name == format!("service_{}_status", service_name) {
let new_status = metric.value.as_string();
// Check if status has changed from original (command completed)
if &new_status != original_status {
// Verify it changed in the expected direction
let expected_change = match command_type {
CommandType::ServiceStart => &new_status == "active",
CommandType::ServiceStop => &new_status != "active",
_ => false,
};
if expected_change {
completed_services.push(service_name.clone());
}
}
break;
}
}
}
// Remove completed transitions
for service_name in completed_services {
host_widgets.pending_service_transitions.remove(&service_name);
}
}
}
@ -697,14 +582,14 @@ impl TuiApp {
// Render services widget for current host // Render services widget for current host
if let Some(hostname) = self.current_host.clone() { if let Some(hostname) = self.current_host.clone() {
let is_focused = true; // Always show service selection let is_focused = true; // Always show service selection
let (scroll_offset, pending_transitions) = { let scroll_offset = {
let host_widgets = self.get_or_create_host_widgets(&hostname); let host_widgets = self.get_or_create_host_widgets(&hostname);
(host_widgets.services_scroll_offset, host_widgets.pending_service_transitions.clone()) host_widgets.services_scroll_offset
}; };
let host_widgets = self.get_or_create_host_widgets(&hostname); let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets host_widgets
.services_widget .services_widget
.render_with_transitions(frame, content_chunks[1], is_focused, scroll_offset, &pending_transitions); // Services takes full right side .render(frame, content_chunks[1], is_focused, scroll_offset); // Services takes full right side
} }
// Render statusbar at the bottom // Render statusbar at the bottom

View File

@ -9,7 +9,6 @@ use tracing::debug;
use super::Widget; use super::Widget;
use crate::ui::theme::{Components, StatusIcons, Theme, Typography}; use crate::ui::theme::{Components, StatusIcons, Theme, Typography};
use crate::ui::CommandType;
use ratatui::style::Style; use ratatui::style::Style;
/// Services widget displaying hierarchical systemd service statuses /// Services widget displaying hierarchical systemd service statuses
@ -125,20 +124,23 @@ impl ServicesWidget {
) )
} }
/// Get status icon for service, considering pending transitions for visual feedback
fn get_service_icon_and_status(&self, service_name: &str, info: &ServiceInfo, pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>) -> (String, String, ratatui::prelude::Color) {
// Check if this service has a pending transition /// Create spans for sub-service with icon next to name
if let Some((command_type, _original_status, _start_time)) = pending_transitions.get(service_name) { fn create_sub_service_spans(
// Show transitional icons for pending commands &self,
let (icon, status_text) = match command_type { name: &str,
CommandType::ServiceStart => ("", "starting"), info: &ServiceInfo,
CommandType::ServiceStop => ("", "stopping"), is_last: bool,
_ => return (StatusIcons::get_icon(info.widget_status).to_string(), info.status.clone(), Theme::status_color(info.widget_status)), // Not a service command ) -> Vec<ratatui::text::Span<'static>> {
}; // Truncate long sub-service names to fit layout (accounting for indentation)
return (icon.to_string(), status_text.to_string(), Theme::highlight()); let short_name = if name.len() > 18 {
} format!("{}...", &name[..15])
} else {
// Normal status display name.to_string()
};
// Get status icon and text
let icon = StatusIcons::get_icon(info.widget_status); let icon = StatusIcons::get_icon(info.widget_status);
let status_color = match info.widget_status { let status_color = match info.widget_status {
Status::Ok => Theme::success(), Status::Ok => Theme::success(),
@ -148,39 +150,17 @@ impl ServicesWidget {
Status::Unknown => Theme::muted_text(), Status::Unknown => Theme::muted_text(),
Status::Offline => Theme::muted_text(), Status::Offline => Theme::muted_text(),
}; };
(icon.to_string(), info.status.clone(), status_color)
}
// For sub-services, prefer latency if available
/// Create spans for sub-service with icon next to name, considering pending transitions let status_str = if let Some(latency) = info.latency_ms {
fn create_sub_service_spans_with_transitions( if latency < 0.0 {
&self, "timeout".to_string()
name: &str, } else {
info: &ServiceInfo, format!("{:.0}ms", latency)
is_last: bool,
pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>,
) -> Vec<ratatui::text::Span<'static>> {
// Truncate long sub-service names to fit layout (accounting for indentation)
let short_name = if name.len() > 18 {
format!("{}...", &name[..15])
} else {
name.to_string()
};
// Get status icon and text, considering pending transitions
let (icon, mut status_str, status_color) = self.get_service_icon_and_status(name, info, pending_transitions);
// For sub-services, prefer latency if available (unless transition is pending)
if !pending_transitions.contains_key(name) {
if let Some(latency) = info.latency_ms {
status_str = if latency < 0.0 {
"timeout".to_string()
} else {
format!("{:.0}ms", latency)
};
} }
} } else {
info.status.clone()
};
let tree_symbol = if is_last { "└─" } else { "├─" }; let tree_symbol = if is_last { "└─" } else { "├─" };
vec![ vec![
@ -266,25 +246,6 @@ impl ServicesWidget {
self.parent_services.len() self.parent_services.len()
} }
/// Get current status of a specific service by name
pub fn get_service_status(&self, service_name: &str) -> Option<String> {
// Check if it's a parent service
if let Some(parent_info) = self.parent_services.get(service_name) {
return Some(parent_info.status.clone());
}
// Check sub-services (format: parent_sub)
for (parent_name, sub_list) in &self.sub_services {
for (sub_name, sub_info) in sub_list {
let full_sub_name = format!("{}_{}", parent_name, sub_name);
if full_sub_name == service_name {
return Some(sub_info.status.clone());
}
}
}
None
}
/// Calculate which parent service index corresponds to a display line index /// Calculate which parent service index corresponds to a display line index
fn calculate_parent_service_index(&self, display_line_index: &usize) -> usize { fn calculate_parent_service_index(&self, display_line_index: &usize) -> usize {
@ -439,8 +400,8 @@ impl Widget for ServicesWidget {
impl ServicesWidget { impl ServicesWidget {
/// Render with focus, scroll, and pending transitions for visual feedback /// Render with focus and scroll
pub fn render_with_transitions(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>) { pub fn render(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) {
let services_block = Components::widget_block("services"); let services_block = Components::widget_block("services");
let inner_area = services_block.inner(area); let inner_area = services_block.inner(area);
frame.render_widget(services_block, area); frame.render_widget(services_block, area);
@ -465,14 +426,14 @@ impl ServicesWidget {
return; return;
} }
// Use the existing render logic but with pending transitions // Render the services list
self.render_services_with_transitions(frame, content_chunks[1], is_focused, scroll_offset, pending_transitions); self.render_services(frame, content_chunks[1], is_focused, scroll_offset);
} }
/// Render services list with pending transitions awareness /// Render services list
fn render_services_with_transitions(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>) { fn render_services(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) {
// Build hierarchical service list for display - include raw service name for pending transition lookups // Build hierarchical service list for display
let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>, String)> = Vec::new(); // Added raw service name let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new();
// Sort parent services alphabetically for consistent order // Sort parent services alphabetically for consistent order
let mut parent_services: Vec<_> = self.parent_services.iter().collect(); let mut parent_services: Vec<_> = self.parent_services.iter().collect();
@ -481,7 +442,7 @@ impl ServicesWidget {
for (parent_name, parent_info) in parent_services { for (parent_name, parent_info) in parent_services {
// Add parent service line // Add parent service line
let parent_line = self.format_parent_service_line(parent_name, parent_info); let parent_line = self.format_parent_service_line(parent_name, parent_info);
display_lines.push((parent_line, parent_info.widget_status, false, None, parent_name.clone())); // Include raw name display_lines.push((parent_line, parent_info.widget_status, false, None));
// Add sub-services for this parent (if any) // Add sub-services for this parent (if any)
if let Some(sub_list) = self.sub_services.get(parent_name) { if let Some(sub_list) = self.sub_services.get(parent_name) {
@ -491,14 +452,12 @@ impl ServicesWidget {
for (i, (sub_name, sub_info)) in sorted_subs.iter().enumerate() { for (i, (sub_name, sub_info)) in sorted_subs.iter().enumerate() {
let is_last_sub = i == sorted_subs.len() - 1; let is_last_sub = i == sorted_subs.len() - 1;
let full_sub_name = format!("{}_{}", parent_name, sub_name);
// Store sub-service info for custom span rendering // Store sub-service info for custom span rendering
display_lines.push(( display_lines.push((
sub_name.clone(), sub_name.clone(),
sub_info.widget_status, sub_info.widget_status,
true, true,
Some((sub_info.clone(), is_last_sub)), Some((sub_info.clone(), is_last_sub)),
full_sub_name, // Raw service name for pending transition lookup
)); // true = sub-service, with is_last info )); // true = sub-service, with is_last info
} }
} }
@ -531,7 +490,7 @@ impl ServicesWidget {
.constraints(vec![Constraint::Length(1); lines_to_show]) .constraints(vec![Constraint::Length(1); lines_to_show])
.split(area); .split(area);
for (i, (line_text, line_status, is_sub, sub_info, raw_service_name)) in visible_lines.iter().enumerate() for (i, (line_text, line_status, is_sub, sub_info)) in visible_lines.iter().enumerate()
{ {
let actual_index = effective_scroll + i; // Real index in the full list let actual_index = effective_scroll + i; // Real index in the full list
@ -545,41 +504,16 @@ impl ServicesWidget {
}; };
let mut spans = if *is_sub && sub_info.is_some() { let mut spans = if *is_sub && sub_info.is_some() {
// Use custom sub-service span creation WITH pending transitions // Use custom sub-service span creation
let (service_info, is_last) = sub_info.as_ref().unwrap(); let (service_info, is_last) = sub_info.as_ref().unwrap();
self.create_sub_service_spans_with_transitions(line_text, service_info, *is_last, pending_transitions) self.create_sub_service_spans(line_text, service_info, *is_last)
} else { } else {
// Parent services - check if this parent service has a pending transition using RAW service name // Parent services - use normal status spans
if pending_transitions.contains_key(raw_service_name) { StatusIcons::create_status_spans(*line_status, line_text)
// Create spans with transitional status
let (icon, status_text, _) = self.get_service_icon_and_status(raw_service_name, &ServiceInfo {
status: "".to_string(),
memory_mb: None,
disk_gb: None,
latency_ms: None,
widget_status: *line_status
}, pending_transitions);
// Use blue for transitional icons when not selected, background color when selected
let icon_color = if is_selected && !*is_sub && is_focused {
Theme::background() // Dark background color for visibility against blue selection
} else {
Theme::highlight() // Blue for normal case
};
vec![
ratatui::text::Span::styled(format!("{} ", icon), Style::default().fg(icon_color)),
ratatui::text::Span::styled(line_text.clone(), Style::default().fg(Theme::primary_text())),
ratatui::text::Span::styled(format!(" {}", status_text), Style::default().fg(icon_color)),
]
} else {
StatusIcons::create_status_spans(*line_status, line_text)
}
}; };
// Apply selection highlighting to parent services only, making icons background color when selected // Apply selection highlighting to parent services only
// Only show selection when Services panel is focused // Only show selection when Services panel is focused
// Show selection highlighting even when transitional icons are present
if is_selected && !*is_sub && is_focused { if is_selected && !*is_sub && is_focused {
for (i, span) in spans.iter_mut().enumerate() { for (i, span) in spans.iter_mut().enumerate() {
if i == 0 { if i == 0 {

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.74" version = "0.1.75"
edition = "2021" edition = "2021"
[dependencies] [dependencies]