pub mod audit;
pub mod blobs;
pub mod daemon_page;
pub mod daemons;
pub mod dataforts;
pub mod failures;
pub mod groups;
pub mod logs;
pub mod migrations;
pub mod net_map;
pub mod node_page;
pub mod nodes;
pub mod nrpc;
pub mod replicas;
pub fn scroll_window(total: usize, body_h: usize, cursor: usize) -> (usize, usize, usize, usize) {
if total == 0 || body_h == 0 {
return (0, 0, 0, 0);
}
if total <= body_h {
return (0, total, 0, 0);
}
let cursor = cursor.min(total - 1);
let mut top_reserve = 0usize;
let mut bot_reserve = 0usize;
for _ in 0..3 {
let viewport = body_h.saturating_sub(top_reserve + bot_reserve);
if viewport == 0 {
return (cursor, cursor, cursor, total - cursor);
}
let half = viewport / 2;
let want_start = cursor.saturating_sub(half);
let want_end = (want_start + viewport).min(total);
let start = want_end.saturating_sub(viewport);
let end = want_end;
let need_top = if start > 0 { 1 } else { 0 };
let need_bot = if end < total { 1 } else { 0 };
if need_top == top_reserve && need_bot == bot_reserve {
return (start, end, start, total - end);
}
top_reserve = need_top;
bot_reserve = need_bot;
}
let viewport = body_h.saturating_sub(top_reserve + bot_reserve).max(1);
let half = viewport / 2;
let want_start = cursor.saturating_sub(half);
let want_end = (want_start + viewport).min(total);
let start = want_end.saturating_sub(viewport);
let end = want_end;
(start, end, start, total - end)
}
pub fn format_age_ms(ms: u64) -> String {
let s = ms / 1_000;
let m = s / 60;
let h = m / 60;
if h > 0 {
format!("{h}h {:02}m", m % 60)
} else if m > 0 {
format!("{m}m {:02}s", s % 60)
} else {
format!("{s}s")
}
}
pub fn short_id(id: u64) -> String {
let s = format!("{id:016x}");
format!("0x{}", &s[..6])
}
pub fn format_bytes(n: u64) -> String {
const KB: u64 = 1_024;
const MB: u64 = 1_024 * KB;
const GB: u64 = 1_024 * MB;
const TB: u64 = 1_024 * GB;
if n < KB {
format!("{n}B")
} else if n < MB {
format!("{:.1}KB", n as f64 / KB as f64)
} else if n < GB {
format!("{:.1}MB", n as f64 / MB as f64)
} else if n < TB {
format!("{:.1}GB", n as f64 / GB as f64)
} else {
format!("{:.1}TB", n as f64 / TB as f64)
}
}
pub fn fmt_ts_hms_ms(ts_ms: u64) -> String {
let total_s = ts_ms / 1000;
let ms = ts_ms % 1000;
let s = total_s % 60;
let m = (total_s / 60) % 60;
let h = (total_s / 3600) % 24;
format!("{h:02}:{m:02}:{s:02}.{ms:03}")
}
pub fn unix_now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub fn event_icon(rec: &net_sdk::deck::LogRecord) -> (char, ratatui::style::Style) {
use net_sdk::deck::LogLevel;
match rec.level {
LogLevel::Error => ('✗', crate::theme::red()),
LogLevel::Warn => ('▲', crate::theme::amber()),
LogLevel::Debug => ('·', crate::theme::dim()),
_ => classify_info(&rec.message),
}
}
fn classify_info(message: &str) -> (char, ratatui::style::Style) {
let lower = message.to_ascii_lowercase();
let contains_any = |needles: &[&str]| needles.iter().any(|n| lower.contains(n));
if contains_any(&[
"announce",
"advertise",
"publish",
"intent",
"commit",
"verified",
"bundle",
]) {
return ('▶', crate::theme::green());
}
if contains_any(&["started", "transfer", "snapshot taken", "register", "store"]) {
return ('↗', crate::theme::green());
}
if contains_any(&[
"drained",
"fetch",
"acked",
"received",
"completed",
"cleared",
"swept",
"pull",
]) {
return ('↘', crate::theme::cyan());
}
if contains_any(&[
"retry",
"restart",
"rotation",
"reflow",
"freeze",
"thaw",
"rebalance",
"cutover",
"drain",
]) {
return ('↻', crate::theme::cyan());
}
('·', crate::theme::dim())
}
pub fn event_source(rec: &net_sdk::deck::LogRecord) -> String {
match (rec.daemon_id, rec.node_id) {
(Some(d), _) => format!("daemon.0x{d:x}"),
(None, Some(n)) => format!("node.0x{n:x}"),
(None, None) => "substrate".to_string(),
}
}
pub fn render_event_line(rec: &net_sdk::deck::LogRecord) -> ratatui::text::Line<'static> {
use ratatui::text::Span;
let (icon, icon_style) = event_icon(rec);
let source = event_source(rec);
const SOURCE_PAD: usize = 19;
ratatui::text::Line::from(vec![
Span::styled(
format!(" {} ", fmt_ts_hms_ms(rec.ts_ms)),
crate::theme::chrome(),
),
Span::styled(format!("{icon} "), icon_style),
Span::styled(
format!("{source:<width$} ", source = source, width = SOURCE_PAD),
icon_style,
),
Span::styled(rec.message.clone(), crate::theme::text()),
])
}