#![allow(dead_code)]
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols,
text::{Line, Span},
widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Sparkline, Table, Wrap},
Frame,
};
use crate::broker::stats::{
format_credits, format_uptime, TaskOfferRecord, TransactionRecord, TransactionStatus,
};
use crate::tui::theme::Theme;
use crate::tui::views::snapshot::{Connection, Snapshot, SourceMode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Panel {
Transactions,
Workers,
Metrics,
}
#[derive(Debug, Clone, Copy)]
pub struct MonitorUi {
pub selected_panel: Panel,
pub show_help: bool,
}
impl Default for MonitorUi {
fn default() -> Self {
MonitorUi {
selected_panel: Panel::Transactions,
show_help: false,
}
}
}
pub fn render(frame: &mut Frame, snap: &Snapshot, theme: &Theme, ui: &MonitorUi) {
let area = frame.size();
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(10), Constraint::Length(1), ])
.split(area);
render_header(frame, main_chunks[0], snap, theme);
match (&snap.mode, &snap.connection) {
(SourceMode::Remote, Connection::Connecting) => {
let loading =
Paragraph::new("Connecting… (fetch runs in background, UI stays responsive)")
.style(Style::default().fg(theme.muted))
.alignment(Alignment::Center);
frame.render_widget(loading, main_chunks[1]);
}
(SourceMode::Remote, Connection::Error(e)) => {
let error_text = Paragraph::new(format!("Error: {}", e))
.style(Style::default().fg(theme.error))
.alignment(Alignment::Center);
frame.render_widget(error_text, main_chunks[1]);
}
_ => {
render_content(frame, main_chunks[1], snap, theme, ui);
}
}
render_footer(frame, main_chunks[2], snap, theme);
if ui.show_help {
render_help_overlay(frame, area, snap, theme);
}
}
fn render_header(frame: &mut Frame, area: Rect, snap: &Snapshot, theme: &Theme) {
let is_remote = snap.mode == SourceMode::Remote;
let mode_indicator = match snap.mode {
SourceMode::Local => Span::styled(
" LOCAL ",
Style::default().bg(theme.warning).fg(Color::Black).bold(),
),
SourceMode::P2p => Span::styled(
" P2P ",
Style::default().bg(theme.accent).fg(Color::Black).bold(),
),
SourceMode::Remote => Span::styled(
" REMOTE ",
Style::default().bg(Color::Magenta).fg(Color::Black).bold(),
),
};
let title_line = if is_remote {
let connection_indicator = match snap.connection {
Connection::Error(_) => Span::styled(" ● ", Style::default().fg(theme.error)),
Connection::Connected => Span::styled(" ● ", Style::default().fg(theme.success)),
Connection::Connecting => Span::styled(" ○ ", Style::default().fg(theme.warning)),
};
vec![
Span::styled(" ◆ ", Style::default().fg(theme.accent).bold()),
Span::styled(snap.title.clone(), Style::default().fg(theme.fg).bold()),
Span::raw(" "),
mode_indicator,
Span::raw(" "),
connection_indicator,
Span::styled(snap.host_port.clone(), Style::default().fg(theme.muted)),
]
} else {
let ledger_indicator = if snap.ledger_connected {
Span::styled(" ● ", Style::default().fg(theme.success))
} else {
Span::styled(" ○ ", Style::default().fg(theme.warning))
};
vec![
Span::styled(" ◆ ", Style::default().fg(theme.accent).bold()),
Span::styled(snap.title.clone(), Style::default().fg(theme.fg).bold()),
Span::raw(" "),
mode_indicator,
Span::raw(" "),
ledger_indicator,
Span::styled(
if snap.ledger_connected { "PG" } else { "Local" },
Style::default().fg(theme.muted),
),
]
};
let uptime = format!(" ⏱ {} ", format_uptime(snap.metrics.uptime_secs));
let wireguard_span = match &snap.wireguard_ip {
Some(ip) => Span::styled(
format!(" wireguard: {}", ip),
Style::default().fg(theme.accent),
),
None if is_remote => Span::styled("", Style::default()),
None => Span::styled(" wireguard: offline", Style::default().fg(theme.muted)),
};
let second_line = if is_remote {
Line::from(vec![
Span::styled(
format!(" Connected to {}", snap.host_port),
Style::default().fg(theme.muted),
),
wireguard_span,
Span::styled(uptime, Style::default().fg(theme.muted)),
])
} else {
Line::from(vec![
Span::styled(
format!(" {}", snap.host_port),
Style::default().fg(theme.muted),
),
wireguard_span,
Span::styled(uptime, Style::default().fg(theme.muted)),
])
};
let header_text = vec![Line::from(title_line), second_line];
let header = Paragraph::new(header_text).block(
Block::default()
.borders(Borders::BOTTOM)
.border_style(Style::default().fg(theme.border))
.border_type(BorderType::Plain),
);
frame.render_widget(header, area);
}
fn render_content(frame: &mut Frame, area: Rect, snap: &Snapshot, theme: &Theme, ui: &MonitorUi) {
let content_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
.split(area);
let is_remote = snap.mode == SourceMode::Remote;
let left_chunks = if is_remote {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(6),
Constraint::Min(6),
Constraint::Length(10),
])
.split(content_chunks[0])
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(6), Constraint::Min(8)])
.split(content_chunks[0])
};
render_task_pile(frame, left_chunks[0], snap, theme);
render_transactions(frame, left_chunks[1], snap, theme, ui);
if is_remote {
render_stream(frame, left_chunks[2], &snap.events, theme);
}
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(content_chunks[1]);
render_workers(frame, right_chunks[0], snap, theme, ui);
render_metrics(frame, right_chunks[1], snap, theme, ui);
}
fn render_task_pile(frame: &mut Frame, area: Rect, snap: &Snapshot, theme: &Theme) {
let is_remote = snap.mode == SourceMode::Remote;
let offers = &snap.task_offers;
let title = if is_remote {
" Task pile → lock → execute → finalized "
} else {
" Task pile (broadcast) "
};
let block = Block::default()
.title(Line::from(vec![Span::styled(
title,
Style::default().fg(theme.fg).bold(),
)]))
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border))
.border_type(BorderType::Plain);
let inner = block.inner(area);
frame.render_widget(block, area);
if offers.is_empty() {
let text = if is_remote {
"No offers yet — pull updates from API"
} else {
"No task offers yet"
};
let msg = Paragraph::new(text)
.style(Style::default().fg(theme.muted))
.alignment(Alignment::Center);
frame.render_widget(msg, inner);
return;
}
if is_remote {
render_task_pile_remote(frame, inner, offers, &snap.transactions, theme);
} else {
render_task_pile_local(frame, inner, offers, theme);
}
}
fn render_task_pile_local(
frame: &mut Frame,
inner: Rect,
offers: &[TaskOfferRecord],
theme: &Theme,
) {
let header_cells = ["TIME", "TASK", "USER", "MAX ₡/hr", "BROKER"]
.iter()
.map(|h| {
Cell::from(*h).style(
Style::default()
.fg(theme.muted)
.add_modifier(Modifier::BOLD),
)
});
let header = Row::new(header_cells).height(1);
let rows: Vec<Row> = offers
.iter()
.map(|o| {
let time = o.timestamp.format("%H:%M:%S").to_string();
let task_short = o.task_id.chars().take(10).collect::<String>();
let user_short = o.requester_user_id.chars().take(10).collect::<String>();
let price = format!("{:.2}", o.max_price_per_hour);
Row::new(vec![
Cell::from(time),
Cell::from(task_short).style(Style::default().fg(theme.accent)),
Cell::from(user_short),
Cell::from(price),
Cell::from(o.source_broker.chars().take(12).collect::<String>())
.style(Style::default().fg(theme.muted)),
])
})
.collect();
let widths = [
Constraint::Length(8),
Constraint::Length(10),
Constraint::Length(10),
Constraint::Length(10),
Constraint::Min(8),
];
let table = Table::new(rows, widths).header(header);
frame.render_widget(table, inner);
}
fn render_task_pile_remote(
frame: &mut Frame,
inner: Rect,
offers: &[TaskOfferRecord],
transactions: &[TransactionRecord],
theme: &Theme,
) {
let header_cells = [
"TIME",
"TASK",
"USER",
"MAX ₡/hr",
"STATUS",
"WORKER / COST",
]
.iter()
.map(|h| {
Cell::from(*h).style(
Style::default()
.fg(theme.muted)
.add_modifier(Modifier::BOLD),
)
});
let header = Row::new(header_cells).height(1);
let rows: Vec<Row> = offers
.iter()
.map(|o| {
let time = o.timestamp.format("%H:%M:%S").to_string();
let task_short = o.task_id.chars().take(8).collect::<String>();
let user_short = o.requester_user_id.chars().take(8).collect::<String>();
let price = format!("{:.2}", o.max_price_per_hour);
let completed = transactions
.iter()
.find(|tx| tx.request_id.as_deref() == Some(o.task_id.as_str()));
let (status_cell, trace_cell) = match completed {
Some(tx) => (
Cell::from("DONE").style(Style::default().fg(theme.success)),
Cell::from(format!(
"{} {} ₡{:.4} {:.0}ms",
tx.worker.as_deref().unwrap_or("-"),
tx.owner_id.as_deref().unwrap_or(""),
tx.cost,
tx.duration_ms
))
.style(Style::default().fg(theme.muted)),
),
None => (
Cell::from("pending").style(Style::default().fg(theme.warning)),
Cell::from("— locked / reserve / execute…")
.style(Style::default().fg(theme.muted)),
),
};
Row::new(vec![
Cell::from(time),
Cell::from(task_short).style(Style::default().fg(theme.accent)),
Cell::from(user_short),
Cell::from(price),
status_cell,
trace_cell,
])
})
.collect();
let widths = [
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(6),
Constraint::Min(10),
];
let table = Table::new(rows, widths).header(header);
frame.render_widget(table, inner);
}
fn render_transactions(
frame: &mut Frame,
area: Rect,
snap: &Snapshot,
theme: &Theme,
ui: &MonitorUi,
) {
let is_selected = ui.selected_panel == Panel::Transactions;
let border_color = if is_selected {
theme.accent
} else {
theme.border
};
let block = Block::default()
.title(Line::from(vec![Span::styled(
" Transactions ",
Style::default().fg(theme.fg).bold(),
)]))
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.border_type(if is_selected {
BorderType::Rounded
} else {
BorderType::Plain
});
let inner_area = block.inner(area);
frame.render_widget(block, area);
let header_cells = ["TIME", "FROM", "WORKER", "OWNER", "PRICE", "COST", "MS"]
.iter()
.map(|h| {
Cell::from(*h).style(
Style::default()
.fg(theme.muted)
.add_modifier(Modifier::BOLD),
)
});
let header = Row::new(header_cells).height(1);
let rows: Vec<Row> = snap
.transactions
.iter()
.enumerate()
.map(|(idx, tx)| {
let status_style = match tx.status {
TransactionStatus::Ok => Style::default().fg(theme.success),
TransactionStatus::Fail => Style::default().fg(theme.error),
TransactionStatus::Pending => Style::default().fg(theme.warning),
};
let status_icon = match tx.status {
TransactionStatus::Ok => "✓",
TransactionStatus::Fail => "✗",
TransactionStatus::Pending => "◌",
};
let time = tx.timestamp.format("%H:%M:%S").to_string();
let cost = format!("{:.6}", tx.cost);
let latency = if tx.duration_ms > 0.0 {
format!("{:.0}", tx.duration_ms)
} else {
"-".to_string()
};
let worker = tx.worker.as_deref().unwrap_or("-");
let worker_display = match (&tx.worker_pid, &tx.worker_ip) {
(Some(pid), Some(ip)) => format!("{} {}@{}", worker, pid, ip),
(Some(pid), None) => format!("{} {}", worker, pid),
(None, Some(ip)) => format!("{} @{}", worker, ip),
(None, None) => worker.to_string(),
};
let price_str = tx
.price_per_hour
.map(|p| format!("{:.2}", p))
.unwrap_or_else(|| "-".to_string());
let owner_str = tx.owner_id.as_deref().unwrap_or("-");
let row_style = if idx % 2 == 0 {
Style::default()
} else {
Style::default().bg(theme.row_alt)
};
Row::new(vec![
Cell::from(format!("{} {}", status_icon, time)).style(status_style),
Cell::from(tx.user_id.chars().take(10).collect::<String>())
.style(Style::default().fg(theme.accent)),
Cell::from(worker_display.chars().take(18).collect::<String>())
.style(Style::default().fg(theme.muted)),
Cell::from(owner_str.chars().take(10).collect::<String>()),
Cell::from(price_str),
Cell::from(cost),
Cell::from(latency),
])
.style(row_style)
})
.collect();
let widths = [
Constraint::Length(11),
Constraint::Length(10),
Constraint::Length(18),
Constraint::Length(10),
Constraint::Length(8),
Constraint::Length(10),
Constraint::Min(4),
];
let table = Table::new(rows, widths)
.header(header)
.highlight_style(Style::default().bg(theme.highlight));
frame.render_widget(table, inner_area);
}
fn render_stream(frame: &mut Frame, area: Rect, event_log: &[String], theme: &Theme) {
let block = Block::default()
.title(Line::from(vec![Span::styled(
" Stream (results) ",
Style::default().fg(theme.fg).bold(),
)]))
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border))
.border_type(BorderType::Plain);
let inner = block.inner(area);
frame.render_widget(block, area);
let lines: Vec<Line> = if event_log.is_empty() {
vec![Line::from(Span::styled(
"Task offers and execute results will appear here as they happen.",
Style::default().fg(theme.muted),
))]
} else {
let start = event_log.len().saturating_sub(inner.height as usize);
event_log[start..]
.iter()
.map(|s| Line::from(Span::raw(s.as_str())))
.collect()
};
let para = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(para, inner);
}
fn render_workers(frame: &mut Frame, area: Rect, snap: &Snapshot, theme: &Theme, ui: &MonitorUi) {
let is_selected = ui.selected_panel == Panel::Workers;
let border_color = if is_selected {
theme.accent
} else {
theme.border
};
let workers = &snap.workers;
let healthy_count = workers
.iter()
.filter(|w| w.status == crate::tui::views::snapshot::WorkerKind::Healthy)
.count();
let block = Block::default()
.title(Line::from(vec![
Span::styled(" Workers ", Style::default().fg(theme.fg).bold()),
Span::styled(
format!(" {}/{} ", healthy_count, workers.len()),
Style::default().fg(theme.muted),
),
]))
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.border_type(if is_selected {
BorderType::Rounded
} else {
BorderType::Plain
});
let inner_area = block.inner(area);
frame.render_widget(block, area);
if workers.is_empty() {
let empty_msg = Paragraph::new("No workers connected")
.style(Style::default().fg(theme.muted))
.alignment(Alignment::Center);
frame.render_widget(empty_msg, inner_area);
return;
}
let rows: Vec<Row> = workers
.iter()
.map(|w| {
use crate::tui::views::snapshot::WorkerKind;
let status_color = match w.status {
WorkerKind::Healthy => theme.success,
WorkerKind::Busy => theme.accent,
WorkerKind::Unhealthy => theme.error,
WorkerKind::Draining => theme.warning,
WorkerKind::Unknown => theme.muted,
};
let name = w.name.chars().take(15).collect::<String>();
let resources = format!("{}c {:.1}G", w.cpus_available as i32, w.memory_gib);
let latency = format!("{:.0}ms", w.avg_latency_ms);
Row::new(vec![
Cell::from(w.status.icon()).style(Style::default().fg(status_color)),
Cell::from(name),
Cell::from(resources).style(Style::default().fg(theme.muted)),
Cell::from(latency).style(Style::default().fg(theme.muted)),
])
})
.collect();
let widths = [
Constraint::Length(2),
Constraint::Min(10),
Constraint::Length(10),
Constraint::Length(8),
];
let table = Table::new(rows, widths);
frame.render_widget(table, inner_area);
}
fn render_metrics(frame: &mut Frame, area: Rect, snap: &Snapshot, theme: &Theme, ui: &MonitorUi) {
let is_selected = ui.selected_panel == Panel::Metrics;
let border_color = if is_selected {
theme.accent
} else {
theme.border
};
let block = Block::default()
.title(Line::from(vec![Span::styled(
" Metrics ",
Style::default().fg(theme.fg).bold(),
)]))
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.border_type(if is_selected {
BorderType::Rounded
} else {
BorderType::Plain
});
let inner_area = block.inner(area);
frame.render_widget(block, area);
let metrics = &snap.metrics;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(6), Constraint::Length(3)])
.split(inner_area);
let stats_text = vec![
Line::from(vec![
Span::styled(" Requests ", Style::default().fg(theme.muted)),
Span::styled(
format!("{}", metrics.total_requests),
Style::default().fg(theme.fg).bold(),
),
Span::styled(
format!(
" ({} OK / {} ERR)",
metrics.successful_requests, metrics.failed_requests
),
Style::default().fg(theme.muted),
),
]),
Line::from(vec![
Span::styled(" RPS ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.1}", metrics.requests_per_sec),
Style::default().fg(theme.accent).bold(),
),
]),
Line::from(vec![
Span::styled(" Latency ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.0}ms", metrics.avg_latency_ms),
Style::default().fg(theme.fg),
),
Span::styled(
format!(" (p95: {:.0}ms)", metrics.p95_latency_ms),
Style::default().fg(theme.muted),
),
]),
Line::from(vec![
Span::styled(" Credits ", Style::default().fg(theme.muted)),
Span::styled(
format_credits(metrics.total_credits_spent),
Style::default().fg(if metrics.local_mode {
theme.muted
} else {
theme.success
}),
),
Span::styled(
if metrics.local_mode {
" (free mode)"
} else {
" spent"
},
Style::default().fg(theme.muted),
),
]),
];
let stats_para = Paragraph::new(stats_text);
frame.render_widget(stats_para, chunks[0]);
if !snap.rps_history.is_empty() {
let data_u64: Vec<u64> = snap
.rps_history
.iter()
.map(|v| (*v * 10.0) as u64)
.collect();
let sparkline = Sparkline::default()
.data(&data_u64)
.style(Style::default().fg(theme.accent))
.bar_set(symbols::bar::NINE_LEVELS);
frame.render_widget(sparkline, chunks[1]);
}
}
fn render_footer(frame: &mut Frame, area: Rect, snap: &Snapshot, theme: &Theme) {
let mut keys = vec![("q", "Quit"), ("Tab", "Switch Panel"), ("j/k", "Scroll")];
if snap.mode == SourceMode::Remote {
keys.push(("r", "Refresh"));
}
keys.push(("?", "Help"));
let spans: Vec<Span> = keys
.iter()
.flat_map(|(key, desc)| {
vec![
Span::styled(
format!(" {} ", key),
Style::default().fg(Color::Black).bg(theme.muted),
),
Span::styled(format!(" {} ", desc), Style::default().fg(theme.muted)),
Span::raw(" "),
]
})
.collect();
let footer = Paragraph::new(Line::from(spans));
frame.render_widget(footer, area);
}
fn render_help_overlay(frame: &mut Frame, area: Rect, snap: &Snapshot, theme: &Theme) {
let mut help_text = vec![
Line::from(""),
Line::from(vec![Span::styled(
" Keyboard Shortcuts",
Style::default().fg(theme.fg).bold(),
)]),
Line::from(""),
Line::from(vec![
Span::styled(" q, Esc ", Style::default().fg(theme.accent)),
Span::raw("Quit"),
]),
Line::from(vec![
Span::styled(" Tab ", Style::default().fg(theme.accent)),
Span::raw("Switch between panels"),
]),
Line::from(vec![
Span::styled(" j, ↓ ", Style::default().fg(theme.accent)),
Span::raw("Scroll down"),
]),
Line::from(vec![
Span::styled(" k, ↑ ", Style::default().fg(theme.accent)),
Span::raw("Scroll up"),
]),
];
if snap.mode == SourceMode::Remote {
help_text.push(Line::from(vec![
Span::styled(" r ", Style::default().fg(theme.accent)),
Span::raw("Force refresh"),
]));
} else {
help_text.push(Line::from(vec![
Span::styled(" g ", Style::default().fg(theme.accent)),
Span::raw("Go to top"),
]));
help_text.push(Line::from(vec![
Span::styled(" G ", Style::default().fg(theme.accent)),
Span::raw("Go to bottom"),
]));
}
help_text.push(Line::from(vec![
Span::styled(" ? ", Style::default().fg(theme.accent)),
Span::raw("Toggle help"),
]));
help_text.push(Line::from(""));
let help_width = 40u16;
let help_height = help_text.len() as u16 + 2;
let x = area.width.saturating_sub(help_width) / 2;
let y = area.height.saturating_sub(help_height) / 2;
let help_area = Rect::new(x, y, help_width, help_height);
let help_block = Block::default()
.title(" Help ")
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent))
.style(Style::default().bg(theme.overlay_bg));
let help_para = Paragraph::new(help_text)
.block(help_block)
.wrap(Wrap { trim: false });
frame.render_widget(ratatui::widgets::Clear, help_area);
frame.render_widget(help_para, help_area);
}