nodo_inspector 0.18.5

Telemetry terminal UI for NODO
// Copyright 2025 David Weikersdorfer

use crate::{style::*, InspectorModel, KeyCode, PAGE_SCROLL_STEP};
use nodo_runtime::proto::nodo as nodo_pb;
use ratatui::{
    prelude::{Buffer, Constraint, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Cell, Row, StatefulWidget, Table, Widget},
};
use std::{cell::RefCell, sync::Arc, time::Duration};

pub struct MonitorsController {
    pub model: Arc<RefCell<InspectorModel>>,
}

impl MonitorsController {
    pub fn on_key(&mut self, key: KeyCode) {
        match key {
            KeyCode::Down => self.select_next(),
            KeyCode::Up => self.select_previous(),
            KeyCode::PageDown => self.on_page_down(),
            KeyCode::PageUp => self.on_page_up(),
            KeyCode::Enter => {}
            _ => {}
        }
    }

    fn select_next(&mut self) {
        self.model.borrow_mut().monitors_table_state.select_next();
    }

    fn select_previous(&mut self) {
        self.model
            .borrow_mut()
            .monitors_table_state
            .select_previous();
    }

    fn on_page_down(&mut self) {
        for _ in 0..PAGE_SCROLL_STEP {
            self.select_next()
        }
    }

    fn on_page_up(&mut self) {
        for _ in 0..PAGE_SCROLL_STEP {
            self.select_previous()
        }
    }
}

pub struct MonitorsView {
    pub model: Arc<RefCell<InspectorModel>>,
}

impl Widget for &MonitorsView {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let mut model = self.model.borrow_mut();

        let mut entries = model.tree.monitors.clone();
        entries.sort_by(|(_, _, m1), (_, _, m2)| {
            let status_to_priority = |status: i32| match nodo_pb::MonitorStatus::try_from(status) {
                Err(_) => 0,
                Ok(nodo_pb::MonitorStatus::Unspecified) => 1,
                Ok(nodo_pb::MonitorStatus::Critical) => 2,
                Ok(nodo_pb::MonitorStatus::Warning) => 3,
                Ok(nodo_pb::MonitorStatus::Nominal) => 4,
            };

            let p1 = status_to_priority(m1.status);
            let p2 = status_to_priority(m2.status);
            p1.cmp(&p2)
        });

        let combined_rows: Vec<_> = entries
            .into_iter()
            .map(|(node_name, meta, mon)| {
                Row::new(vec![
                    {
                        let (label, color) = match nodo_pb::MonitorStatus::try_from(mon.status) {
                            Ok(nodo_pb::MonitorStatus::Unspecified) => {
                                ("N/A", STYLE_COLOR_UNAVAILABLE)
                            }
                            Ok(nodo_pb::MonitorStatus::Nominal) => ("Nominal", STYLE_COLOR_OK),
                            Ok(nodo_pb::MonitorStatus::Warning) => ("Warning", STYLE_COLOR_WARN),
                            Ok(nodo_pb::MonitorStatus::Critical) => ("Critical", STYLE_COLOR_ERR),
                            _ => ("(error)", STYLE_COLOR_UNAVAILABLE),
                        };

                        Cell::from(Span::styled(label.to_string(), Style::default().fg(color)))
                    },
                    Cell::from(Span::styled(meta.info, Style::default().fg(Color::Gray))),
                    Cell::from(Span::styled(
                        node_name,
                        Style::default().fg(STYLE_COLOR_NODE_NAME),
                    )),
                    {
                        meta.key
                            .as_ref()
                            .map(|key| format_gauge_key(key))
                            .unwrap_or_else(|| {
                                Cell::from(Span::styled(
                                    "N/A",
                                    Style::default().fg(STYLE_COLOR_ERR),
                                ))
                            })
                    },
                    {
                        let label_opt =
                            mon.value.as_ref().and_then(|cell| cell.value.as_ref()).map(
                                |v| match v {
                                    nodo_pb::gauge_value::Value::Bool(v) => format!("{v}"),
                                    nodo_pb::gauge_value::Value::Int64(v) => format!("{v}"),
                                    nodo_pb::gauge_value::Value::Usize(v) => format!("{v}"),
                                    nodo_pb::gauge_value::Value::Float64(v) => format!("{v}"),
                                    nodo_pb::gauge_value::Value::String(v) => format!("{v}"),
                                    nodo_pb::gauge_value::Value::Pubtime(v) => format_time(v),
                                    nodo_pb::gauge_value::Value::Acqtime(v) => format_time(v),
                                },
                            );

                        let (label, color) = match label_opt {
                            Some(label) => (label, STYLE_COLOR_VALUE_DEFAULT),
                            None => ("N/A".into(), STYLE_COLOR_UNAVAILABLE),
                        };

                        Cell::from(Span::styled(label.to_string(), Style::default().fg(color)))
                    },
                    Cell::from(match &mon.pubtime {
                        Some(cell) => Span::styled(
                            format_time(cell),
                            Style::default().fg(STYLE_COLOR_VALUE_DEFAULT),
                        ),
                        None => Span::styled(
                            String::from("N/A"),
                            Style::default().fg(STYLE_COLOR_UNAVAILABLE),
                        ),
                    }),
                ])
            })
            .collect();

