diff --git a/Cargo.lock b/Cargo.lock index 774d34e..e0118b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.78" +version = "0.1.79" dependencies = [ "anyhow", "chrono", @@ -292,7 +292,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.78" +version = "0.1.79" dependencies = [ "anyhow", "async-trait", @@ -315,7 +315,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.78" +version = "0.1.79" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 7c98b9d..f5fc73d 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.79" +version = "0.1.80" edition = "2021" [dependencies] diff --git a/agent/src/collectors/systemd.rs b/agent/src/collectors/systemd.rs index afa8daf..7c912c4 100644 --- a/agent/src/collectors/systemd.rs +++ b/agent/src/collectors/systemd.rs @@ -8,7 +8,6 @@ use tracing::debug; use super::{Collector, CollectorError}; use crate::config::SystemdConfig; -use crate::service_tracker::UserStoppedServiceTracker; /// Systemd collector for monitoring systemd services pub struct SystemdCollector { @@ -357,34 +356,15 @@ impl SystemdCollector { /// Calculate service status, taking user-stopped services into account fn calculate_service_status(&self, service_name: &str, active_status: &str) -> Status { match active_status.to_lowercase().as_str() { - "active" => { - // If service is now active and was marked as user-stopped, clear the flag - if UserStoppedServiceTracker::is_service_user_stopped(service_name) { - debug!("Service '{}' is now active - clearing user-stopped flag", service_name); - // Note: We can't directly clear here because this is a read-only context - // The agent will need to handle this differently - } - Status::Ok - }, + "active" => Status::Ok, "inactive" | "dead" => { - // Check if this service was stopped by user action - if UserStoppedServiceTracker::is_service_user_stopped(service_name) { - debug!("Service '{}' is inactive but marked as user-stopped - treating as OK", service_name); - Status::Ok - } else { - debug!("Service '{}' is inactive - treating as Inactive status", service_name); - Status::Inactive - } + debug!("Service '{}' is inactive - treating as Inactive status", service_name); + Status::Inactive }, "failed" | "error" => Status::Critical, "activating" | "deactivating" | "reloading" | "start" | "stop" | "restart" => { - // For user-stopped services that are transitioning, keep them as OK during transition - if UserStoppedServiceTracker::is_service_user_stopped(service_name) { - debug!("Service '{}' is transitioning but was user-stopped - treating as OK", service_name); - Status::Ok - } else { - Status::Pending - } + debug!("Service '{}' is transitioning - treating as Pending", service_name); + Status::Pending }, _ => Status::Unknown, } diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index 2b42e39..05126b1 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.79" +version = "0.1.80" edition = "2021" [dependencies] diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index 936a8f5..7a55c46 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -34,10 +34,6 @@ pub struct HostWidgets { pub services_widget: ServicesWidget, /// Backup widget state pub backup_widget: BackupWidget, - /// Scroll offsets for each panel - pub system_scroll_offset: usize, - pub services_scroll_offset: usize, - pub backup_scroll_offset: usize, /// Last update time for this host pub last_update: Option, } @@ -48,9 +44,6 @@ impl HostWidgets { system_widget: SystemWidget::new(), services_widget: ServicesWidget::new(), backup_widget: BackupWidget::new(), - system_scroll_offset: 0, - services_scroll_offset: 0, - backup_scroll_offset: 0, last_update: None, } } @@ -274,7 +267,7 @@ impl TuiApp { if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) { let connection_ip = self.get_connection_ip(&hostname); let service_start_command = format!( - "bash -c 'cat << \"EOF\"\nService Start: {}.service\nTarget: {} ({})\n\nEOF\nssh -tt {}@{} \"echo \\\"Starting service...\\\" && sudo systemctl start {}.service && echo \\\"Following logs until service is active...\\\" && echo \\\"========================================\\\" && {{ sudo journalctl -u {}.service -f --no-pager -n 10 & JOURNAL_PID=\\$!; while true; do if sudo systemctl is-active {}.service --quiet; then echo; echo \\\"========================================\\\"; echo \\\"Service is now active!\\\"; kill \\$JOURNAL_PID 2>/dev/null; break; fi; sleep 1; done; wait \\$JOURNAL_PID 2>/dev/null; }} && 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 \\\"Following logs until service is active...\\\" && echo \\\"========================================\\\" && {{ sudo journalctl -fu {} --no-pager & JOURNAL_PID=\\$!; while true; do if sudo systemctl is-active {} --quiet; then echo; echo \\\"========================================\\\"; echo \\\"Service is now active!\\\"; kill \\$JOURNAL_PID 2>/dev/null; break; fi; sleep 1; done; wait \\$JOURNAL_PID 2>/dev/null; }} && 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, hostname, connection_ip, @@ -584,14 +577,10 @@ impl TuiApp { // Render services widget for current host if let Some(hostname) = self.current_host.clone() { let is_focused = true; // Always show service selection - let scroll_offset = { - let host_widgets = self.get_or_create_host_widgets(&hostname); - host_widgets.services_scroll_offset - }; let host_widgets = self.get_or_create_host_widgets(&hostname); host_widgets .services_widget - .render(frame, content_chunks[1], is_focused, scroll_offset); // Services takes full right side + .render(frame, content_chunks[1], is_focused); // Services takes full right side } // Render statusbar at the bottom @@ -775,14 +764,10 @@ impl TuiApp { frame.render_widget(system_block, area); // Get current host widgets, create if none exist if let Some(hostname) = self.current_host.clone() { - let scroll_offset = { - let host_widgets = self.get_or_create_host_widgets(&hostname); - host_widgets.system_scroll_offset - }; // Clone the config to avoid borrowing issues let config = self.config.clone(); let host_widgets = self.get_or_create_host_widgets(&hostname); - host_widgets.system_widget.render_with_scroll(frame, inner_area, scroll_offset, &hostname, Some(&config)); + host_widgets.system_widget.render(frame, inner_area, &hostname, Some(&config)); } } @@ -793,12 +778,8 @@ impl TuiApp { // Get current host widgets for backup widget if let Some(hostname) = self.current_host.clone() { - let scroll_offset = { - let host_widgets = self.get_or_create_host_widgets(&hostname); - host_widgets.backup_scroll_offset - }; let host_widgets = self.get_or_create_host_widgets(&hostname); - host_widgets.backup_widget.render_with_scroll(frame, inner_area, scroll_offset); + host_widgets.backup_widget.render(frame, inner_area); } } diff --git a/dashboard/src/ui/widgets/backup.rs b/dashboard/src/ui/widgets/backup.rs index 4559b96..e50fa06 100644 --- a/dashboard/src/ui/widgets/backup.rs +++ b/dashboard/src/ui/widgets/backup.rs @@ -285,8 +285,8 @@ impl Widget for BackupWidget { } impl BackupWidget { - /// Render with scroll offset support - pub fn render_with_scroll(&mut self, frame: &mut Frame, area: Rect, scroll_offset: usize) { + /// Render backup widget + pub fn render(&mut self, frame: &mut Frame, area: Rect) { let mut lines = Vec::new(); // Latest backup section @@ -366,42 +366,20 @@ impl BackupWidget { let total_lines = lines.len(); let available_height = area.height as usize; - // Calculate scroll boundaries - let max_scroll = if total_lines > available_height { - total_lines - available_height - } else { - total_lines.saturating_sub(1) - }; - let effective_scroll = scroll_offset.min(max_scroll); - - // Apply scrolling if needed - if scroll_offset > 0 || total_lines > available_height { + // Show only what fits, with "X more below" if needed + if total_lines > available_height { + let lines_for_content = available_height.saturating_sub(1); // Reserve one line for "more below" let mut visible_lines: Vec<_> = lines .into_iter() - .skip(effective_scroll) - .take(available_height) + .take(lines_for_content) .collect(); - // Add scroll indicator if there are hidden lines - if total_lines > available_height { - let hidden_above = effective_scroll; - let hidden_below = total_lines.saturating_sub(effective_scroll + available_height); - - if (hidden_above > 0 || hidden_below > 0) && !visible_lines.is_empty() { - let scroll_text = if hidden_above > 0 && hidden_below > 0 { - format!("... {} above, {} below", hidden_above, hidden_below) - } else if hidden_above > 0 { - format!("... {} more above", hidden_above) - } else { - format!("... {} more below", hidden_below) - }; - - // Replace last line with scroll indicator - visible_lines.pop(); - visible_lines.push(ratatui::text::Line::from(vec![ - ratatui::text::Span::styled(scroll_text, Typography::muted()) - ])); - } + let hidden_below = total_lines.saturating_sub(lines_for_content); + if hidden_below > 0 { + let more_line = ratatui::text::Line::from(vec![ + ratatui::text::Span::styled(format!("... {} more below", hidden_below), Typography::muted()) + ]); + visible_lines.push(more_line); } let paragraph = Paragraph::new(ratatui::text::Text::from(visible_lines)); diff --git a/dashboard/src/ui/widgets/services.rs b/dashboard/src/ui/widgets/services.rs index f68dfe1..3bab658 100644 --- a/dashboard/src/ui/widgets/services.rs +++ b/dashboard/src/ui/widgets/services.rs @@ -401,8 +401,8 @@ impl Widget for ServicesWidget { impl ServicesWidget { - /// Render with focus and scroll - pub fn render(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) { + /// Render with focus + pub fn render(&mut self, frame: &mut Frame, area: Rect, is_focused: bool) { let services_block = Components::widget_block("services"); let inner_area = services_block.inner(area); frame.render_widget(services_block, area); @@ -428,11 +428,11 @@ impl ServicesWidget { } // Render the services list - self.render_services(frame, content_chunks[1], is_focused, scroll_offset); + self.render_services(frame, content_chunks[1], is_focused); } /// Render services list - fn render_services(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) { + fn render_services(&mut self, frame: &mut Frame, area: Rect, is_focused: bool) { // Build hierarchical service list for display let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new(); @@ -464,36 +464,37 @@ impl ServicesWidget { } } - // Apply scroll offset and render visible lines (same as existing logic) + // Show only what fits, with "X more below" if needed let available_lines = area.height as usize; let total_lines = display_lines.len(); - // Calculate scroll boundaries - let max_scroll = if total_lines > available_lines { - total_lines - available_lines + // Reserve one line for "X more below" if needed + let lines_for_content = if total_lines > available_lines { + available_lines.saturating_sub(1) } else { - total_lines.saturating_sub(1) + available_lines }; - let effective_scroll = scroll_offset.min(max_scroll); - // Get visible lines after scrolling let visible_lines: Vec<_> = display_lines .iter() - .skip(effective_scroll) - .take(available_lines) + .take(lines_for_content) .collect(); + let hidden_below = total_lines.saturating_sub(lines_for_content); + let lines_to_show = visible_lines.len(); if lines_to_show > 0 { + // Add space for "X more below" message if needed + let total_chunks_needed = if hidden_below > 0 { lines_to_show + 1 } else { lines_to_show }; let service_chunks = Layout::default() .direction(Direction::Vertical) - .constraints(vec![Constraint::Length(1); lines_to_show]) + .constraints(vec![Constraint::Length(1); total_chunks_needed]) .split(area); 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 = i; // Simple index since we're not scrolling // Only parent services can be selected - calculate parent service index let is_selected = if !*is_sub { @@ -535,33 +536,12 @@ impl ServicesWidget { frame.render_widget(service_para, service_chunks[i]); } - } - - // Show scroll indicator if there are more services than we can display (same as existing) - if total_lines > available_lines { - let hidden_above = effective_scroll; - let hidden_below = total_lines.saturating_sub(effective_scroll + available_lines); - if hidden_above > 0 || hidden_below > 0 { - let scroll_text = if hidden_above > 0 && hidden_below > 0 { - format!("... {} above, {} below", hidden_above, hidden_below) - } else if hidden_above > 0 { - format!("... {} more above", hidden_above) - } else { - format!("... {} more below", hidden_below) - }; - - if available_lines > 0 && lines_to_show > 0 { - let last_line_area = Rect { - x: area.x, - y: area.y + (lines_to_show - 1) as u16, - width: area.width, - height: 1, - }; - - let scroll_para = Paragraph::new(scroll_text).style(Typography::muted()); - frame.render_widget(scroll_para, last_line_area); - } + // Show "X more below" message if content was truncated + if hidden_below > 0 { + let more_text = format!("... {} more below", hidden_below); + let more_para = Paragraph::new(more_text).style(Typography::muted()); + frame.render_widget(more_para, service_chunks[lines_to_show]); } } } diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index a10bf8d..876c383 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -438,8 +438,8 @@ impl Widget for SystemWidget { } impl SystemWidget { - /// Render with scroll offset support - pub fn render_with_scroll(&mut self, frame: &mut Frame, area: Rect, scroll_offset: usize, hostname: &str, config: Option<&crate::config::DashboardConfig>) { + /// Render system widget + pub fn render(&mut self, frame: &mut Frame, area: Rect, hostname: &str, config: Option<&crate::config::DashboardConfig>) { let mut lines = Vec::new(); // NixOS section @@ -560,22 +560,22 @@ impl SystemWidget { let total_lines = lines.len(); let available_height = area.height as usize; - // Always apply scrolling if scroll_offset > 0, even if content fits - if scroll_offset > 0 || total_lines > available_height { - let max_scroll = if total_lines > available_height { - total_lines - available_height - } else { - total_lines.saturating_sub(1) - }; - let effective_scroll = scroll_offset.min(max_scroll); - - // Take only the visible portion after scrolling - let visible_lines: Vec = lines + // Show only what fits, with "X more below" if needed + if total_lines > available_height { + let lines_for_content = available_height.saturating_sub(1); // Reserve one line for "more below" + let mut visible_lines: Vec = lines .into_iter() - .skip(effective_scroll) - .take(available_height) + .take(lines_for_content) .collect(); - + + let hidden_below = total_lines.saturating_sub(lines_for_content); + if hidden_below > 0 { + let more_line = Line::from(vec![ + Span::styled(format!("... {} more below", hidden_below), Typography::muted()) + ]); + visible_lines.push(more_line); + } + let paragraph = Paragraph::new(Text::from(visible_lines)); frame.render_widget(paragraph, area); } else { diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 9cdf222..03e9482 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.79" +version = "0.1.80" edition = "2021" [dependencies]