Remove all scrolling code and user-stopped tracking logic
All checks were successful
Build and Release / build-and-release (push) Successful in 2m36s

- Remove scroll offset fields from HostWidgets struct
- Replace scrolling with simple "X more below" indicators in all widgets
- Remove user-stopped service tracking from agent (now uses SSH control)
- Inactive services now consistently show Status::Inactive with empty circles
- Simplify widget render methods by removing scroll parameters
- Clean up unused imports and legacy scrolling infrastructure
- Fix journalctl command to use -fu for proper log following
This commit is contained in:
Christoffer Martinsson 2025-11-19 08:32:42 +01:00
parent 9575077045
commit c0f7a97a6f
9 changed files with 64 additions and 145 deletions

6
Cargo.lock generated
View File

@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.78" version = "0.1.79"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -292,7 +292,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.78" version = "0.1.79"
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.78" version = "0.1.79"
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.79" version = "0.1.80"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -8,7 +8,6 @@ use tracing::debug;
use super::{Collector, CollectorError}; use super::{Collector, CollectorError};
use crate::config::SystemdConfig; use crate::config::SystemdConfig;
use crate::service_tracker::UserStoppedServiceTracker;
/// Systemd collector for monitoring systemd services /// Systemd collector for monitoring systemd services
pub struct SystemdCollector { pub struct SystemdCollector {
@ -357,34 +356,15 @@ impl SystemdCollector {
/// Calculate service status, taking user-stopped services into account /// Calculate service status, taking user-stopped services into account
fn calculate_service_status(&self, service_name: &str, active_status: &str) -> Status { fn calculate_service_status(&self, service_name: &str, active_status: &str) -> Status {
match active_status.to_lowercase().as_str() { match active_status.to_lowercase().as_str() {
"active" => { "active" => Status::Ok,
// 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
},
"inactive" | "dead" => { "inactive" | "dead" => {
// Check if this service was stopped by user action debug!("Service '{}' is inactive - treating as Inactive status", service_name);
if UserStoppedServiceTracker::is_service_user_stopped(service_name) { Status::Inactive
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
}
}, },
"failed" | "error" => Status::Critical, "failed" | "error" => Status::Critical,
"activating" | "deactivating" | "reloading" | "start" | "stop" | "restart" => { "activating" | "deactivating" | "reloading" | "start" | "stop" | "restart" => {
// For user-stopped services that are transitioning, keep them as OK during transition debug!("Service '{}' is transitioning - treating as Pending", service_name);
if UserStoppedServiceTracker::is_service_user_stopped(service_name) { Status::Pending
debug!("Service '{}' is transitioning but was user-stopped - treating as OK", service_name);
Status::Ok
} else {
Status::Pending
}
}, },
_ => Status::Unknown, _ => Status::Unknown,
} }

View File

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

View File

@ -34,10 +34,6 @@ pub struct HostWidgets {
pub services_widget: ServicesWidget, pub services_widget: ServicesWidget,
/// Backup widget state /// Backup widget state
pub backup_widget: BackupWidget, 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 /// Last update time for this host
pub last_update: Option<Instant>, pub last_update: Option<Instant>,
} }
@ -48,9 +44,6 @@ impl HostWidgets {
system_widget: SystemWidget::new(), system_widget: SystemWidget::new(),
services_widget: ServicesWidget::new(), services_widget: ServicesWidget::new(),
backup_widget: BackupWidget::new(), backup_widget: BackupWidget::new(),
system_scroll_offset: 0,
services_scroll_offset: 0,
backup_scroll_offset: 0,
last_update: None, last_update: None,
} }
} }
@ -274,7 +267,7 @@ impl TuiApp {
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()) {
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 {}@{} \"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, service_name,
hostname, hostname,
connection_ip, connection_ip,
@ -584,14 +577,10 @@ 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 = {
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); let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets host_widgets
.services_widget .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 // Render statusbar at the bottom
@ -775,14 +764,10 @@ impl TuiApp {
frame.render_widget(system_block, area); frame.render_widget(system_block, area);
// Get current host widgets, create if none exist // Get current host widgets, create if none exist
if let Some(hostname) = self.current_host.clone() { 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 // Clone the config to avoid borrowing issues
let config = self.config.clone(); let config = self.config.clone();
let host_widgets = self.get_or_create_host_widgets(&hostname); 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 // Get current host widgets for backup widget
if let Some(hostname) = self.current_host.clone() { 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); 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);
} }
} }

View File

