mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
fipstop: ratatui-based TUI for real-time monitoring of a running FIPS daemon. Tabs and navigation: - 8 navigable tabs: Node, Peers, Transports, Sessions, Tree, Filters, Performance, Routing - Tab/BackTab navigation with group separators in tab bar - Table views with selectable rows, detail drill-down panels, and scrollbars Node tab: - Runtime info: pid, exe path, uptime, control socket path, TUN adapter name - Identity: npub, node_addr, ipv6 address - State summary with peer/session/link/transport/connection counts - TUN IPv6 traffic and forwarded transit traffic counters Peers tab: - Table with Name, Address, Conn, Depth, SRTT, Loss, LQI, Pkts Tx/Rx - Detail panel: identity, connection info, transport cross-reference, tree/bloom state, link stats, MMP metrics with LQI Sessions tab: - Table with Name, Remote Addr, State, Role, SRTT, Loss, SQI, Path MTU, Last Activity - Detail panel: identity, session info, traffic stats, MMP metrics with SQI Transports tab: - Hierarchical tree view: expandable transport parents with nested links (▼/▶ indicators, ├─/└─ tree chars, Space/Arrow to expand/collapse) - Transport detail: type-specific stats (UDP/TCP/Ethernet) - Link detail: peer cross-reference with MMP metrics and LQI Performance tab: - Link-layer MMP: SRTT, loss, ETX, LQI, goodput per peer - Session-layer MMP: SRTT, loss, ETX, SQI, path MTU per session - Trend indicators (rising/falling/stable) with context-aware coloring Routing tab: - Routing state: cache sizes, pending lookups, recent requests - Coordinate cache: entries, fill ratio, TTL, expiry, avg age - Statistics: forwarding, discovery request/response, error signal counters Tree tab: - Spanning tree position with 16 announce stats (inbound/outbound/cumulative) Filters tab: - Bloom filter announce stats, per-peer fill ratio and estimated node count MMP metrics enhancements: - Add etx_trend DualEwma for smoothed ETX tracking - Add smoothed_loss() and smoothed_etx() accessors (long-term EWMA) - LQI (Link Quality Index) = smoothed_etx * (1 + srtt_ms / 100) - SQI (Session Quality Index) = same formula for session layer - All loss/ETX displays prefer smoothed values with raw fallback Control socket: - Add smoothed_loss, smoothed_etx, lqi/sqi to show_peers, show_sessions, and show_mmp JSON responses - Rename fips_address to ipv6_addr in show_status and show_peers - Add tun_name and control_socket to show_status - FHS-compliant 3-tier default path: $XDG_RUNTIME_DIR, /run/fips, /tmp Node extensions: - Add started_at/uptime() to Node - Add tun_name() accessor Docker sidecar updates: - TCP transport support via FIPS_PEER_TRANSPORT env var - Build scripts include fipstop binary
172 lines
5.3 KiB
Rust
172 lines
5.3 KiB
Rust
use ratatui::style::{Color, Modifier, Style};
|
|
use ratatui::text::{Line, Span};
|
|
use serde_json::Value;
|
|
|
|
/// Extract a string field from JSON, returning "-" if missing.
|
|
pub fn str_field<'a>(data: &'a Value, key: &str) -> &'a str {
|
|
data.get(key).and_then(|v| v.as_str()).unwrap_or("-")
|
|
}
|
|
|
|
/// Extract a u64 field from JSON, returning "-" if missing.
|
|
pub fn u64_field(data: &Value, key: &str) -> String {
|
|
data.get(key)
|
|
.and_then(|v| v.as_u64())
|
|
.map(|n| n.to_string())
|
|
.unwrap_or_else(|| "-".into())
|
|
}
|
|
|
|
/// Truncate a hex string to the given length, adding "..." if truncated.
|
|
pub fn truncate_hex(s: &str, max_len: usize) -> String {
|
|
if s.len() <= max_len {
|
|
s.to_string()
|
|
} else {
|
|
format!("{}...", &s[..max_len.saturating_sub(3)])
|
|
}
|
|
}
|
|
|
|
/// Format bytes-per-second with engineering units (B/s, KB/s, MB/s, GB/s) and 3 significant digits.
|
|
pub fn format_throughput(bytes_per_sec: f64) -> String {
|
|
if bytes_per_sec < 0.0 {
|
|
return "0 B/s".into();
|
|
}
|
|
let (scaled, unit) = if bytes_per_sec < 1_000.0 {
|
|
(bytes_per_sec, "B/s")
|
|
} else if bytes_per_sec < 1_000_000.0 {
|
|
(bytes_per_sec / 1_000.0, "KB/s")
|
|
} else if bytes_per_sec < 1_000_000_000.0 {
|
|
(bytes_per_sec / 1_000_000.0, "MB/s")
|
|
} else {
|
|
(bytes_per_sec / 1_000_000_000.0, "GB/s")
|
|
};
|
|
let decimals = if scaled >= 100.0 {
|
|
0
|
|
} else if scaled >= 10.0 {
|
|
1
|
|
} else {
|
|
2
|
|
};
|
|
format!("{:.prec$} {unit}", scaled, prec = decimals)
|
|
}
|
|
|
|
/// Extract a nested f64 field and format as engineering-unit throughput.
|
|
pub fn nested_throughput(data: &Value, outer: &str, inner: &str) -> String {
|
|
data.get(outer)
|
|
.and_then(|o| o.get(inner))
|
|
.and_then(|v| v.as_f64())
|
|
.map(format_throughput)
|
|
.unwrap_or_else(|| "-".into())
|
|
}
|
|
|
|
/// Format a byte count as human-readable (B, KB, MB, GB).
|
|
pub fn format_bytes(bytes: u64) -> String {
|
|
if bytes < 1024 {
|
|
format!("{bytes}B")
|
|
} else if bytes < 1024 * 1024 {
|
|
format!("{:.1}KB", bytes as f64 / 1024.0)
|
|
} else if bytes < 1024 * 1024 * 1024 {
|
|
format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
|
|
} else {
|
|
format!("{:.2}GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
|
|
}
|
|
}
|
|
|
|
/// Format millisecond timestamp as relative duration from now (e.g., "3.2s ago").
|
|
pub fn format_elapsed_ms(ms: u64) -> String {
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis() as u64;
|
|
if ms == 0 || ms > now_ms {
|
|
return "-".into();
|
|
}
|
|
let elapsed = now_ms - ms;
|
|
if elapsed < 1000 {
|
|
format!("{elapsed}ms")
|
|
} else if elapsed < 60_000 {
|
|
format!("{:.1}s", elapsed as f64 / 1000.0)
|
|
} else if elapsed < 3_600_000 {
|
|
format!("{:.1}m", elapsed as f64 / 60_000.0)
|
|
} else {
|
|
format!("{:.1}h", elapsed as f64 / 3_600_000.0)
|
|
}
|
|
}
|
|
|
|
/// Get a nested string field.
|
|
pub fn nested_str(data: &Value, outer: &str, inner: &str) -> String {
|
|
data.get(outer)
|
|
.and_then(|o| o.get(inner))
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("-")
|
|
.to_string()
|
|
}
|
|
|
|
/// Get a nested field value (e.g., "stats.packets_sent").
|
|
pub fn nested_u64(data: &Value, outer: &str, inner: &str) -> String {
|
|
data.get(outer)
|
|
.and_then(|o| o.get(inner))
|
|
.and_then(|v| v.as_u64())
|
|
.map(|n| n.to_string())
|
|
.unwrap_or_else(|| "-".into())
|
|
}
|
|
|
|
/// Get a nested f64 field formatted to given decimal places.
|
|
pub fn nested_f64(data: &Value, outer: &str, inner: &str, decimals: usize) -> String {
|
|
data.get(outer)
|
|
.and_then(|o| o.get(inner))
|
|
.and_then(|v| v.as_f64())
|
|
.map(|n| format!("{:.prec$}", n, prec = decimals))
|
|
.unwrap_or_else(|| "-".into())
|
|
}
|
|
|
|
/// Get a nested f64 field, preferring `preferred` key with fallback to `fallback` key.
|
|
pub fn nested_f64_prefer(data: &Value, outer: &str, preferred: &str, fallback: &str, decimals: usize) -> String {
|
|
data.get(outer)
|
|
.and_then(|o| o.get(preferred).or_else(|| o.get(fallback)))
|
|
.and_then(|v| v.as_f64())
|
|
.map(|n| format!("{:.prec$}", n, prec = decimals))
|
|
.unwrap_or_else(|| "-".into())
|
|
}
|
|
|
|
/// Extract a bool field from JSON, returning "yes"/"no" or "-" if missing.
|
|
pub fn bool_field(data: &Value, key: &str) -> &'static str {
|
|
data.get(key)
|
|
.and_then(|v| v.as_bool())
|
|
.map(|b| if b { "yes" } else { "no" })
|
|
.unwrap_or("-")
|
|
}
|
|
|
|
/// Format a duration in milliseconds as compact string (e.g., "42ms", "3.2s", "5.0m").
|
|
pub fn format_duration_ms(ms: u64) -> String {
|
|
if ms < 1000 {
|
|
format!("{ms}ms")
|
|
} else if ms < 60_000 {
|
|
format!("{:.1}s", ms as f64 / 1000.0)
|
|
} else if ms < 3_600_000 {
|
|
format!("{:.1}m", ms as f64 / 60_000.0)
|
|
} else {
|
|
format!("{:.1}h", ms as f64 / 3_600_000.0)
|
|
}
|
|
}
|
|
|
|
/// Section header line for detail views.
|
|
pub fn section_header(title: &str) -> Line<'static> {
|
|
Line::from(Span::styled(
|
|
format!(" {title}"),
|
|
Style::default()
|
|
.fg(Color::Cyan)
|
|
.add_modifier(Modifier::BOLD),
|
|
))
|
|
}
|
|
|
|
/// Key-value line for detail views.
|
|
pub fn kv_line(key: &str, value: &str) -> Line<'static> {
|
|
Line::from(vec![
|
|
Span::styled(
|
|
format!(" {key}: "),
|
|
Style::default().fg(Color::DarkGray),
|
|
),
|
|
Span::raw(value.to_string()),
|
|
])
|
|
}
|
|
|