Compare commits

..

5 Commits

Author SHA1 Message Date
c3fc5a181d Fix service name mismatch in pending transitions lookup
All checks were successful
Build and Release / build-and-release (push) Successful in 1m12s
The root cause of transitional service icons not showing was that service names
were stored as raw names (e.g., "sshd") in pending_transitions but looked up
against formatted display lines (e.g., "sshd                    active     1M     ").

Changes:
- Modified display_lines structure to include both formatted text and raw service names
- Updated rendering loop to use raw service names for pending transition lookups
- Fixed get_selected_service() method to use the new tuple structure
- Transitional icons (↑ ↓ ↻) should now appear correctly when pressing s/S/r keys
2025-10-28 15:00:48 +01:00
3f45a172b3 Add debug rendering to test transitional icon visibility
All checks were successful
Build and Release / build-and-release (push) Successful in 1m14s
- Force sshd service to always show "↑ starting" for debugging
- Test if basic directional arrow rendering works in services widget
- Temporary debug change to isolate rendering vs logic issues
- Will help determine if problem is in pending transitions or rendering

If arrow appears: pending transitions logic issue
If no arrow: basic rendering path issue
2025-10-28 14:49:24 +01:00
5b12c12228 Fix transitional icons by always storing pending transitions for visual feedback
All checks were successful
Build and Release / build-and-release (push) Successful in 1m13s
- Store pending transitions even for redundant commands (start active service)
- Add 3-second timeout for redundant command visual feedback
- Include timestamp in pending transitions to enable timeout clearing
- Show directional arrows immediately regardless of command validation result
- Fix core issue where state validation prevented visual feedback storage

Now pressing s/S/r always shows immediate directional arrows, even for
redundant operations, providing consistent visual feedback to users.
2025-10-28 14:38:33 +01:00
651b801de3 Fix transitional service icons being overridden by selection highlighting
All checks were successful
Build and Release / build-and-release (push) Successful in 1m14s
- Prevent selection highlighting when service has pending transition
- Allow directional arrows (↑ ↓ ↻) to show through on selected services
- Fix core issue where selection styling was overwriting transitional icons
- Transitional icons now properly visible during service command execution

The selection highlighting logic now skips services with pending transitions,
ensuring that directional arrows are visible when executing service commands.
2025-10-28 14:22:40 +01:00
71b9f93d7c Implement immediate transitional service icons with pending state tracking
All checks were successful
Build and Release / build-and-release (push) Successful in 2m8s
- Replace timeout-based command status with pending service transitions
- Show immediate directional arrows when pressing service commands (↑ ↓ ↻)
- Track original service status and command type for each pending operation
- Automatically clear transitional icons when real status updates arrive
- Remove unused TerminalPopup and CommandStatus infrastructure
- Simplify visual feedback system using state-based approach

Service commands now provide instant visual feedback that persists until
the actual service state changes, eliminating timing issues and improving UX.
2025-10-28 14:11:59 +01:00
7 changed files with 125 additions and 122 deletions

6
Cargo.lock generated
View File

@@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.22" version = "0.1.27"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -291,7 +291,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.22" version = "0.1.27"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -314,7 +314,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.22" version = "0.1.27"
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.23" version = "0.1.28"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

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

View File

@@ -14,7 +14,7 @@ use app::Dashboard;
/// Get hardcoded version /// Get hardcoded version
fn get_version() -> &'static str { fn get_version() -> &'static str {
"v0.1.23" "v0.1.28"
} }
/// Check if running inside tmux session /// Check if running inside tmux session

View File

