met 1.0.0-beta-preview

Metrale Engine: pure Rust LLM inference server (HTTP API, scheduler, TUI, CLI).
// SPDX-License-Identifier: MIT OR Apache-2.0

//! 2026-09-26: The Network section: the TP/EP topology as node cards (one
//! centred card for a single node, up to four joined by connectors), over a
//! detail pane for the selected node.
//!
//! Owner: server tui.
//! Invariants: none beyond the types.
//!
//! A worker's state and the link latency are not measured, and the pane says
//! so instead of drawing a number.

use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::Modifier;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;

use super::panel;
use crate::tui::app::App;
use crate::tui::theme;

pub fn draw(f: &mut Frame, app: &App, area: Rect) {
    let world = app.args.world_size.max(1);
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Percentage(60), Constraint::Min(4)])
        .split(area);
    if world == 1 {
        draw_single(f, app, rows[0]);
    } else {
        draw_multi(f, app, rows[0], world);
    }
    draw_detail(f, app, rows[1]);
}

fn card_lines(app: &App, rank: usize) -> Vec<Line<'static>> {
    let s = &app.stats;
    let is_head = rank == 0;
    let role = if is_head { "HEAD" } else { "WORKER" };
    let mut lines = vec![Line::from(Span::styled(
        format!("⬡ rank {rank} · {role}"),
        theme::text().add_modifier(Modifier::BOLD),
    ))];
    if is_head {
        lines.push(Line::from(Span::styled(
            "NVIDIA GB10 · local".to_string(),
            theme::text2(),
        )));
        // 2026-09-26: System memory from /proc/meminfo
        // (`metrics_poll::host_mem_gb`), used/total, which answers with or
        // without a model; the GPU figures stay 0.0 until a device reading
        // lands. "—" while /proc/meminfo has not been read.
        let total = s.host_total_gb;
        if total > 0.0 {
            let used = (total - s.host_avail_gb).max(0.0);
            let w = 18usize;
            let filled = ((used / total).clamp(0.0, 1.0) * w as f64) as usize;
            let bar: String = "█".repeat(filled) + &"░".repeat(w - filled);
            lines.push(Line::from(vec![
                Span::styled("mem ", theme::dim()),
                Span::styled(bar, theme::brand_cyan()),
                Span::styled(format!(" {used:.0}/{total:.0} GB"), theme::text2()),
            ]));
        } else {
            lines.push(Line::from(vec![
                Span::styled("mem ", theme::dim()),
                Span::styled("—", theme::dim()),
            ]));
        }
        lines.push(Line::from(Span::styled(
            format!("{}:{}", app.args.bind, app.args.port),
            theme::text2(),
        )));
    } else {
        lines.push(Line::from(Span::styled(
            format!("{}:{}", app.args.master_addr, app.args.master_port),
            theme::text2(),
        )));
        lines.push(Line::from(Span::styled(
            "state not visible from head (no worker HTTP)".to_string(),
            theme::dim(),
        )));
    }
    lines
}

fn draw_single(f: &mut Frame, app: &App, area: Rect) {
    let w = 44.min(area.width.saturating_sub(4));
    let h = 8.min(area.height.saturating_sub(2));
    let card = Rect {
        x: area.x + (area.width.saturating_sub(w)) / 2,
        y: area.y + (area.height.saturating_sub(h)) / 2,
        width: w,
        height: h,
    };
    // 2026-09-26: Decorative dim chevrons either side of the card.
    if card.x >= area.x + 6 {
        let deco = |x: u16| Rect {
            x,
            y: card.y + h / 2,
            width: 4,
            height: 1,
        };
        let l = Line::from(Span::styled(
            "❯❯❯",
            theme::brand_purple().add_modifier(Modifier::DIM),
        ));
        f.render_widget(Paragraph::new(l.clone()), deco(area.x + 1));
        f.render_widget(Paragraph::new(l), deco(card.x + w + 2));
    }
    // 2026-09-26: Heartbeat: the border alternates green and dim every 20
    // ticks, 2 s at the 100 ms `events::TICK`.
    let beat = (app.tick / 20).is_multiple_of(2);
    let border = if beat {
        theme::brand_green()
    } else {
        theme::dim()
    };
    let block = panel("⬡ rank 0 · HEAD ─".into(), false).border_style(border);
    f.render_widget(Paragraph::new(card_lines(app, 0)).block(block), card);
    let caption = format!(
        "tp {} · ep {} · world 1 — all local",
        app.args.tp_size, app.args.ep_size
    );
    f.render_widget(
        Paragraph::new(Span::styled(caption, theme::text2())).alignment(Alignment::Center),
        Rect {
            y: (card.y + h).min(area.y + area.height - 1),
            height: 1,
            ..area
        },
    );
}