        // Create the combined table.
        let table = Table::new(
            combined_rows,
            &[
                Constraint::Fill(1),
                Constraint::Fill(5),
                Constraint::Fill(2),
                Constraint::Fill(2),
                Constraint::Fill(2),
                Constraint::Fill(1),
            ],
        )
        .header(
            Row::new(vec![
                "Status".into(),
                "Info".into(),
                "Node".to_string(),
                "Gauge".into(),
                "Value".into(),
                "Time".into(),
            ])
            .style(
                Style::default()
                    .add_modifier(Modifier::BOLD)
                    .add_modifier(Modifier::REVERSED),
            ),
        )
        .style(Style::new().fg(Color::White))
        .row_highlight_style(Style::new().add_modifier(Modifier::REVERSED));

        // Render the combined table.
        StatefulWidget::render(table, area, buf, &mut model.monitors_table_state);
    }
}

fn format_gauge_key(key: &nodo_pb::GaugeKey) -> Cell<'static> {
    nodo_pb::GaugeKeyKind::try_from(key.kind)
        .ok()
        .map(|kind| match kind {
            nodo_pb::GaugeKeyKind::Unspecified => Cell::from(Span::styled(
                "(error)",
                Style::default().fg(STYLE_COLOR_ERR),
            )),
            nodo_pb::GaugeKeyKind::SignalValue => Cell::from(Line::from(vec![
                Span::styled(
                    key.name.clone(),
                    Style::default().fg(STYLE_COLOR_DETAIL_NAME),
                ),
                Span::raw(" "),
                Span::styled("[VALUE]", Style::default().fg(STYLE_COLOR_TYPENAME)),
            ])),
            nodo_pb::GaugeKeyKind::SignalPubtime => Cell::from(Line::from(vec![
                Span::styled(
                    key.name.clone(),
                    Style::default().fg(STYLE_COLOR_DETAIL_NAME),
                ),
                Span::raw(" "),
                Span::styled("[PUBTIME]", Style::default().fg(STYLE_COLOR_TYPENAME)),
            ])),
            nodo_pb::GaugeKeyKind::RxAvailable => Cell::from(Line::from(vec![
                Span::styled(
                    key.name.clone(),
                    Style::default().fg(STYLE_COLOR_DETAIL_NAME),
                ),
                Span::raw(" "),
                Span::styled("[RX_AVAILABLE]", Style::default().fg(STYLE_COLOR_TYPENAME)),
            ])),
            nodo_pb::GaugeKeyKind::TxTotal => Cell::from(Line::from(vec![
                Span::styled(
                    key.name.clone(),
                    Style::default().fg(STYLE_COLOR_DETAIL_NAME),
                ),
                Span::raw(" "),
                Span::styled("[TX_TOTAL]", Style::default().fg(STYLE_COLOR_TYPENAME)),
            ])),
            nodo_pb::GaugeKeyKind::TxPubtime => Cell::from(Line::from(vec![
                Span::styled(
                    key.name.clone(),
                    Style::default().fg(STYLE_COLOR_DETAIL_NAME),
                ),
                Span::raw(" "),
                Span::styled("[TX_PUBTIME]", Style::default().fg(STYLE_COLOR_TYPENAME)),
            ])),
        })
        .unwrap_or_else(move || {
            Cell::from(Span::styled(
                "(invalid)",
                Style::default().fg(STYLE_COLOR_ERR),
            ))
        })
}

fn format_time(cell: &nodo_pb::Duration) -> String {
    format!(
        "{:0.03}",
        Duration::new(cell.secs as u64, cell.nanos).as_secs_f32()
    )
}