@@ -7,7 +7,7 @@ use ratatui::{
Frame, Frame,
}; };
use std::collections::HashMap; use std::collections::HashMap;
use std::time::{Duration, Instant}; use std::time::Instant;
use tracing::info; use tracing::info;
pub mod theme; pub mod theme;
@@ -28,12 +28,6 @@ pub enum UiCommand {
TriggerBackup { hostname: String }, TriggerBackup { hostname: String },
} }
/// Command execution status for visual feedback
#[derive(Debug, Clone)]
pub enum CommandStatus {
/// Command is executing
InProgress { command_type: CommandType, target: String, start_time: std::time::Instant },
}
/// Types of commands for status tracking /// Types of commands for status tracking
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -70,8 +64,8 @@ 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>,
/// Active command status for visual feedback /// Pending service transitions for immediate visual feedback
pub command_status: Option<CommandStatus>, pub pending_service_transitions: HashMap<String, (CommandType, String, Instant)>, // service_name -> (command_type, original_status, start_time)
} }
impl HostWidgets { impl HostWidgets {
@@ -84,7 +78,7 @@ impl HostWidgets {
services_scroll_offset: 0, services_scroll_offset: 0,
backup_scroll_offset: 0, backup_scroll_offset: 0,
last_update: None, last_update: None,
command_status: None, pending_service_transitions: HashMap::new(),
} }
} }
} }
@@ -133,8 +127,6 @@ impl TuiApp {
/// Update widgets with metrics from store (only for current host) /// Update widgets with metrics from store (only for current host)
pub fn update_metrics(&mut self, metric_store: &MetricStore) { pub fn update_metrics(&mut self, metric_store: &MetricStore) {
// Check for command timeouts first
self.check_command_timeouts();
// Check for rebuild completion by agent hash change // Check for rebuild completion by agent hash change
@@ -168,6 +160,9 @@ impl TuiApp {
.copied() .copied()
.collect(); .collect();
// 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);
@@ -209,9 +204,9 @@ impl TuiApp {
// Sort hosts alphabetically // Sort hosts alphabetically
let mut sorted_hosts = hosts.clone(); let mut sorted_hosts = hosts.clone();
// Keep hosts that have ongoing commands even if they're offline // Keep hosts that have pending transitions even if they're offline
for (hostname, host_widgets) in &self.host_widgets { for (hostname, host_widgets) in &self.host_widgets {
if let Some(CommandStatus::InProgress { .. }) = &host_widgets.command_status { if !host_widgets.pending_service_transitions.is_empty() {
if !sorted_hosts.contains(hostname) { if !sorted_hosts.contains(hostname) {
sorted_hosts.push(hostname.clone()); sorted_hosts.push(hostname.clone());
} }
@@ -427,7 +422,7 @@ impl TuiApp {
None None
} }
/// Start command execution and track status for visual feedback (with state validation) /// Start command execution with immediate visual feedback
pub fn start_command(&mut self, hostname: &str, command_type: CommandType, target: String) -> bool { pub fn start_command(&mut self, hostname: &str, command_type: CommandType, target: String) -> bool {
// Get current service status to validate command // Get current service status to validate command
let current_status = self.get_current_service_status(hostname, &target); let current_status = self.get_current_service_status(hostname, &target);
@@ -438,13 +433,11 @@ impl TuiApp {
(CommandType::ServiceStop, Some("active")) => true, (CommandType::ServiceStop, Some("active")) => true,
(CommandType::ServiceRestart, Some("active") | Some("inactive") | Some("failed") | Some("dead")) => true, (CommandType::ServiceRestart, Some("active") | Some("inactive") | Some("failed") | Some("dead")) => true,
(CommandType::ServiceStart, Some("active")) => { (CommandType::ServiceStart, Some("active")) => {
// Already running - show brief feedback but don't execute // Already running - don't execute
// TODO: Could show a brief "already running" message
false false
}, },
(CommandType::ServiceStop, Some("inactive") | Some("failed") | Some("dead")) => { (CommandType::ServiceStop, Some("inactive") | Some("failed") | Some("dead")) => {
// Already stopped - show brief feedback but don't execute // Already stopped - don't execute
// TODO: Could show a brief "already stopped" message
false false
}, },
(_, None) => { (_, None) => {
@@ -454,38 +447,58 @@ impl TuiApp {
_ => true, // Default: allow other combinations _ => true, // Default: allow other combinations
}; };
if should_execute { // 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) { if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
host_widgets.command_status = Some(CommandStatus::InProgress { host_widgets.pending_service_transitions.insert(
command_type, target.clone(),
target, (command_type, current_status.unwrap_or_else(|| "unknown".to_string()), Instant::now())
start_time: Instant::now(), );
});
}
} }
should_execute should_execute
} }
/// Check for command timeouts and automatically clear them /// Clear pending transitions when real status updates arrive or timeout
pub fn check_command_timeouts(&mut self) { 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();
let now = Instant::now(); let now = Instant::now();
let mut hosts_to_clear = Vec::new();
for (hostname, host_widgets) in &self.host_widgets { // Check each pending transition to see if real status has changed or timed out
if let Some(CommandStatus::InProgress { command_type: _, start_time, .. }) = &host_widgets.command_status { for (service_name, (command_type, original_status, start_time)) in &host_widgets.pending_service_transitions {
let timeout_duration = Duration::from_secs(30); // 30 seconds for service commands // Clear if too much time has passed (3 seconds for redundant commands)
if now.duration_since(*start_time).as_secs() > 3 {
completed_services.push(service_name.clone());
continue;
}
if now.duration_since(*start_time) > timeout_duration { // Look for status metric for this service
hosts_to_clear.push(hostname.clone()); 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",
CommandType::ServiceRestart => true, // Any change indicates restart completed
_ => false,
};
if expected_change {
completed_services.push(service_name.clone());
}
}
break;
} }
} }
} }
// Clear timed out commands // Remove completed transitions
for hostname in hosts_to_clear { for service_name in completed_services {
if let Some(host_widgets) = self.host_widgets.get_mut(&hostname) { host_widgets.pending_service_transitions.remove(&service_name);
host_widgets.command_status = None;
} }
} }
} }
@@ -614,14 +627,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 = self.focused_panel == PanelType::Services; let is_focused = self.focused_panel == PanelType::Services;
let (scroll_offset, command_status) = { let (scroll_offset, pending_transitions) = {
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.command_status.clone()) (host_widgets.services_scroll_offset, host_widgets.pending_service_transitions.clone())
}; };
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_command_status(frame, content_chunks[1], is_focused, scroll_offset, command_status.as_ref()); // Services takes full right side .render_with_transitions(frame, content_chunks[1], is_focused, scroll_offset, &pending_transitions); // Services takes full right side
} }
// Render statusbar at the bottom // Render statusbar at the bottom

