From f2296ec82d17d98ecceeb2440920a32facf5cc4c Mon Sep 17 00:00:00 2001 From: Marco De Araujo Date: Wed, 24 Dec 2025 10:42:07 -0400 Subject: [PATCH] Fix --- src/locales/en-US/main.ftl | 12 ++++- src/locales/pt-BR/main.ftl | 12 ++++- src/ui/app.rs | 8 ++- src/ui/components/mod.rs | 1 + src/ui/components/monitor_list.rs | 86 +++++++++++++++++++++++++------ src/ui/components/status_line.rs | 77 +++++++++++++++++++++++++++ src/ui/dashboard/model.rs | 25 +++++++++ 7 files changed, 196 insertions(+), 25 deletions(-) create mode 100644 src/ui/components/status_line.rs diff --git a/src/locales/en-US/main.ftl b/src/locales/en-US/main.ftl index 080f61e..39b6859 100644 --- a/src/locales/en-US/main.ftl +++ b/src/locales/en-US/main.ftl @@ -8,7 +8,15 @@ invalid-json-heartbeat = ❌ Error parssing heartbeat JSON invalid-uptime-key-format = Invalid format for uptime key. Expected format "monitorID_period". Received key: {key} invalid-monitor-id = Invalid monitor ID: {id} invalid-period-hours = Invalid period in hours: {hours} -loading = Loading -dashboard-header = Dashboard +loading = Loading... monitors = Monitors unknown = Unknown +services = Services +monitor-not-found = No monitors found +status = Status +error = Error +dashboard-header = Status Dashboard +never = Never +auto-update-failed = Automatic update failed +update-fail = Failed to update data +now = Now diff --git a/src/locales/pt-BR/main.ftl b/src/locales/pt-BR/main.ftl index c668e5a..1bf9c23 100644 --- a/src/locales/pt-BR/main.ftl +++ b/src/locales/pt-BR/main.ftl @@ -8,7 +8,15 @@ invalid-json-heartbeat = ❌ Falha ao parsear JSON do heartbeat invalid-uptime-key-format = Formato inválido na chave de uptime. Chave esperada no formato "monitorID_periodo". Chave recebida: {key} invalid-monitor-id = ID de monitor inválido: {id} invalid-period-hours = Período em horas inválido: {hours} -loading = Carregando -dashboard-header = Dashboard +loading = Carregando... monitors = Monitors unknown = Desconhecido +services = Serviços +monitor-not-found = Nenhum monitor encontrado +status = Status +error = Erro +dashboard-header = Dashboard de Status +never = Nunca +auto-update-failed = Falha na atualização automática +update-fail = Falha ao atualizar dados +now = Agora diff --git a/src/ui/app.rs b/src/ui/app.rs index 354b7a8..3d88ef2 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,12 +1,10 @@ use crate::api::{UptimeKumaClient, UptimeKumaEndpoints}; use crate::core; -use crate::i18n::{t}; +use crate::i18n::t; use crate::ui::components::{render_header, render_monitor_list}; -use crate::ui::dashboard::model::{DashboardViewState}; +use crate::ui::dashboard::model::DashboardViewState; use crossterm::{ - event::{ - self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, - }, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index db85d66..ce50a70 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,5 +1,6 @@ pub mod header; pub mod monitor_list; +pub mod status_line; pub use header::render_header; pub use monitor_list::render_monitor_list; diff --git a/src/ui/components/monitor_list.rs b/src/ui/components/monitor_list.rs index 2cd60f5..698bd78 100644 --- a/src/ui/components/monitor_list.rs +++ b/src/ui/components/monitor_list.rs @@ -1,5 +1,6 @@ use crate::i18n::t; -use crate::ui::dashboard::model::{DashboardViewState, MonitorStatus}; +use crate::ui::dashboard::MonitorStatus; +use crate::ui::dashboard::model::DashboardViewState; use ratatui::style::Modifier; use ratatui::widgets::{List, ListItem, ListState}; use ratatui::{ @@ -10,6 +11,8 @@ use ratatui::{ widgets::{Block, Borders}, }; +const STATUS_LINE_LENGTH: usize = 20; + pub fn render_monitor_list( frame: &mut Frame, area: Rect, @@ -25,22 +28,16 @@ pub fn render_monitor_list( .monitors .iter() .map(|monitor| { - let status_icon = match monitor.status { - MonitorStatus::Up => "✅", - MonitorStatus::Down => "❌", - MonitorStatus::Unknown => "❓", - }; + let status_icon = get_status_emoji(&monitor.status); + let status_color = get_status_color(&monitor.status); + let status_line = create_status_line_spans(&monitor.status_history); - let status_color = match monitor.status { - MonitorStatus::Up => Color::Green, - MonitorStatus::Down => Color::Red, - MonitorStatus::Unknown => Color::Yellow, - }; - - let line = Line::from(vec![ + let mut spans = vec![ Span::styled( format!("{} ", status_icon), - Style::default().fg(status_color), + Style::default() + .fg(status_color) + .add_modifier(Modifier::BOLD), ), Span::styled( &monitor.name, @@ -56,9 +53,14 @@ pub fn render_monitor_list( format!(" | {}%", monitor.uptime_24h), Style::default().fg(Color::Magenta), ), - ]); + Span::raw(" | "), + ]; - ListItem::new(line) + spans.extend(status_line); + + let line = Line::from(spans); + let lines = vec![line]; + ListItem::new(lines) }) .collect(); @@ -69,3 +71,55 @@ pub fn render_monitor_list( frame.render_stateful_widget(list, area, list_state); } + +fn create_status_line_spans(status_history: &[MonitorStatus]) -> Vec> { + let mut spans = Vec::new(); + let recent_status: Vec<_> = status_history + .iter() + .rev() + .take(STATUS_LINE_LENGTH) + .collect(); + + for status in recent_status.iter().rev() { + let c = get_status_char(status); + let color = get_status_color(status); + + spans.push(Span::styled(c.to_string(), Style::default().fg(color))); + } + + if !spans.is_empty() { + spans.push(Span::raw(" ")); + spans.push(Span::styled( + "(24)", + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::ITALIC), + )); + } + + spans +} + +pub fn get_status_char(status: &MonitorStatus) -> char { + match status { + MonitorStatus::Up => '█', + MonitorStatus::Down => '█', + MonitorStatus::Unknown => '░', + } +} + +pub fn get_status_color(status: &MonitorStatus) -> Color { + match status { + MonitorStatus::Up => Color::Green, + MonitorStatus::Down => Color::Red, + MonitorStatus::Unknown => Color::Yellow, + } +} + +pub fn get_status_emoji(status: &MonitorStatus) -> &str { + match status { + MonitorStatus::Up => "✅", + MonitorStatus::Down => "❌", + MonitorStatus::Unknown => "❓", + } +} diff --git a/src/ui/components/status_line.rs b/src/ui/components/status_line.rs new file mode 100644 index 0000000..f21e08f --- /dev/null +++ b/src/ui/components/status_line.rs @@ -0,0 +1,77 @@ +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::Color, + widgets::{Block, Borders, Paragraph}, +}; + +use crate::{ + i18n::t, + ui::dashboard::model::{MonitorStatus, MonitorViewState}, +}; + +const STATUS_LINE_LENGTH: usize = 100; + +pub fn render_status_line(frame: &mut Frame, area: Rect, monitor: &MonitorViewState) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Length(1), Constraint::Length(1)]) + .split(area); + + let status_line = create_status_line(&monitor.status_history); + let status_line_widget = + Paragraph::new(status_line).block(Block::default().borders(Borders::NONE)); + + frame.render_widget(status_line_widget, chunks[0]); + + let time_markers = Paragraph::new("1h".to_string()) + .alignment(Alignment::Left) + .block(Block::default().borders(Borders::NONE)); + + frame.render_widget(time_markers, chunks[1]); + + let now_markers = Paragraph::new(t("now")) + .alignment(Alignment::Right) + .block(Block::default().borders(Borders::NONE)); + + frame.render_widget(now_markers, chunks[1]); +} + +fn create_status_line(status_history: &[MonitorStatus]) -> String { + let mut line = String::with_capacity(STATUS_LINE_LENGTH); + + let points = status_history + .iter() + .take(STATUS_LINE_LENGTH) + .chain( + std::iter::repeat(&MonitorStatus::Unknown) + .take(STATUS_LINE_LENGTH - status_history.len()), + ) + .collect::>(); + + for status in points { + match status { + MonitorStatus::Down => line.push_str("░"), + MonitorStatus::Up => line.push_str("█"), + MonitorStatus::Unknown => line.push_str(" "), + } + } + line +} + +pub fn get_status_emoji(status: &MonitorStatus) -> &str { + match status { + MonitorStatus::Up => "✅", + MonitorStatus::Down => "❌", + MonitorStatus::Unknown => "❓", + } +} + +pub fn get_status_color(status: &MonitorStatus) -> Color { + match status { + MonitorStatus::Up => Color::Green, + MonitorStatus::Down => Color::Red, + MonitorStatus::Unknown => Color::Yellow, + } +} diff --git a/src/ui/dashboard/model.rs b/src/ui/dashboard/model.rs index b353fcd..8b893a5 100644 --- a/src/ui/dashboard/model.rs +++ b/src/ui/dashboard/model.rs @@ -1,4 +1,5 @@ use crate::core::models::{UnifiedData, UnifiedMonitorData}; +use crate::data::heartbeat::model::HeartbeatEntry; use crate::i18n::t; #[derive(Debug, Clone, PartialEq)] @@ -17,6 +18,7 @@ pub struct MonitorViewState { pub response_time: String, pub uptime_24h: String, pub last_check: String, + pub status_history: Vec, } #[derive(Debug, Clone)] @@ -45,6 +47,8 @@ impl DashboardViewState { let mut monitors = Vec::new(); for monitor in data.monitors { + let status_history = get_status_history(&monitor.heartbeats); + let status = match monitor.heartbeats.last().map(|h| h.status) { Some(1) => MonitorStatus::Up, Some(0) => MonitorStatus::Down, @@ -77,6 +81,7 @@ impl DashboardViewState { response_time, uptime_24h, last_check, + status_history, }); } @@ -96,3 +101,23 @@ impl DashboardViewState { self.monitors.get(self.selected_index) } } + +fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec { + let mut history = heartbeats + .iter() + .rev() + .take(100) + .map(|h| match h.status { + 0 => MonitorStatus::Down, + 1 => MonitorStatus::Up, + _ => MonitorStatus::Unknown, + }) + .collect::>(); + + while history.len() < 100 { + history.push(MonitorStatus::Unknown); + } + + history.reverse(); + history +}