Implement hysteresis for metric status changes to prevent flapping

Add comprehensive hysteresis support to prevent status oscillation near
threshold boundaries while maintaining responsive alerting.

Key Features:
- HysteresisThresholds with configurable upper/lower limits
- StatusTracker for per-metric status history
- Default gaps: CPU load 10%, memory 5%, disk temp 5°C

Updated Components:
- CPU load collector (5-minute average with hysteresis)
- Memory usage collector (percentage-based thresholds)
- Disk temperature collector (SMART data monitoring)
- All collectors updated to support StatusTracker interface

Cache Interval Adjustments:
- Service status: 60s → 10s (faster response)
- Disk usage: 300s → 60s (more frequent checks)
- Backup status: 900s → 60s (quicker updates)
- SMART data: moved to 600s tier (10 minutes)

Architecture:
- Individual metric status calculation in collectors
- Centralized StatusTracker in MetricCollectionManager
- Status aggregation preserved in dashboard widgets
This commit is contained in:
2025-10-20 18:45:41 +02:00
parent e998679901
commit 00a8ed3da2
34 changed files with 1037 additions and 770 deletions

View File

@@ -1,6 +1,6 @@
use ratatui::style::{Color, Style, Modifier};
use ratatui::widgets::{Block, Borders};
use cm_dashboard_shared::Status;
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders};
/// Complete terminal color palette matching your configuration
#[allow(dead_code)]
@@ -10,7 +10,7 @@ pub struct TerminalColors {
pub dim_foreground: Color,
pub bright_foreground: Color,
pub background: Color,
// Normal colors
pub normal_black: Color,
pub normal_red: Color,
@@ -20,7 +20,7 @@ pub struct TerminalColors {
pub normal_magenta: Color,
pub normal_cyan: Color,
pub normal_white: Color,
// Bright colors
pub bright_black: Color,
pub bright_red: Color,
@@ -30,7 +30,7 @@ pub struct TerminalColors {
pub bright_magenta: Color,
pub bright_cyan: Color,
pub bright_white: Color,
// Dim colors
pub dim_black: Color,
pub dim_red: Color,
@@ -46,40 +46,40 @@ impl Default for TerminalColors {
fn default() -> Self {
Self {
// Primary colors
foreground: Color::Rgb(198, 198, 198), // #c6c6c6
dim_foreground: Color::Rgb(112, 112, 112), // #707070
foreground: Color::Rgb(198, 198, 198), // #c6c6c6
dim_foreground: Color::Rgb(112, 112, 112), // #707070
bright_foreground: Color::Rgb(255, 255, 255), // #ffffff
background: Color::Rgb(38, 38, 38), // #262626
background: Color::Rgb(38, 38, 38), // #262626
// Normal colors
normal_black: Color::Rgb(0, 0, 0), // #000000
normal_red: Color::Rgb(215, 84, 0), // #d75400
normal_green: Color::Rgb(175, 215, 135), // #afd787
normal_yellow: Color::Rgb(215, 175, 95), // #d7af5f
normal_blue: Color::Rgb(135, 175, 215), // #87afd7
normal_magenta: Color::Rgb(215, 215, 175), // #d7d7af
normal_cyan: Color::Rgb(160, 160, 160), // #a0a0a0
normal_white: Color::Rgb(238, 238, 238), // #eeeeee
normal_black: Color::Rgb(0, 0, 0), // #000000
normal_red: Color::Rgb(215, 84, 0), // #d75400
normal_green: Color::Rgb(175, 215, 135), // #afd787
normal_yellow: Color::Rgb(215, 175, 95), // #d7af5f
normal_blue: Color::Rgb(135, 175, 215), // #87afd7
normal_magenta: Color::Rgb(215, 215, 175), // #d7d7af
normal_cyan: Color::Rgb(160, 160, 160), // #a0a0a0
normal_white: Color::Rgb(238, 238, 238), // #eeeeee
// Bright colors
bright_black: Color::Rgb(48, 48, 48), // #303030
bright_red: Color::Rgb(215, 84, 0), // #d75400
bright_green: Color::Rgb(175, 215, 135), // #afd787
bright_yellow: Color::Rgb(215, 175, 95), // #d7af5f
bright_blue: Color::Rgb(135, 175, 215), // #87afd7
bright_magenta: Color::Rgb(215, 215, 175), // #d7d7af
bright_cyan: Color::Rgb(160, 160, 160), // #a0a0a0
bright_white: Color::Rgb(255, 255, 255), // #ffffff
bright_black: Color::Rgb(48, 48, 48), // #303030
bright_red: Color::Rgb(215, 84, 0), // #d75400
bright_green: Color::Rgb(175, 215, 135), // #afd787
bright_yellow: Color::Rgb(215, 175, 95), // #d7af5f
bright_blue: Color::Rgb(135, 175, 215), // #87afd7
bright_magenta: Color::Rgb(215, 215, 175), // #d7d7af
bright_cyan: Color::Rgb(160, 160, 160), // #a0a0a0
bright_white: Color::Rgb(255, 255, 255), // #ffffff
// Dim colors
dim_black: Color::Rgb(0, 0, 0), // #000000
dim_red: Color::Rgb(215, 84, 0), // #d75400
dim_green: Color::Rgb(175, 215, 135), // #afd787
dim_yellow: Color::Rgb(215, 175, 95), // #d7af5f
dim_blue: Color::Rgb(135, 175, 215), // #87afd7
dim_magenta: Color::Rgb(215, 215, 175), // #d7d7af
dim_cyan: Color::Rgb(160, 160, 160), // #a0a0a0
dim_white: Color::Rgb(221, 221, 221), // #dddddd
dim_black: Color::Rgb(0, 0, 0), // #000000
dim_red: Color::Rgb(215, 84, 0), // #d75400
dim_green: Color::Rgb(175, 215, 135), // #afd787
dim_yellow: Color::Rgb(215, 175, 95), // #d7af5f
dim_blue: Color::Rgb(135, 175, 215), // #87afd7
dim_magenta: Color::Rgb(215, 215, 175), // #d7d7af
dim_cyan: Color::Rgb(160, 160, 160), // #a0a0a0
dim_white: Color::Rgb(221, 221, 221), // #dddddd
}
}
}
@@ -93,52 +93,52 @@ impl Theme {
static COLORS: std::sync::OnceLock<TerminalColors> = std::sync::OnceLock::new();
COLORS.get_or_init(TerminalColors::default)
}
// Semantic color mapping using the terminal color struct
pub fn primary_text() -> Color {
Self::colors().normal_white
}
pub fn secondary_text() -> Color {
Self::colors().foreground
}
pub fn muted_text() -> Color {
Self::colors().dim_foreground
}
pub fn border() -> Color {
Self::colors().dim_foreground
}
pub fn border_title() -> Color {
Self::colors().bright_white
}
pub fn background() -> Color {
Self::colors().background
}
pub fn success() -> Color {
Self::colors().normal_green
}
pub fn warning() -> Color {
Self::colors().normal_yellow
}
pub fn error() -> Color {
Self::colors().normal_red
}
pub fn info() -> Color {
Self::colors().normal_cyan
}
pub fn highlight() -> Color {
Self::colors().normal_blue
}
/// Get color for status level
pub fn status_color(status: Status) -> Color {
match status {
@@ -148,12 +148,12 @@ impl Theme {
Status::Unknown => Self::muted_text(),
}
}
/// Get style for status level
pub fn status_style(status: Status) -> Style {
Style::default().fg(Self::status_color(status))
}
/// CPU usage colors using terminal color struct
pub fn cpu_color(percentage: u16) -> Color {
match percentage {
@@ -164,7 +164,7 @@ impl Theme {
_ => Self::colors().normal_red, // Over 100%
}
}
/// Memory usage colors using terminal color struct
pub fn memory_color(percentage: u16) -> Color {
match percentage {
@@ -175,7 +175,7 @@ impl Theme {
_ => Self::colors().normal_red, // Over 100%
}
}
/// Get gauge color based on percentage
pub fn gauge_color(percentage: u16, warning_threshold: u16, critical_threshold: u16) -> Color {
if percentage >= critical_threshold {
@@ -186,25 +186,31 @@ impl Theme {
Self::success()
}
}
/// Widget border style
pub fn widget_border_style() -> Style {
Style::default().fg(Self::border()).bg(Self::background())
}
/// Inactive widget border style
pub fn widget_border_inactive_style() -> Style {
Style::default().fg(Self::muted_text()).bg(Self::background())
Style::default()
.fg(Self::muted_text())
.bg(Self::background())
}
/// Title style
pub fn title_style() -> Style {
Style::default().fg(Self::border_title()).bg(Self::background())
Style::default()
.fg(Self::border_title())
.bg(Self::background())
}
/// Status bar style
pub fn status_bar_style() -> Style {
Style::default().fg(Self::muted_text()).bg(Self::background())
Style::default()
.fg(Self::muted_text())
.bg(Self::background())
}
}
@@ -245,29 +251,29 @@ impl StatusIcons {
}
}
/// Create spans with status icon colored and text in foreground color
pub fn create_status_spans(status: Status, text: &str) -> Vec<ratatui::text::Span<'static>> {
let icon = Self::get_icon(status);
let status_color = match status {
Status::Ok => Theme::success(), // Green
Status::Warning => Theme::warning(), // Yellow
Status::Critical => Theme::error(), // Red
Status::Ok => Theme::success(), // Green
Status::Warning => Theme::warning(), // Yellow
Status::Critical => Theme::error(), // Red
Status::Unknown => Theme::muted_text(), // Gray
};
vec![
ratatui::text::Span::styled(
format!("{} ", icon),
Style::default().fg(status_color).bg(Theme::background())
Style::default().fg(status_color).bg(Theme::background()),
),
ratatui::text::Span::styled(
text.to_string(),
Style::default().fg(Theme::secondary_text()).bg(Theme::background())
Style::default()
.fg(Theme::secondary_text())
.bg(Theme::background()),
),
]
}
}
impl Components {
@@ -277,9 +283,12 @@ impl Components {
.title(title)
.borders(Borders::ALL)
.style(Style::default().fg(Theme::border()).bg(Theme::background()))
.title_style(Style::default().fg(Theme::border_title()).bg(Theme::background()))
.title_style(
Style::default()
.fg(Theme::border_title())
.bg(Theme::background()),
)
}
}
impl Typography {
@@ -289,7 +298,7 @@ impl Typography {
.fg(Theme::primary_text())
.bg(Theme::background())
}
/// Widget title style (panel headers) - bold bright white
pub fn widget_title() -> Style {
Style::default()
@@ -297,14 +306,14 @@ impl Typography {
.bg(Theme::background())
.add_modifier(Modifier::BOLD)
}
/// Secondary content text
pub fn secondary() -> Style {
Style::default()
.fg(Theme::secondary_text())
.bg(Theme::background())
}
/// Muted text (inactive items, placeholders) - now bold bright white for headers
pub fn muted() -> Style {
Style::default()
@@ -312,5 +321,4 @@ impl Typography {
.bg(Theme::background())
.add_modifier(Modifier::BOLD)
}
}