fn draw_multi(f: &mut Frame, app: &App, area: Rect, world: usize) {
    let n = world.min(4);
    let mut constraints = Vec::new();
    for _ in 0..n {
        constraints.push(Constraint::Ratio(1, n as u32));
    }
    let cols = Layout::default()
        .direction(Direction::Horizontal)
        .constraints(constraints)
        .split(area);
    for rank in 0..n {
        let selected = app.network_selected == rank;
        let border = if selected {
            theme::brand_cyan()
        } else {
            theme::brand_green()
        };
        let title = format!("⬡ rank {rank} ─");
        let block = panel(title, selected).border_style(border);
        let cell = cols[rank];
        let card = Rect {
            x: cell.x + 1,
            y: cell.y + 1,
            width: cell.width.saturating_sub(2),
            height: cell.height.saturating_sub(3).min(8),
        };
        f.render_widget(Paragraph::new(card_lines(app, rank)).block(block), card);
        if rank + 1 < n {
            let link = Rect {
                x: cell.x + cell.width.saturating_sub(1),
                y: card.y + card.height / 2,
                width: 2.min(area.width),
                height: 1,
            };
            f.render_widget(
                Paragraph::new(Span::styled("══", theme::brand_cyan())),
                link,
            );
        }
    }
    let label = if app.args.ep_size > 1 {
        format!("═ NCCL ═ ep {} · experts sharded", app.args.ep_size)
    } else {
        format!("═ NCCL ═ tp {} · ring", app.args.tp_size)
    };
    f.render_widget(
        Paragraph::new(Span::styled(label, theme::brand_cyan())).alignment(Alignment::Center),
        Rect {
            y: area.y + area.height.saturating_sub(1),
            height: 1,
            ..area
        },
    );
}

fn draw_detail(f: &mut Frame, app: &App, area: Rect) {
    let block = panel(format!("NODE {} ─ detail ─", app.network_selected), false);
    let mut lines = vec![Line::from(vec![
        Span::styled(" role ", theme::dim()),
        Span::styled(
            if app.network_selected == 0 {
                "head (serves HTTP, owns scheduler)"
            } else {
                "worker (EP shard, no HTTP)"
            },
            theme::text(),
        ),
    ])];
    lines.push(Line::from(vec![
        Span::styled(" bootstrap ", theme::dim()),
        Span::styled(
            format!(
                "{}:{} (NCCL master)",
                app.args.master_addr, app.args.master_port
            ),
            theme::text(),
        ),
    ]));
    lines.push(Line::from(vec![
        Span::styled(" topology ", theme::dim()),
        Span::styled(
            format!(
                "world {} · tp {} · ep {} · this rank {}",
                app.args.world_size, app.args.tp_size, app.args.ep_size, app.args.rank
            ),
            theme::text(),
        ),
    ]));
    lines.push(Line::from(vec![
        Span::styled(" latency ", theme::dim()),
        Span::styled(
            "— (probes race the collectives; deliberately not measured live)",
            theme::dim(),
        ),
    ]));
    f.render_widget(Paragraph::new(lines).block(block), area);
}

#[cfg(test)]
#[path = "network_tab_tests.rs"]
mod tests;