Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f22e3ee95e | |||
| e890c5e810 | |||
| 078c30a592 | |||
| a847674004 | |||
| 2618f6b62f |
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.27"
|
version = "0.1.32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -291,7 +291,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.27"
|
version = "0.1.32"
|
||||||
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.27"
|
version = "0.1.32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.28"
|
version = "0.1.33"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -274,7 +274,6 @@ impl Agent {
|
|||||||
let action_str = match action {
|
let action_str = match action {
|
||||||
ServiceAction::Start => "start",
|
ServiceAction::Start => "start",
|
||||||
ServiceAction::Stop => "stop",
|
ServiceAction::Stop => "stop",
|
||||||
ServiceAction::Restart => "restart",
|
|
||||||
ServiceAction::Status => "status",
|
ServiceAction::Status => "status",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -299,7 +298,7 @@ impl Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Force refresh metrics after service control to update service status
|
// Force refresh metrics after service control to update service status
|
||||||
if matches!(action, ServiceAction::Start | ServiceAction::Stop | ServiceAction::Restart) {
|
if matches!(action, ServiceAction::Start | ServiceAction::Stop) {
|
||||||
info!("Triggering immediate metric refresh after service control");
|
info!("Triggering immediate metric refresh after service control");
|
||||||
if let Err(e) = self.collect_metrics_only().await {
|
if let Err(e) = self.collect_metrics_only().await {
|
||||||
error!("Failed to refresh metrics after service control: {}", e);
|
error!("Failed to refresh metrics after service control: {}", e);
|
||||||
|
|||||||
@@ -136,8 +136,21 @@ impl SystemdCollector {
|
|||||||
/// Auto-discover interesting services to monitor (internal version that doesn't update state)
|
/// Auto-discover interesting services to monitor (internal version that doesn't update state)
|
||||||
fn discover_services_internal(&self) -> Result<(Vec<String>, std::collections::HashMap<String, ServiceStatusInfo>)> {
|
fn discover_services_internal(&self) -> Result<(Vec<String>, std::collections::HashMap<String, ServiceStatusInfo>)> {
|
||||||
debug!("Starting systemd service discovery with status caching");
|
debug!("Starting systemd service discovery with status caching");
|
||||||
// Get all services (includes inactive, running, failed - everything)
|
|
||||||
let units_output = Command::new("systemctl")
|
// First: Get all service unit files (includes services that have never been started)
|
||||||
|
let unit_files_output = Command::new("systemctl")
|
||||||
|
.arg("list-unit-files")
|
||||||
|
.arg("--type=service")
|
||||||
|
.arg("--no-pager")
|
||||||
|
.arg("--plain")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !unit_files_output.status.success() {
|
||||||
|
return Err(anyhow::anyhow!("systemctl list-unit-files command failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second: Get runtime status of all units
|
||||||
|
let units_status_output = Command::new("systemctl")
|
||||||
.arg("list-units")
|
.arg("list-units")
|
||||||
.arg("--type=service")
|
.arg("--type=service")
|
||||||
.arg("--all")
|
.arg("--all")
|
||||||
@@ -145,22 +158,33 @@ impl SystemdCollector {
|
|||||||
.arg("--plain")
|
.arg("--plain")
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !units_output.status.success() {
|
if !units_status_output.status.success() {
|
||||||
return Err(anyhow::anyhow!("systemctl system command failed"));
|
return Err(anyhow::anyhow!("systemctl list-units command failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let units_str = String::from_utf8(units_output.stdout)?;
|
let unit_files_str = String::from_utf8(unit_files_output.stdout)?;
|
||||||
|
let units_status_str = String::from_utf8(units_status_output.stdout)?;
|
||||||
let mut services = Vec::new();
|
let mut services = Vec::new();
|
||||||
|
|
||||||
// Use configuration instead of hardcoded values
|
// Use configuration instead of hardcoded values
|
||||||
let excluded_services = &self.config.excluded_services;
|
let excluded_services = &self.config.excluded_services;
|
||||||
let service_name_filters = &self.config.service_name_filters;
|
let service_name_filters = &self.config.service_name_filters;
|
||||||
|
|
||||||
// Parse all services and cache their status information
|
// Parse all service unit files to get complete service list
|
||||||
let mut all_service_names = std::collections::HashSet::new();
|
let mut all_service_names = std::collections::HashSet::new();
|
||||||
let mut status_cache = std::collections::HashMap::new();
|
|
||||||
|
|
||||||
for line in units_str.lines() {
|
for line in unit_files_str.lines() {
|
||||||
|
let fields: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if fields.len() >= 2 && fields[0].ends_with(".service") {
|
||||||
|
let service_name = fields[0].trim_end_matches(".service");
|
||||||
|
all_service_names.insert(service_name.to_string());
|
||||||
|
debug!("Found service unit file: {}", service_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse runtime status for all units
|
||||||
|
let mut status_cache = std::collections::HashMap::new();
|
||||||
|
for line in units_status_str.lines() {
|
||||||
let fields: Vec<&str> = line.split_whitespace().collect();
|
let fields: Vec<&str> = line.split_whitespace().collect();
|
||||||
if fields.len() >= 4 && fields[0].ends_with(".service") {
|
if fields.len() >= 4 && fields[0].ends_with(".service") {
|
||||||
let service_name = fields[0].trim_end_matches(".service");
|
let service_name = fields[0].trim_end_matches(".service");
|
||||||
@@ -177,8 +201,19 @@ impl SystemdCollector {
|
|||||||
sub_state: sub_state.clone(),
|
sub_state: sub_state.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
all_service_names.insert(service_name.to_string());
|
debug!("Got runtime status for service: {} (load:{}, active:{}, sub:{})", service_name, load_state, active_state, sub_state);
|
||||||
debug!("Parsed service: {} (load:{}, active:{}, sub:{})", service_name, load_state, active_state, sub_state);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For services found in unit files but not in runtime status, set default inactive status
|
||||||
|
for service_name in &all_service_names {
|
||||||
|
if !status_cache.contains_key(service_name) {
|
||||||
|
status_cache.insert(service_name.to_string(), ServiceStatusInfo {
|
||||||
|
load_state: "not-loaded".to_string(),
|
||||||
|
active_state: "inactive".to_string(),
|
||||||
|
sub_state: "dead".to_string(),
|
||||||
|
});
|
||||||
|
debug!("Service {} found in unit files but not runtime - marked as inactive", service_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,5 @@ pub enum AgentCommand {
|
|||||||
pub enum ServiceAction {
|
pub enum ServiceAction {
|
||||||
Start,
|
Start,
|
||||||
Stop,
|
Stop,
|
||||||
Restart,
|
|
||||||
Status,
|
Status,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.28"
|
version = "0.1.33"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -294,14 +294,6 @@ impl Dashboard {
|
|||||||
/// Execute a UI command by sending it to the appropriate agent
|
/// Execute a UI command by sending it to the appropriate agent
|
||||||
async fn execute_ui_command(&self, command: UiCommand) -> Result<()> {
|
async fn execute_ui_command(&self, command: UiCommand) -> Result<()> {
|
||||||
match command {
|
match command {
|
||||||
UiCommand::ServiceRestart { hostname, service_name } => {
|
|
||||||
info!("Sending restart command for service {} on {}", service_name, hostname);
|
|
||||||
let agent_command = AgentCommand::ServiceControl {
|
|
||||||
service_name,
|
|
||||||
action: ServiceAction::Restart,
|
|
||||||
};
|
|
||||||
self.zmq_command_sender.send_command(&hostname, agent_command).await?;
|
|
||||||
}
|
|
||||||
UiCommand::ServiceStart { hostname, service_name } => {
|
UiCommand::ServiceStart { hostname, service_name } => {
|
||||||
info!("Sending start command for service {} on {}", service_name, hostname);
|
info!("Sending start command for service {} on {}", service_name, hostname);
|
||||||
let agent_command = AgentCommand::ServiceControl {
|
let agent_command = AgentCommand::ServiceControl {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ pub enum AgentCommand {
|
|||||||
pub enum ServiceAction {
|
pub enum ServiceAction {
|
||||||
Start,
|
Start,
|
||||||
Stop,
|
Stop,
|
||||||
Restart,
|
|
||||||
Status,
|
Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.28"
|
"v0.1.33"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if running inside tmux session
|
/// Check if running inside tmux session
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
use crossterm::event::{Event, KeyCode};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
style::Style,
|
style::Style,
|
||||||
@@ -22,7 +22,6 @@ use widgets::{BackupWidget, ServicesWidget, SystemWidget, Widget};
|
|||||||
/// Commands that can be triggered from the UI
|
/// Commands that can be triggered from the UI
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum UiCommand {
|
pub enum UiCommand {
|
||||||
ServiceRestart { hostname: String, service_name: String },
|
|
||||||
ServiceStart { hostname: String, service_name: String },
|
ServiceStart { hostname: String, service_name: String },
|
||||||
ServiceStop { hostname: String, service_name: String },
|
ServiceStop { hostname: String, service_name: String },
|
||||||
TriggerBackup { hostname: String },
|
TriggerBackup { hostname: String },
|
||||||
@@ -32,22 +31,12 @@ pub enum UiCommand {
|
|||||||
/// Types of commands for status tracking
|
/// Types of commands for status tracking
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CommandType {
|
pub enum CommandType {
|
||||||
ServiceRestart,
|
|
||||||
ServiceStart,
|
ServiceStart,
|
||||||
ServiceStop,
|
ServiceStop,
|
||||||
BackupTrigger,
|
BackupTrigger,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Panel types for focus management
|
/// Panel types for focus management
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum PanelType {
|
|
||||||
System,
|
|
||||||
Services,
|
|
||||||
Backup,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PanelType {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Widget states for a specific host
|
/// Widget states for a specific host
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -94,8 +83,6 @@ pub struct TuiApp {
|
|||||||
available_hosts: Vec<String>,
|
available_hosts: Vec<String>,
|
||||||
/// Host index for navigation
|
/// Host index for navigation
|
||||||
host_index: usize,
|
host_index: usize,
|
||||||
/// Currently focused panel
|
|
||||||
focused_panel: PanelType,
|
|
||||||
/// Should quit application
|
/// Should quit application
|
||||||
should_quit: bool,
|
should_quit: bool,
|
||||||
/// Track if user manually navigated away from localhost
|
/// Track if user manually navigated away from localhost
|
||||||
@@ -111,7 +98,6 @@ impl TuiApp {
|
|||||||
current_host: None,
|
current_host: None,
|
||||||
available_hosts: Vec::new(),
|
available_hosts: Vec::new(),
|
||||||
host_index: 0,
|
host_index: 0,
|
||||||
focused_panel: PanelType::System, // Start with System panel focused
|
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
user_navigated_away: false,
|
user_navigated_away: false,
|
||||||
config,
|
config,
|
||||||
@@ -256,9 +242,7 @@ impl TuiApp {
|
|||||||
self.navigate_host(1);
|
self.navigate_host(1);
|
||||||
}
|
}
|
||||||
KeyCode::Char('r') => {
|
KeyCode::Char('r') => {
|
||||||
match self.focused_panel {
|
// System rebuild command - works on any panel for current host
|
||||||
PanelType::System => {
|
|
||||||
// Simple tmux popup with SSH rebuild using configured user and alias
|
|
||||||
if let Some(hostname) = self.current_host.clone() {
|
if let Some(hostname) = self.current_host.clone() {
|
||||||
// Launch tmux popup with SSH using config values
|
// Launch tmux popup with SSH using config values
|
||||||
let ssh_command = format!(
|
let ssh_command = format!(
|
||||||
@@ -274,21 +258,7 @@ impl TuiApp {
|
|||||||
.ok(); // Ignore errors, tmux will handle them
|
.ok(); // Ignore errors, tmux will handle them
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PanelType::Services => {
|
|
||||||
// Service restart command
|
|
||||||
if let (Some(service_name), Some(hostname)) = (self.get_selected_service(), self.current_host.clone()) {
|
|
||||||
if self.start_command(&hostname, CommandType::ServiceRestart, service_name.clone()) {
|
|
||||||
return Ok(Some(UiCommand::ServiceRestart { hostname, service_name }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
info!("Manual refresh requested");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char('s') => {
|
KeyCode::Char('s') => {
|
||||||
if self.focused_panel == PanelType::Services {
|
|
||||||
// Service start command
|
// Service start command
|
||||||
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()) {
|
||||||
if self.start_command(&hostname, CommandType::ServiceStart, service_name.clone()) {
|
if self.start_command(&hostname, CommandType::ServiceStart, service_name.clone()) {
|
||||||
@@ -296,9 +266,7 @@ impl TuiApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
KeyCode::Char('S') => {
|
KeyCode::Char('S') => {
|
||||||
if self.focused_panel == PanelType::Services {
|
|
||||||
// Service stop command
|
// Service stop command
|
||||||
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()) {
|
||||||
if self.start_command(&hostname, CommandType::ServiceStop, service_name.clone()) {
|
if self.start_command(&hostname, CommandType::ServiceStop, service_name.clone()) {
|
||||||
@@ -306,36 +274,34 @@ impl TuiApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
KeyCode::Char('b') => {
|
KeyCode::Char('b') => {
|
||||||
if self.focused_panel == PanelType::Backup {
|
|
||||||
// Trigger backup
|
// Trigger backup
|
||||||
if let Some(hostname) = self.current_host.clone() {
|
if let Some(hostname) = self.current_host.clone() {
|
||||||
self.start_command(&hostname, CommandType::BackupTrigger, hostname.clone());
|
self.start_command(&hostname, CommandType::BackupTrigger, hostname.clone());
|
||||||
return Ok(Some(UiCommand::TriggerBackup { hostname }));
|
return Ok(Some(UiCommand::TriggerBackup { hostname }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
if key.modifiers.contains(KeyModifiers::SHIFT) {
|
|
||||||
// Shift+Tab cycles through panels
|
|
||||||
self.next_panel();
|
|
||||||
} else {
|
|
||||||
// Tab cycles to next host
|
// Tab cycles to next host
|
||||||
self.navigate_host(1);
|
self.navigate_host(1);
|
||||||
}
|
}
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
|
// Move service selection up
|
||||||
|
if let Some(hostname) = self.current_host.clone() {
|
||||||
|
let host_widgets = self.get_or_create_host_widgets(&hostname);
|
||||||
|
host_widgets.services_widget.select_previous();
|
||||||
}
|
}
|
||||||
KeyCode::BackTab => {
|
|
||||||
// BackTab (Shift+Tab on some terminals) also cycles panels
|
|
||||||
self.next_panel();
|
|
||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
// Scroll up in focused panel
|
// Move service selection down
|
||||||
self.scroll_focused_panel(-1);
|
if let Some(hostname) = self.current_host.clone() {
|
||||||
|
let total_services = {
|
||||||
|
let host_widgets = self.get_or_create_host_widgets(&hostname);
|
||||||
|
host_widgets.services_widget.get_total_services_count()
|
||||||
|
};
|
||||||
|
let host_widgets = self.get_or_create_host_widgets(&hostname);
|
||||||
|
host_widgets.services_widget.select_next(total_services);
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
|
||||||
// Scroll down in focused panel
|
|
||||||
self.scroll_focused_panel(1);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -376,25 +342,6 @@ impl TuiApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Switch to next panel (Shift+Tab) - only cycles through visible panels
|
|
||||||
pub fn next_panel(&mut self) {
|
|
||||||
let visible_panels = self.get_visible_panels();
|
|
||||||
if visible_panels.len() <= 1 {
|
|
||||||
return; // Can't switch if only one or no panels visible
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find current panel index in visible panels
|
|
||||||
if let Some(current_index) = visible_panels.iter().position(|&p| p == self.focused_panel) {
|
|
||||||
// Move to next visible panel
|
|
||||||
let next_index = (current_index + 1) % visible_panels.len();
|
|
||||||
self.focused_panel = visible_panels[next_index];
|
|
||||||
} else {
|
|
||||||
// Current panel not visible, switch to first visible panel
|
|
||||||
self.focused_panel = visible_panels[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Switched to panel: {:?}", self.focused_panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -431,7 +378,6 @@ impl TuiApp {
|
|||||||
let should_execute = match (&command_type, current_status.as_deref()) {
|
let should_execute = match (&command_type, current_status.as_deref()) {
|
||||||
(CommandType::ServiceStart, Some("inactive") | Some("failed") | Some("dead")) => true,
|
(CommandType::ServiceStart, Some("inactive") | Some("failed") | Some("dead")) => true,
|
||||||
(CommandType::ServiceStop, Some("active")) => true,
|
(CommandType::ServiceStop, Some("active")) => true,
|
||||||
(CommandType::ServiceRestart, Some("active") | Some("inactive") | Some("failed") | Some("dead")) => true,
|
|
||||||
(CommandType::ServiceStart, Some("active")) => {
|
(CommandType::ServiceStart, Some("active")) => {
|
||||||
// Already running - don't execute
|
// Already running - don't execute
|
||||||
false
|
false
|
||||||
@@ -462,15 +408,9 @@ impl TuiApp {
|
|||||||
fn clear_completed_transitions(&mut self, hostname: &str, service_metrics: &[&Metric]) {
|
fn clear_completed_transitions(&mut self, hostname: &str, service_metrics: &[&Metric]) {
|
||||||
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
|
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
|
||||||
let mut completed_services = Vec::new();
|
let mut completed_services = Vec::new();
|
||||||
let now = Instant::now();
|
|
||||||
|
|
||||||
// Check each pending transition to see if real status has changed or timed out
|
// 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 {
|
for (service_name, (command_type, original_status, _start_time)) in &host_widgets.pending_service_transitions {
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for status metric for this service
|
// Look for status metric for this service
|
||||||
for metric in service_metrics {
|
for metric in service_metrics {
|
||||||
@@ -483,7 +423,6 @@ impl TuiApp {
|
|||||||
let expected_change = match command_type {
|
let expected_change = match command_type {
|
||||||
CommandType::ServiceStart => &new_status == "active",
|
CommandType::ServiceStart => &new_status == "active",
|
||||||
CommandType::ServiceStop => &new_status != "active",
|
CommandType::ServiceStop => &new_status != "active",
|
||||||
CommandType::ServiceRestart => true, // Any change indicates restart completed
|
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -503,61 +442,8 @@ impl TuiApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll the focused panel up or down
|
|
||||||
pub fn scroll_focused_panel(&mut self, direction: i32) {
|
|
||||||
if let Some(hostname) = self.current_host.clone() {
|
|
||||||
let focused_panel = self.focused_panel; // Get the value before borrowing
|
|
||||||
let host_widgets = self.get_or_create_host_widgets(&hostname);
|
|
||||||
|
|
||||||
match focused_panel {
|
|
||||||
PanelType::System => {
|
|
||||||
if direction > 0 {
|
|
||||||
host_widgets.system_scroll_offset = host_widgets.system_scroll_offset.saturating_add(1);
|
|
||||||
} else {
|
|
||||||
host_widgets.system_scroll_offset = host_widgets.system_scroll_offset.saturating_sub(1);
|
|
||||||
}
|
|
||||||
info!("System panel scroll offset: {}", host_widgets.system_scroll_offset);
|
|
||||||
}
|
|
||||||
PanelType::Services => {
|
|
||||||
// For services panel, Up/Down moves selection cursor, not scroll
|
|
||||||
let total_services = host_widgets.services_widget.get_total_services_count();
|
|
||||||
|
|
||||||
if direction > 0 {
|
|
||||||
host_widgets.services_widget.select_next(total_services);
|
|
||||||
info!("Services selection moved down");
|
|
||||||
} else {
|
|
||||||
host_widgets.services_widget.select_previous();
|
|
||||||
info!("Services selection moved up");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PanelType::Backup => {
|
|
||||||
if direction > 0 {
|
|
||||||
host_widgets.backup_scroll_offset = host_widgets.backup_scroll_offset.saturating_add(1);
|
|
||||||
} else {
|
|
||||||
host_widgets.backup_scroll_offset = host_widgets.backup_scroll_offset.saturating_sub(1);
|
|
||||||
}
|
|
||||||
info!("Backup panel scroll offset: {}", host_widgets.backup_scroll_offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Get list of currently visible panels
|
|
||||||
fn get_visible_panels(&self) -> Vec<PanelType> {
|
|
||||||
let mut visible_panels = vec![PanelType::System, PanelType::Services];
|
|
||||||
|
|
||||||
// Check if backup panel should be shown
|
|
||||||
if let Some(hostname) = &self.current_host {
|
|
||||||
if let Some(host_widgets) = self.host_widgets.get(hostname) {
|
|
||||||
if host_widgets.backup_widget.has_data() {
|
|
||||||
visible_panels.push(PanelType::Backup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visible_panels
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the dashboard (real btop-style multi-panel layout)
|
/// Render the dashboard (real btop-style multi-panel layout)
|
||||||
pub fn render(&mut self, frame: &mut Frame, metric_store: &MetricStore) {
|
pub fn render(&mut self, frame: &mut Frame, metric_store: &MetricStore) {
|
||||||
@@ -626,7 +512,7 @@ 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 = true; // Always show service selection
|
||||||
let (scroll_offset, pending_transitions) = {
|
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.pending_service_transitions.clone())
|
(host_widgets.services_scroll_offset, host_widgets.pending_service_transitions.clone())
|
||||||
@@ -755,25 +641,10 @@ impl TuiApp {
|
|||||||
|
|
||||||
// Global shortcuts
|
// Global shortcuts
|
||||||
shortcuts.push("Tab: Switch Host".to_string());
|
shortcuts.push("Tab: Switch Host".to_string());
|
||||||
shortcuts.push("Shift+Tab: Switch Panel".to_string());
|
shortcuts.push("↑↓/jk: Select Service".to_string());
|
||||||
|
shortcuts.push("R: Rebuild Host".to_string());
|
||||||
// Scroll shortcuts (always available)
|
shortcuts.push("S: Start Service".to_string());
|
||||||
shortcuts.push("↑↓: Scroll".to_string());
|
shortcuts.push("Shift+S: Stop Service".to_string());
|
||||||
|
|
||||||
// Panel-specific shortcuts
|
|
||||||
match self.focused_panel {
|
|
||||||
PanelType::System => {
|
|
||||||
shortcuts.push("R: Rebuild".to_string());
|
|
||||||
}
|
|
||||||
PanelType::Services => {
|
|
||||||
shortcuts.push("S: Start".to_string());
|
|
||||||
shortcuts.push("Shift+S: Stop".to_string());
|
|
||||||
shortcuts.push("R: Restart".to_string());
|
|
||||||
}
|
|
||||||
PanelType::Backup => {
|
|
||||||
shortcuts.push("B: Trigger Backup".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always show quit
|
// Always show quit
|
||||||
shortcuts.push("Q: Quit".to_string());
|
shortcuts.push("Q: Quit".to_string());
|
||||||
@@ -782,11 +653,7 @@ impl TuiApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_system_panel(&mut self, frame: &mut Frame, area: Rect, _metric_store: &MetricStore) {
|
fn render_system_panel(&mut self, frame: &mut Frame, area: Rect, _metric_store: &MetricStore) {
|
||||||
let system_block = if self.focused_panel == PanelType::System {
|
let system_block = Components::widget_block("system");
|
||||||
Components::focused_widget_block("system")
|
|
||||||
} else {
|
|
||||||
Components::widget_block("system")
|
|
||||||
};
|
|
||||||
let inner_area = system_block.inner(area);
|
let inner_area = system_block.inner(area);
|
||||||
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
|
||||||
@@ -801,11 +668,7 @@ impl TuiApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_backup_panel(&mut self, frame: &mut Frame, area: Rect) {
|
fn render_backup_panel(&mut self, frame: &mut Frame, area: Rect) {
|
||||||
let backup_block = if self.focused_panel == PanelType::Backup {
|
let backup_block = Components::widget_block("backup");
|
||||||
Components::focused_widget_block("backup")
|
|
||||||
} else {
|
|
||||||
Components::widget_block("backup")
|
|
||||||
};
|
|
||||||
let inner_area = backup_block.inner(area);
|
let inner_area = backup_block.inner(area);
|
||||||
frame.render_widget(backup_block, area);
|
frame.render_widget(backup_block, area);
|
||||||
|
|
||||||
|
|||||||
@@ -289,18 +289,6 @@ impl Components {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Widget block with focus indicator (blue border)
|
|
||||||
pub fn focused_widget_block(title: &str) -> Block<'_> {
|
|
||||||
Block::default()
|
|
||||||
.title(title)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.style(Style::default().fg(Theme::highlight()).bg(Theme::background())) // Blue border for focus
|
|
||||||
.title_style(
|
|
||||||
Style::default()
|
|
||||||
.fg(Theme::highlight()) // Blue title for focus
|
|
||||||
.bg(Theme::background()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Typography {
|
impl Typography {
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ impl ServicesWidget {
|
|||||||
if let Some((command_type, _original_status, _start_time)) = pending_transitions.get(service_name) {
|
if let Some((command_type, _original_status, _start_time)) = pending_transitions.get(service_name) {
|
||||||
// Show transitional icons for pending commands
|
// Show transitional icons for pending commands
|
||||||
let (icon, status_text) = match command_type {
|
let (icon, status_text) = match command_type {
|
||||||
CommandType::ServiceRestart => ("↻", "restarting"),
|
|
||||||
CommandType::ServiceStart => ("↑", "starting"),
|
CommandType::ServiceStart => ("↑", "starting"),
|
||||||
CommandType::ServiceStop => ("↓", "stopping"),
|
CommandType::ServiceStop => ("↓", "stopping"),
|
||||||
_ => return (StatusIcons::get_icon(info.widget_status).to_string(), info.status.clone(), Theme::status_color(info.widget_status)), // Not a service command
|
_ => return (StatusIcons::get_icon(info.widget_status).to_string(), info.status.clone(), Theme::status_color(info.widget_status)), // Not a service command
|
||||||
@@ -444,11 +443,7 @@ impl ServicesWidget {
|
|||||||
|
|
||||||
/// Render with focus, scroll, and pending transitions for visual feedback
|
/// Render with focus, scroll, and pending transitions for visual feedback
|
||||||
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_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 = Components::widget_block("services");
|
||||||
Components::focused_widget_block("services")
|
|
||||||
} else {
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -559,17 +554,25 @@ impl ServicesWidget {
|
|||||||
// Parent services - check if this parent service has a pending transition using RAW service name
|
// Parent services - check if this parent service has a pending transition using RAW service name
|
||||||
if pending_transitions.contains_key(raw_service_name) {
|
if pending_transitions.contains_key(raw_service_name) {
|
||||||
// Create spans with transitional status
|
// Create spans with transitional status
|
||||||
let (icon, status_text, status_color) = self.get_service_icon_and_status(raw_service_name, &ServiceInfo {
|
let (icon, status_text, _) = self.get_service_icon_and_status(raw_service_name, &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
|
||||||
}, pending_transitions);
|
}, 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![
|
vec![
|
||||||
ratatui::text::Span::styled(format!("{} ", icon), Style::default().fg(status_color)),
|
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(line_text.clone(), Style::default().fg(Theme::primary_text())),
|
||||||
ratatui::text::Span::styled(format!(" {}", status_text), Style::default().fg(status_color)),
|
ratatui::text::Span::styled(format!(" {}", status_text), Style::default().fg(icon_color)),
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
StatusIcons::create_status_spans(*line_status, line_text)
|
StatusIcons::create_status_spans(*line_status, line_text)
|
||||||
@@ -578,8 +581,8 @@ impl ServicesWidget {
|
|||||||
|
|
||||||
// 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
|
||||||
// IMPORTANT: Don't override transitional icons that show pending commands
|
// Show selection highlighting even when transitional icons are present
|
||||||
if is_selected && !*is_sub && is_focused && !pending_transitions.contains_key(raw_service_name) {
|
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 {
|
||||||
// First span is the status icon - preserve its color
|
// First span is the status icon - preserve its color
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.28"
|
version = "0.1.33"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user