@ -285,8 +285,8 @@ impl Widget for BackupWidget {
} }
impl BackupWidget { impl BackupWidget {
/// Render with scroll offset support /// Render backup widget
pub fn render_with_scroll(&mut self, frame: &mut Frame, area: Rect, scroll_offset: usize) { pub fn render(&mut self, frame: &mut Frame, area: Rect) {
let mut lines = Vec::new(); let mut lines = Vec::new();
// Latest backup section // Latest backup section
@ -366,42 +366,20 @@ impl BackupWidget {
let total_lines = lines.len(); let total_lines = lines.len();
let available_height = area.height as usize; let available_height = area.height as usize;
// Calculate scroll boundaries // Show only what fits, with "X more below" if needed
let max_scroll = if total_lines > available_height { if total_lines > available_height {
total_lines - available_height let lines_for_content = available_height.saturating_sub(1); // Reserve one line for "more below"
} 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 {
let mut visible_lines: Vec<_> = lines let mut visible_lines: Vec<_> = lines
.into_iter() .into_iter()
.skip(effective_scroll) .take(lines_for_content)
.take(available_height)
.collect(); .collect();
// Add scroll indicator if there are hidden lines let hidden_below = total_lines.saturating_sub(lines_for_content);
if total_lines > available_height { if hidden_below > 0 {
let hidden_above = effective_scroll; let more_line = ratatui::text::Line::from(vec![
let hidden_below = total_lines.saturating_sub(effective_scroll + available_height); ratatui::text::Span::styled(format!("... {} more below", hidden_below), Typography::muted())
]);
if (hidden_above > 0 || hidden_below > 0) && !visible_lines.is_empty() { visible_lines.push(more_line);
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 paragraph = Paragraph::new(ratatui::text::Text::from(visible_lines)); let paragraph = Paragraph::new(ratatui::text::Text::from(visible_lines));

View File

@ -401,8 +401,8 @@ impl Widget for ServicesWidget {
impl ServicesWidget { impl ServicesWidget {
/// Render with focus and scroll /// Render with focus
pub fn render(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) { pub fn render(&mut self, frame: &mut Frame, area: Rect, is_focused: bool) {
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);
@ -428,11 +428,11 @@ impl ServicesWidget {
} }
// Render the services list // 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 /// 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 // Build hierarchical service list for display
let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new(); 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 available_lines = area.height as usize;
let total_lines = display_lines.len(); let total_lines = display_lines.len();
// Calculate scroll boundaries // Reserve one line for "X more below" if needed
let max_scroll = if total_lines > available_lines { let lines_for_content = if total_lines > available_lines {
total_lines - available_lines available_lines.saturating_sub(1)
} else { } 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 let visible_lines: Vec<_> = display_lines
.iter() .iter()
.skip(effective_scroll) .take(lines_for_content)
.take(available_lines)
.collect(); .collect();
let hidden_below = total_lines.saturating_sub(lines_for_content);
let lines_to_show = visible_lines.len(); let lines_to_show = visible_lines.len();
if lines_to_show > 0 { 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() let service_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); lines_to_show]) .constraints(vec![Constraint::Length(1); total_chunks_needed])
.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)) 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 // Only parent services can be selected - calculate parent service index
let is_selected = if !*is_sub { let is_selected = if !*is_sub {
@ -535,33 +536,12 @@ impl ServicesWidget {
frame.render_widget(service_para, service_chunks[i]); 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 { // Show "X more below" message if content was truncated
let scroll_text = if hidden_above > 0 && hidden_below > 0 { if hidden_below > 0 {
format!("... {} above, {} below", hidden_above, hidden_below) let more_text = format!("... {} more below", hidden_below);
} else if hidden_above > 0 { let more_para = Paragraph::new(more_text).style(Typography::muted());
format!("... {} more above", hidden_above) frame.render_widget(more_para, service_chunks[lines_to_show]);
} 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);
}
} }
} }
} }

View File

@ -438,8 +438,8 @@ impl Widget for SystemWidget {
} }
impl SystemWidget { impl SystemWidget {
/// Render with scroll offset support /// Render system widget
pub fn render_with_scroll(&mut self, frame: &mut Frame, area: Rect, scroll_offset: usize, hostname: &str, config: Option<&crate::config::DashboardConfig>) { pub fn render(&mut self, frame: &mut Frame, area: Rect, hostname: &str, config: Option<&crate::config::DashboardConfig>) {
let mut lines = Vec::new(); let mut lines = Vec::new();
// NixOS section // NixOS section
@ -560,22 +560,22 @@ impl SystemWidget {
let total_lines = lines.len(); let total_lines = lines.len();
let available_height = area.height as usize; let available_height = area.height as usize;
// Always apply scrolling if scroll_offset > 0, even if content fits // Show only what fits, with "X more below" if needed
if scroll_offset > 0 || total_lines > available_height { if total_lines > available_height {
let max_scroll = if total_lines > available_height { let lines_for_content = available_height.saturating_sub(1); // Reserve one line for "more below"
total_lines - available_height let mut visible_lines: Vec<Line> = lines
} 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<Line> = lines
.into_iter() .into_iter()
.skip(effective_scroll) .take(lines_for_content)
.take(available_height)
.collect(); .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)); let paragraph = Paragraph::new(Text::from(visible_lines));
frame.render_widget(paragraph, area); frame.render_widget(paragraph, area);
} else { } else {

View File

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