diff --git a/Cargo.lock b/Cargo.lock index bbb43a3..314f25a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.260" +version = "0.1.261" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.260" +version = "0.1.261" dependencies = [ "anyhow", "async-trait", @@ -325,7 +325,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.260" +version = "0.1.261" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 8f1a812..76d1d97 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.261" +version = "0.1.262" edition = "2021" [dependencies] diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index b069bcb..f03b199 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.261" +version = "0.1.262" edition = "2021" [dependencies] diff --git a/dashboard/src/metrics/store.rs b/dashboard/src/metrics/store.rs index 159f58e..41e50f9 100644 --- a/dashboard/src/metrics/store.rs +++ b/dashboard/src/metrics/store.rs @@ -86,16 +86,6 @@ impl MetricStore { self.current_agent_data.get(hostname) } - /// Get ZMQ communication statistics for a host - pub fn get_zmq_stats(&mut self, hostname: &str) -> Option { - let now = Instant::now(); - self.zmq_stats.get_mut(hostname).map(|stats| { - // Update packet age - stats.last_packet_age_secs = now.duration_since(stats.last_packet_time).as_secs_f64(); - stats.clone() - }) - } - /// Get connected hosts (hosts with recent heartbeats) pub fn get_connected_hosts(&self, timeout: Duration) -> Vec { let now = Instant::now(); diff --git a/dashboard/src/ui/widgets/services.rs b/dashboard/src/ui/widgets/services.rs index 061b4a6..9bcfc95 100644 --- a/dashboard/src/ui/widgets/services.rs +++ b/dashboard/src/ui/widgets/services.rs @@ -102,7 +102,6 @@ pub struct ServicesWidget { struct ServiceInfo { metrics: Vec<(String, f32, Option)>, // (label, value, unit) widget_status: Status, - service_type: String, // "nginx_site", "container", "image", or empty for parent services memory_bytes: Option, restart_count: Option, uptime_seconds: Option, @@ -344,18 +343,86 @@ impl ServicesWidget { pub fn select_previous(&mut self) { if self.selected_index > 0 { self.selected_index -= 1; + self.ensure_selected_visible(); } debug!("Service selection moved up to: {}", self.selected_index); } - /// Move selection down + /// Move selection down pub fn select_next(&mut self, total_services: usize) { if total_services > 0 && self.selected_index < total_services.saturating_sub(1) { self.selected_index += 1; + self.ensure_selected_visible(); } debug!("Service selection: {}/{}", self.selected_index, total_services); } + /// Convert parent service index to display line index + fn parent_index_to_display_line(&self, parent_index: usize) -> usize { + let mut parent_services: Vec<_> = self.parent_services.iter().collect(); + parent_services.sort_by(|(a, _), (b, _)| a.cmp(b)); + + let mut display_line = 0; + for (idx, (parent_name, _)) in parent_services.iter().enumerate() { + if idx == parent_index { + return display_line; + } + display_line += 1; // Parent service line + + // Add sub-service lines + if let Some(sub_list) = self.sub_services.get(*parent_name) { + display_line += sub_list.len(); + } + } + display_line + } + + /// Ensure the currently selected service is visible in the viewport + fn ensure_selected_visible(&mut self) { + if self.last_viewport_height == 0 { + return; // Can't adjust without knowing viewport size + } + + let display_line = self.parent_index_to_display_line(self.selected_index); + let total_display_lines = self.get_total_display_lines(); + let viewport_height = self.last_viewport_height; + + // Check if selected line is above visible area + if display_line < self.scroll_offset { + self.scroll_offset = display_line; + return; + } + + // Calculate current effective viewport (accounting for "more below" if present) + let current_remaining = total_display_lines.saturating_sub(self.scroll_offset); + let current_has_more = current_remaining > viewport_height; + let current_effective = if current_has_more { + viewport_height.saturating_sub(1) + } else { + viewport_height + }; + + // Check if selected line is below current visible area + if display_line >= self.scroll_offset + current_effective { + // Need to scroll down. Position selected line so there's room for "more below" if needed + // Strategy: if there are lines below the selected line, don't put it at the very bottom + let has_content_below = display_line < total_display_lines - 1; + + if has_content_below { + // Leave room for "... X more below" message by positioning selected line + // one position higher than the last line + let target_position = viewport_height.saturating_sub(2); + self.scroll_offset = display_line.saturating_sub(target_position); + } else { + // This is the last line, can put it at the bottom + self.scroll_offset = display_line.saturating_sub(viewport_height - 1); + } + } + + debug!("Auto-scroll: selected={}, display_line={}, scroll_offset={}, viewport={}, total={}", + self.selected_index, display_line, self.scroll_offset, viewport_height, total_display_lines); + } + /// Get currently selected service name (for actions) /// Only returns parent service names since only parent services can be selected pub fn get_selected_service(&self) -> Option { @@ -488,7 +555,6 @@ impl Widget for ServicesWidget { let parent_info = ServiceInfo { metrics: Vec::new(), // Parent services don't have custom metrics widget_status: service.service_status, - service_type: String::new(), // Parent services have no type memory_bytes: service.memory_bytes, restart_count: service.restart_count, uptime_seconds: service.uptime_seconds, @@ -507,7 +573,6 @@ impl Widget for ServicesWidget { let sub_info = ServiceInfo { metrics, widget_status: sub_service.service_status, - service_type: sub_service.service_type.clone(), memory_bytes: None, // Sub-services don't have individual metrics yet restart_count: None, uptime_seconds: None, @@ -552,7 +617,6 @@ impl ServicesWidget { .or_insert(ServiceInfo { metrics: Vec::new(), widget_status: Status::Unknown, - service_type: String::new(), memory_bytes: None, restart_count: None, uptime_seconds: None, @@ -581,7 +645,6 @@ impl ServicesWidget { ServiceInfo { metrics: Vec::new(), widget_status: Status::Unknown, - service_type: String::new(), // Unknown type in legacy path memory_bytes: None, restart_count: None, uptime_seconds: None, diff --git a/shared/Cargo.toml b/shared/Cargo.toml index b2293e7..1f9e531 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.261" +version = "0.1.262" edition = "2021" [dependencies]