This commit is contained in:
Marco De Araujo 2025-12-24 10:42:07 -04:00
parent ae9065a1e0
commit f2296ec82d
7 changed files with 196 additions and 25 deletions

View file

@ -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-uptime-key-format = Invalid format for uptime key. Expected format "monitorID_period". Received key: {key}
invalid-monitor-id = Invalid monitor ID: {id} invalid-monitor-id = Invalid monitor ID: {id}
invalid-period-hours = Invalid period in hours: {hours} invalid-period-hours = Invalid period in hours: {hours}
loading = Loading loading = Loading...
dashboard-header = Dashboard
monitors = Monitors monitors = Monitors
unknown = Unknown 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

View file

@ -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-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-monitor-id = ID de monitor inválido: {id}
invalid-period-hours = Período em horas inválido: {hours} invalid-period-hours = Período em horas inválido: {hours}
loading = Carregando loading = Carregando...
dashboard-header = Dashboard
monitors = Monitors monitors = Monitors
unknown = Desconhecido 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

View file

@ -1,12 +1,10 @@
use crate::api::{UptimeKumaClient, UptimeKumaEndpoints}; use crate::api::{UptimeKumaClient, UptimeKumaEndpoints};
use crate::core; use crate::core;
use crate::i18n::{t}; use crate::i18n::t;
use crate::ui::components::{render_header, render_monitor_list}; use crate::ui::components::{render_header, render_monitor_list};
use crate::ui::dashboard::model::{DashboardViewState}; use crate::ui::dashboard::model::DashboardViewState;
use crossterm::{ use crossterm::{
event::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
},
execute, execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
}; };

View file

@ -1,5 +1,6 @@
pub mod header; pub mod header;
pub mod monitor_list; pub mod monitor_list;
pub mod status_line;
pub use header::render_header; pub use header::render_header;
pub use monitor_list::render_monitor_list; pub use monitor_list::render_monitor_list;

View file

@ -1,5 +1,6 @@
use crate::i18n::t; 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::style::Modifier;
use ratatui::widgets::{List, ListItem, ListState}; use ratatui::widgets::{List, ListItem, ListState};
use ratatui::{ use ratatui::{
@ -10,6 +11,8 @@ use ratatui::{
widgets::{Block, Borders}, widgets::{Block, Borders},
}; };
const STATUS_LINE_LENGTH: usize = 20;
pub fn render_monitor_list( pub fn render_monitor_list(
frame: &mut Frame, frame: &mut Frame,
area: Rect, area: Rect,
@ -25,22 +28,16 @@ pub fn render_monitor_list(
.monitors .monitors
.iter() .iter()
.map(|monitor| { .map(|monitor| {
let status_icon = match monitor.status { let status_icon = get_status_emoji(&monitor.status);
MonitorStatus::Up => "", let status_color = get_status_color(&monitor.status);
MonitorStatus::Down => "", let status_line = create_status_line_spans(&monitor.status_history);
MonitorStatus::Unknown => "",
};
let status_color = match monitor.status { let mut spans = vec![
MonitorStatus::Up => Color::Green,
MonitorStatus::Down => Color::Red,
MonitorStatus::Unknown => Color::Yellow,
};
let line = Line::from(vec![
Span::styled( Span::styled(
format!("{} ", status_icon), format!("{} ", status_icon),
Style::default().fg(status_color), Style::default()
.fg(status_color)
.add_modifier(Modifier::BOLD),
), ),
Span::styled( Span::styled(
&monitor.name, &monitor.name,
@ -56,9 +53,14 @@ pub fn render_monitor_list(
format!(" | {}%", monitor.uptime_24h), format!(" | {}%", monitor.uptime_24h),
Style::default().fg(Color::Magenta), 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(); .collect();
@ -69,3 +71,55 @@ pub fn render_monitor_list(
frame.render_stateful_widget(list, area, list_state); frame.render_stateful_widget(list, area, list_state);
} }
fn create_status_line_spans(status_history: &[MonitorStatus]) -> Vec<Span<'static>> {
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 => "",
}
}

View file

@ -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::<Vec<_>>();
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,
}
}

View file

@ -1,4 +1,5 @@
use crate::core::models::{UnifiedData, UnifiedMonitorData}; use crate::core::models::{UnifiedData, UnifiedMonitorData};
use crate::data::heartbeat::model::HeartbeatEntry;
use crate::i18n::t; use crate::i18n::t;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -17,6 +18,7 @@ pub struct MonitorViewState {
pub response_time: String, pub response_time: String,
pub uptime_24h: String, pub uptime_24h: String,
pub last_check: String, pub last_check: String,
pub status_history: Vec<MonitorStatus>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -45,6 +47,8 @@ impl DashboardViewState {
let mut monitors = Vec::new(); let mut monitors = Vec::new();
for monitor in data.monitors { for monitor in data.monitors {
let status_history = get_status_history(&monitor.heartbeats);
let status = match monitor.heartbeats.last().map(|h| h.status) { let status = match monitor.heartbeats.last().map(|h| h.status) {
Some(1) => MonitorStatus::Up, Some(1) => MonitorStatus::Up,
Some(0) => MonitorStatus::Down, Some(0) => MonitorStatus::Down,
@ -77,6 +81,7 @@ impl DashboardViewState {
response_time, response_time,
uptime_24h, uptime_24h,
last_check, last_check,
status_history,
}); });
} }
@ -96,3 +101,23 @@ impl DashboardViewState {
self.monitors.get(self.selected_index) self.monitors.get(self.selected_index)
} }
} }
fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
let mut history = heartbeats
.iter()
.rev()
.take(100)
.map(|h| match h.status {
0 => MonitorStatus::Down,
1 => MonitorStatus::Up,
_ => MonitorStatus::Unknown,
})
.collect::<Vec<_>>();
while history.len() < 100 {
history.push(MonitorStatus::Unknown);
}
history.reverse();
history
}