View File

@@ -9,7 +9,7 @@ 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::{CommandStatus, CommandType}; 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
@@ -128,26 +128,19 @@ impl ServicesWidget {
) )
} }
/// Get status icon for service, considering command status for visual feedback /// Get status icon for service, considering pending transitions for visual feedback
fn get_service_icon_and_status(&self, service_name: &str, info: &ServiceInfo, command_status: Option<&CommandStatus>) -> (String, String, ratatui::prelude::Color) { 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 is currently being operated on // Check if this service has a pending transition
if let Some(status) = command_status { if let Some((command_type, _original_status, _start_time)) = pending_transitions.get(service_name) {
match status { // Show transitional icons for pending commands
CommandStatus::InProgress { command_type, target, .. } => { let (icon, status_text) = match command_type {
if target == service_name { CommandType::ServiceRestart => ("", "restarting"),
// Only show special icons for service commands CommandType::ServiceStart => ("", "starting"),
if let Some((icon, status_text)) = match command_type { CommandType::ServiceStop => ("", "stopping"),
CommandType::ServiceRestart => Some(("", "restarting")), _ => return (StatusIcons::get_icon(info.widget_status).to_string(), info.status.clone(), Theme::status_color(info.widget_status)), // Not a service command
CommandType::ServiceStart => Some(("", "starting")), };
CommandType::ServiceStop => Some(("", "stopping")),
_ => None, // Don't handle non-service commands here
} {
return (icon.to_string(), status_text.to_string(), Theme::highlight()); return (icon.to_string(), status_text.to_string(), Theme::highlight());
} }
}
}
}
}
// Normal status display // Normal status display
let icon = StatusIcons::get_icon(info.widget_status); let icon = StatusIcons::get_icon(info.widget_status);
@@ -163,13 +156,13 @@ impl ServicesWidget {
} }
/// Create spans for sub-service with icon next to name, considering command status /// Create spans for sub-service with icon next to name, considering pending transitions
fn create_sub_service_spans_with_status( fn create_sub_service_spans_with_transitions(
&self, &self,
name: &str, name: &str,
info: &ServiceInfo, info: &ServiceInfo,
is_last: bool, is_last: bool,
command_status: Option<&CommandStatus>, pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>,
) -> Vec<ratatui::text::Span<'static>> { ) -> Vec<ratatui::text::Span<'static>> {
// Truncate long sub-service names to fit layout (accounting for indentation) // Truncate long sub-service names to fit layout (accounting for indentation)
let short_name = if name.len() > 18 { let short_name = if name.len() > 18 {
@@ -178,11 +171,11 @@ impl ServicesWidget {
name.to_string() name.to_string()
}; };
// Get status icon and text, considering command status // Get status icon and text, considering pending transitions
let (icon, mut status_str, status_color) = self.get_service_icon_and_status(name, info, command_status); 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 command is in progress) // For sub-services, prefer latency if available (unless transition is pending)
if command_status.is_none() { if !pending_transitions.contains_key(name) {
if let Some(latency) = info.latency_ms { if let Some(latency) = info.latency_ms {
status_str = if latency < 0.0 { status_str = if latency < 0.0 {
"timeout".to_string() "timeout".to_string()
@@ -240,13 +233,14 @@ impl ServicesWidget {
/// Get currently selected service name (for actions) /// Get currently selected service name (for actions)
pub fn get_selected_service(&self) -> Option<String> { pub fn get_selected_service(&self) -> Option<String> {
// Build the same display list to find the selected service // Build the same display list to find the selected service
let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new(); let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>, String)> = Vec::new();
let mut parent_services: Vec<_> = self.parent_services.iter().collect(); let mut parent_services: Vec<_> = self.parent_services.iter().collect();
parent_services.sort_by(|(a, _), (b, _)| a.cmp(b)); parent_services.sort_by(|(a, _), (b, _)| a.cmp(b));
for (parent_name, parent_info) in parent_services { for (parent_name, parent_info) in parent_services {
display_lines.push((parent_name.clone(), parent_info.widget_status, false, None)); 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()));
if let Some(sub_list) = self.sub_services.get(parent_name) { if let Some(sub_list) = self.sub_services.get(parent_name) {
let mut sorted_subs = sub_list.clone(); let mut sorted_subs = sub_list.clone();
@@ -254,17 +248,19 @@ 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);
display_lines.push(( display_lines.push((
format!("{}_{}", parent_name, sub_name), // Use parent_sub format for sub-services 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,
)); ));
} }
} }
} }
display_lines.get(self.selected_index).map(|(name, _, _, _)| name.clone()) display_lines.get(self.selected_index).map(|(_, _, _, _, raw_name)| raw_name.clone())
} }
/// Get total count of selectable services (parent services only, not sub-services) /// Get total count of selectable services (parent services only, not sub-services)
@@ -446,8 +442,8 @@ impl Widget for ServicesWidget {
impl ServicesWidget { impl ServicesWidget {
/// Render with focus, scroll, and command status for visual feedback /// Render with focus, scroll, and pending transitions for visual feedback
pub fn render_with_command_status(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, command_status: Option<&CommandStatus>) { 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)>) {
let services_block = if is_focused { let services_block = if is_focused {
Components::focused_widget_block("services") Components::focused_widget_block("services")
} else { } else {
@@ -476,14 +472,14 @@ impl ServicesWidget {
return; return;
} }
// Use the existing render logic but with command status // Use the existing render logic but with pending transitions
self.render_services_with_status(frame, content_chunks[1], is_focused, scroll_offset, command_status); self.render_services_with_transitions(frame, content_chunks[1], is_focused, scroll_offset, pending_transitions);
} }
/// Render services list with command status awareness /// Render services list with pending transitions awareness
fn render_services_with_status(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, command_status: Option<&CommandStatus>) { 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)>) {
// Build hierarchical service list for display (same as existing logic) // Build hierarchical service list for display - include raw service name for pending transition lookups
let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new(); let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>, String)> = Vec::new(); // Added raw service name
// 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();
@@ -492,7 +488,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)); // false = not sub-service display_lines.push((parent_line, parent_info.widget_status, false, None, parent_name.clone())); // Include raw name
// 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) {
@@ -502,12 +498,14 @@ 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
} }
} }
@@ -540,7 +538,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)) in visible_lines.iter().enumerate() for (i, (line_text, line_status, is_sub, sub_info, raw_service_name)) 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
@@ -554,23 +552,20 @@ 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 command status // Use custom sub-service span creation WITH pending transitions
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_status(line_text, service_info, *is_last, command_status) self.create_sub_service_spans_with_transitions(line_text, service_info, *is_last, pending_transitions)
} else { } else {
// Parent services - check if this parent service has a command in progress // Parent services - check if this parent service has a pending transition using RAW service name
let service_spans = if let Some(status) = command_status { if pending_transitions.contains_key(raw_service_name) {
match status { // Create spans with transitional status
CommandStatus::InProgress { target, .. } => { let (icon, status_text, status_color) = self.get_service_icon_and_status(raw_service_name, &ServiceInfo {
if target == line_text {
// Create spans with progress status
let (icon, status_text, status_color) = self.get_service_icon_and_status(line_text, &ServiceInfo {
status: "".to_string(), status: "".to_string(),
memory_mb: None, memory_mb: None,
disk_gb: None, disk_gb: None,
latency_ms: None, latency_ms: None,
widget_status: *line_status widget_status: *line_status
}, command_status); }, pending_transitions);
vec![ vec![
ratatui::text::Span::styled(format!("{} ", icon), Style::default().fg(status_color)), ratatui::text::Span::styled(format!("{} ", icon), Style::default().fg(status_color)),
ratatui::text::Span::styled(line_text.clone(), Style::default().fg(Theme::primary_text())), ratatui::text::Span::styled(line_text.clone(), Style::default().fg(Theme::primary_text())),
@@ -579,17 +574,12 @@ impl ServicesWidget {
} else { } else {
StatusIcons::create_status_spans(*line_status, line_text) StatusIcons::create_status_spans(*line_status, line_text)
} }
}
}
} else {
StatusIcons::create_status_spans(*line_status, line_text)
};
service_spans
}; };
// Apply selection highlighting to parent services only, preserving status icon color // Apply selection highlighting to parent services only, preserving status icon color
// Only show selection when Services panel is focused // Only show selection when Services panel is focused
if is_selected && !*is_sub && is_focused { // IMPORTANT: Don't override transitional icons that show pending commands
if is_selected && !*is_sub && is_focused && !pending_transitions.contains_key(raw_service_name) {
for (i, span) in spans.iter_mut().enumerate() { for (i, span) in spans.iter_mut().enumerate() {
if i == 0 { if i == 0 {
// First span is the status icon - preserve its color // First span is the status icon - preserve its color

View File

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