//! The Settings tab: federation settings, visualization scripts and status. use ratatui::Frame; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph}; use super::theme; use crate::app::state::{AppState, DevicePresenceSection, FedRow, SimilarityRow, settings_rows}; pub fn draw(frame: &mut Frame, area: Rect, state: &AppState) { let block = Block::bordered() .title(" Settings ") .title_style(theme::header_for(state)) .border_style(theme::border_for(state)); let inner = block.inner(area); frame.render_widget(block, area); if inner.width >= 132 { let desired_settings_width = ((inner.width as usize * 44) / 100).clamp(68, 92) as u16; let settings_width = desired_settings_width.min(inner.width.saturating_sub(56)); let [rows_area, _, status_area] = Layout::horizontal([ Constraint::Length(settings_width), Constraint::Length(2), Constraint::Min(0), ]) .areas(inner); draw_settings_rows(frame, rows_area, state); draw_status_column(frame, status_area, state); return; } let rows_height = (settings_rows(state).len() + 10 + device_presence_sections(state).len()) as u16; let [rows_area, _, status_area] = Layout::vertical([ Constraint::Length(rows_height.min(inner.height)), Constraint::Length(1), Constraint::Min(0), ]) .areas(inner); draw_settings_rows(frame, rows_area, state); draw_status_column(frame, status_area, state); } fn device_presence_sections(state: &AppState) -> Vec { let Some(status) = &state.federation.devices else { return Vec::new(); }; let now = crate::app::state::unix_time_ms(); let mut sections = Vec::new(); for index in crate::app::state::device_status_order(state) { let Some(device) = status.devices.get(index) else { continue; }; let section = crate::app::state::device_presence_section(state, device, now); if sections.last().copied() != Some(section) { sections.push(section); } } sections } fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) { let settings = &state.federation.settings; let on_off = |on: bool| if on { "on" } else { "off" }; let mut y = area.y; let mut cursor = 0usize; draw_section(frame, area, state, &mut y, "Library"); draw_row( frame, area, state, &mut y, cursor, state.settings_cursor, "Music save directory", if state.music_dir_changing { format!("{} checking/changing…", state.spinner()) } else { state.music_dir.to_string_lossy().into_owned() }, ); cursor += 1; y = y.saturating_add(1); draw_section(frame, area, state, &mut y, "Similarity Search"); let similarity = &state.similarity.settings; for row in SimilarityRow::ALL { let (label, value) = match row { SimilarityRow::Toggle => ("Similarity search", on_off(similarity.enabled).to_string()), SimilarityRow::Model => ( "Embedding model", crate::similarity::model_by_id(&similarity.model) .map(|model| format!("{} · {}", model.id, model.license)) .unwrap_or_else(|| similarity.model.clone()), ), SimilarityRow::Profile => ( "Preprocessing profile", format!("{} (enter for details)", similarity.profile), ), SimilarityRow::Workers => ("Background workers", similarity.workers.to_string()), SimilarityRow::Clear => ("Clear all stored embeddings", "↵".to_string()), }; draw_row( frame, area, state, &mut y, cursor, state.settings_cursor, label, value, ); cursor += 1; } y = y.saturating_add(1); draw_section(frame, area, state, &mut y, "Federation"); for row in FedRow::ALL { let (label, value) = match row { FedRow::Toggle => ("Federation", on_off(settings.enabled).to_string()), FedRow::NetworkId => ( "Network ID", if settings.network_id.is_empty() { "(not set — press enter)".to_string() } else { settings.network_id.clone() }, ), FedRow::SaveOnListen => ( "Save federated tracks to the library on listen", on_off(settings.save_on_listen).to_string(), ), FedRow::SyncNow => ( "Publish the library now", if state.federation.publishing { format!("{} publishing", state.spinner()) } else { "↵".to_string() }, ), FedRow::ShowTicket => ("Show my connection ticket", "↵".to_string()), FedRow::Connect => ("Connect to a peer by ticket…", "↵".to_string()), }; draw_row( frame, area, state, &mut y, cursor, state.settings_cursor, label, value, ); cursor += 1; } y = y.saturating_add(1); let connected_devices_enabled = state.connected_devices_enabled(); draw_section(frame, area, state, &mut y, "Connected Devices"); let disabled_value = "enable federation first".to_string(); let devices = state.federation.devices.as_ref(); draw_row_enabled( frame, area, state, &mut y, cursor, state.settings_cursor, "This device name", if connected_devices_enabled { devices .map(|status| status.this_device_name.clone()) .unwrap_or_else(|| "loading…".to_string()) } else { disabled_value.clone() }, connected_devices_enabled, ); cursor += 1; draw_row_enabled( frame, area, state, &mut y, cursor, state.settings_cursor, "Generate device invite", if connected_devices_enabled { "↵".to_string() } else { disabled_value.clone() }, connected_devices_enabled, ); cursor += 1; draw_row_enabled( frame, area, state, &mut y, cursor, state.settings_cursor, "Connect device by invite…", if connected_devices_enabled { "↵".to_string() } else { disabled_value.clone() }, connected_devices_enabled, ); cursor += 1; draw_row_enabled( frame, area, state, &mut y, cursor, state.settings_cursor, "Sync devices now", if connected_devices_enabled { if state.federation.device_syncing { format!("{} syncing", state.spinner()) } else { "↵".to_string() } } else { disabled_value.clone() }, connected_devices_enabled, ); cursor += 1; draw_row_enabled( frame, area, state, &mut y, cursor, state.settings_cursor, "Leave device group", if connected_devices_enabled { "↵".to_string() } else { disabled_value.clone() }, connected_devices_enabled, ); cursor += 1; if let Some(status) = devices { let now = crate::app::state::unix_time_ms(); let mut current_section = None; for index in crate::app::state::device_status_order(state) { let Some(device) = status.devices.get(index) else { continue; }; let section = crate::app::state::device_presence_section(state, device, now); if current_section != Some(section) { draw_subsection(frame, area, &mut y, section.title()); current_section = Some(section); } let name = crate::app::state::device_display_name(device); let label = if device.is_self { format!("* {name}") } else if device.revoked { format!(" {name} (revoked)") } else { format!(" {name}") }; let version = if device.client_version.is_empty() { "unknown".to_string() } else { format!("v{}", device.client_version) }; let can_revoke = connected_devices_enabled && !device.is_self && !device.revoked; let presence = match section { DevicePresenceSection::Online => "online", DevicePresenceSection::Offline => "offline", DevicePresenceSection::Revoked => "revoked", }; let value = if can_revoke { format!("{version} · {presence} · revoke ↵") } else if connected_devices_enabled { format!("{version} · {presence}") } else { disabled_value.clone() }; draw_row_enabled( frame, area, state, &mut y, cursor, state.settings_cursor, &label, value, connected_devices_enabled, ); cursor += 1; } } y = y.saturating_add(1); draw_section(frame, area, state, &mut y, "Visualizations"); draw_row( frame, area, state, &mut y, cursor, state.settings_cursor, "Show clock", if state.visualizer.config.show_clock { "[x]".to_string() } else { "[ ]".to_string() }, ); cursor += 1; for (index, script) in state.visualizer.scripts.iter().enumerate() { let selected_script = state .visualizer .selected_script_index() .is_some_and(|selected| selected == index); let label = if selected_script { format!("* {}", script.name) } else { format!(" {}", script.name) }; let value = script .path .file_name() .and_then(|name| name.to_str()) .unwrap_or("") .to_string(); draw_row( frame, area, state, &mut y, cursor, state.settings_cursor, &label, value, ); cursor += 1; } draw_row( frame, area, state, &mut y, cursor, state.settings_cursor, "+ New visualization script", "↵".to_string(), ); cursor += 1; if state.visualizer.selected_script().is_some() { draw_row( frame, area, state, &mut y, cursor, state.settings_cursor, "Edit selected visualization", "↵".to_string(), ); cursor += 1; } y = y.saturating_add(1); draw_row( frame, area, state, &mut y, cursor, state.settings_cursor, "Full status details", "enter".to_string(), ); } fn protocol_label(id: &str) -> &str { match id { "federation_net" => "Federation transport", "ticket" => "Peer ticket", "rendezvous" => "Rendezvous", "music_dht" => "Music DHT", "catalog" => "Catalog", "audio" => "Audio transfer", "similarity" => "Similarity search", "device_sync" => "Device sync", "jam" => "Jam", other => other, } } fn draw_section(frame: &mut Frame, area: Rect, state: &AppState, y: &mut u16, title: &'static str) { if *y >= area.y + area.height { return; } let rect = Rect { x: area.x, y: *y, width: area.width, height: 1, }; frame.render_widget( Paragraph::new(Line::styled(title, theme::header_for(state))), rect, ); *y = (*y).saturating_add(1); } fn draw_subsection(frame: &mut Frame, area: Rect, y: &mut u16, title: &'static str) { if *y >= area.y + area.height { return; } let rect = Rect { x: area.x, y: *y, width: area.width, height: 1, }; frame.render_widget( Paragraph::new(Line::from(vec![ Span::raw(" "), Span::styled(title, theme::dim()), ])), rect, ); *y = (*y).saturating_add(1); } fn draw_row( frame: &mut Frame, area: Rect, state: &AppState, y: &mut u16, row_index: usize, cursor: usize, label: &str, value: String, ) { draw_row_enabled(frame, area, state, y, row_index, cursor, label, value, true); } fn draw_row_enabled( frame: &mut Frame, area: Rect, state: &AppState, y: &mut u16, row_index: usize, cursor: usize, label: &str, value: String, enabled: bool, ) { if *y >= area.y + area.height { return; } let selected = row_index == cursor; let rect = Rect { x: area.x, y: *y, width: area.width, height: 1, }; let marker = if selected { "▶ " } else { " " }; let label_width = settings_label_width(area.width); let line = Line::from(vec![ Span::styled( marker, if enabled { theme::accent_for(state) } else { theme::dim() }, ), Span::styled( format!("{label: usize { let width = width as usize; if width >= 88 { 48 } else { width.saturating_sub(28).clamp(24, 48) } } fn status_line(label: &str, value: String) -> Line<'static> { Line::from(vec![ Span::styled(format!("{label:<18}"), theme::dim()), Span::raw(value), ]) } fn short_bytes_label(bytes: u64) -> String { if bytes >= 1024 * 1024 { format!("{:.1} MiB", bytes as f64 / 1024.0 / 1024.0) } else if bytes >= 1024 { format!("{:.1} KiB", bytes as f64 / 1024.0) } else { format!("{bytes} B") } } fn rtt_label(ms: Option) -> String { ms.map(|ms| format!("{ms} ms")) .unwrap_or_else(|| "rtt n/a".to_string()) } fn short_id(id: &str) -> String { id.chars().take(12).collect::() + "…" } fn draw_status_column(frame: &mut Frame, area: Rect, state: &AppState) { if area.height < 12 { draw_status(frame, area, state); return; } let [similarity_area, _, federation_area] = Layout::vertical([ Constraint::Length(8), Constraint::Length(1), Constraint::Min(0), ]) .areas(area); draw_similarity_status(frame, similarity_area, state); draw_status(frame, federation_area, state); } fn draw_similarity_status(frame: &mut Frame, area: Rect, state: &AppState) { let status = &state.similarity.status; let progress = if status.total_tracks == 0 { "0 / 0".to_string() } else { format!("{} / {}", status.completed_tracks, status.total_tracks) }; let active = status .active_profile .as_deref() .map(short_id) .unwrap_or_else(|| "not ready".to_string()); let target = status .target_profile .as_deref() .map(short_id) .unwrap_or_else(|| "—".to_string()); draw_summary_card( frame, area, state, " Similarity Processing ", vec![ status_line("State", status.phase.label().to_string()), status_line("Progress", progress), status_line("Active", active), status_line("Processing", target), status_line( "Stored", format!( "{} vectors / {}", status.stored_vectors, short_bytes_label(status.stored_bytes) ), ), status_line( "Current / errors", status .current_track .clone() .or_else(|| status.last_error.clone()) .unwrap_or_else(|| format!("{} errors", status.failed_tracks)), ), ], ); } fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) { if area.width == 0 || area.height == 0 { return; } if area.height < 15 || area.width < 36 { frame.render_widget( Paragraph::new(compact_status_lines(state)) .wrap(ratatui::widgets::Wrap { trim: false }), area, ); return; } if area.width >= 60 && area.height >= 20 { let protocols_height = protocol_card_height(state, area.width.saturating_sub(2), area.height); let [top_area, _, bottom_area, _, protocols_area, _] = Layout::vertical([ Constraint::Length(7), Constraint::Length(1), Constraint::Length(7), Constraint::Length(1), Constraint::Length(protocols_height), Constraint::Min(0), ]) .areas(area); let [status_area, _, local_area] = Layout::horizontal([ Constraint::Percentage(50), Constraint::Length(1), Constraint::Percentage(50), ]) .areas(top_area); let [transport_area, _, devices_area] = Layout::horizontal([ Constraint::Percentage(50), Constraint::Length(1), Constraint::Percentage(50), ]) .areas(bottom_area); draw_summary_card( frame, status_area, state, " Status ", node_summary_lines(state), ); draw_summary_card( frame, local_area, state, " Local Data ", local_data_summary_lines(state), ); draw_summary_card( frame, transport_area, state, " Iroh Transport ", transport_summary_lines(state), ); draw_summary_card( frame, devices_area, state, " Connected Devices ", device_summary_lines(state), ); draw_summary_card( frame, protocols_area, state, " Protocol Versions ", protocol_summary_lines(state, protocols_area.width.saturating_sub(2)), ); return; } if area.height < 39 { frame.render_widget( Paragraph::new(compact_status_lines(state)) .wrap(ratatui::widgets::Wrap { trim: false }), area, ); return; } let [ node_area, _, transport_area, _, devices_area, _, local_area, _, protocols_area, _, ] = Layout::vertical([ Constraint::Length(7), Constraint::Length(1), Constraint::Length(7), Constraint::Length(1), Constraint::Length(7), Constraint::Length(1), Constraint::Length(7), Constraint::Length(1), Constraint::Length(protocol_card_height( state, area.width.saturating_sub(2), area.height, )), Constraint::Min(0), ]) .areas(area); draw_summary_card( frame, node_area, state, " Status ", node_summary_lines(state), ); draw_summary_card( frame, transport_area, state, " Iroh Transport ", transport_summary_lines(state), ); draw_summary_card( frame, devices_area, state, " Connected Devices ", device_summary_lines(state), ); draw_summary_card( frame, local_area, state, " Local Data ", local_data_summary_lines(state), ); draw_summary_card( frame, protocols_area, state, " Protocol Versions ", protocol_summary_lines(state, protocols_area.width.saturating_sub(2)), ); } fn compact_status_lines(state: &AppState) -> Vec> { let mut lines = Vec::new(); lines.push(Line::styled("Status", theme::header_for(state))); lines.extend(node_summary_lines(state).into_iter().take(2)); lines.push(Line::default()); lines.push(Line::styled("Local Data", theme::header_for(state))); lines.extend(local_data_summary_lines(state).into_iter().take(3)); lines.push(Line::default()); lines.push(Line::styled("Iroh Transport", theme::header_for(state))); lines.extend(transport_summary_lines(state).into_iter().take(2)); lines.push(Line::default()); lines.push(Line::styled("Connected Devices", theme::header_for(state))); lines.extend(device_summary_lines(state).into_iter().take(2)); lines.push(Line::default()); lines.push(Line::styled("Protocol Versions", theme::header_for(state))); lines.extend(protocol_summary_lines(state, 0).into_iter().take(3)); lines } fn draw_summary_card( frame: &mut Frame, area: Rect, state: &AppState, title: &'static str, lines: Vec>, ) { if area.width == 0 || area.height == 0 { return; } let block = Block::bordered() .title(title) .title_style(theme::header_for(state)) .border_style(theme::border_for(state)); let inner = block.inner(area); frame.render_widget(block, area); frame.render_widget(Paragraph::new(lines), inner); } fn summary_line(label: &'static str, value: String) -> Line<'static> { Line::from(vec![ Span::styled(format!("{label:<10}"), theme::dim()), Span::raw(value), ]) } fn protocol_summary_line( label: &str, value: String, style: Style, label_width: usize, ) -> Line<'static> { Line::from(vec![ Span::styled( format!("{: Vec> { let Some(status) = state.federation.status.as_ref() else { return vec![protocol_summary_line( "status", "[UNKNOWN]".to_string(), theme::dim(), 22, )]; }; let protocols = &status.protocols; let newer = !protocols.newer.is_empty(); let badge = if newer { "[NEWER VERSION SEEN]" } else if status.running && protocols.observed_peers == 0 { "[CURRENT · waiting for peers]" } else { "[CURRENT]" }; let badge_style = Style::new() .fg(if newer { Color::LightRed } else { Color::Green }) .add_modifier(Modifier::BOLD); let mut lines = vec![protocol_summary_line( "status", badge.to_string(), badge_style, 22, )]; let mut entries = Vec::new(); for (id, local) in &protocols.local { let observed = protocols.observed.get(id).copied(); let value = match observed { Some(remote) if remote > *local => format!("local {local} · network {remote}"), Some(remote) => format!("{local} · seen {remote}"), None => local.to_string(), }; let style = if observed.is_some_and(|remote| remote > *local) { Style::new() .fg(Color::LightRed) .add_modifier(Modifier::BOLD) } else { Style::default() }; entries.push((id.as_str(), value, style)); } if width >= 58 { let cell_width = width as usize / 2; let label_width = 22.min(cell_width.saturating_sub(3)); let value_width = cell_width.saturating_sub(label_width + 2); for pair in entries.chunks(2) { let mut spans = protocol_cell_spans(pair[0].0, &pair[0].1, pair[0].2, label_width, value_width); if let Some(second) = pair.get(1) { spans.push(Span::styled(" ", theme::dim())); spans.extend(protocol_cell_spans( second.0, &second.1, second.2, label_width, value_width, )); } lines.push(Line::from(spans)); } } else { lines.extend( entries .into_iter() .map(|(id, value, style)| protocol_summary_line(id, value, style, 22)), ); } if newer { lines.push(Line::styled( "A newer protocol was observed; update Furumi for compatibility.", Style::new().fg(Color::LightRed), )); } lines } fn protocol_card_height(state: &AppState, width: u16, available: u16) -> u16 { let content = protocol_summary_lines(state, width).len() as u16; content.saturating_add(2).min(available) } fn protocol_cell_spans( label: &str, value: &str, style: Style, label_width: usize, value_width: usize, ) -> Vec> { let value = value.chars().take(value_width).collect::(); vec![ Span::styled( format!("{: Vec> { match &state.federation.status { None => vec![ summary_line("Node", "loading".to_string()), summary_line("Network", "unknown".to_string()), summary_line("Peers", "waiting for status".to_string()), ], Some(status) if !status.running => { let network = if state.federation.settings.network_id.trim().is_empty() { "network id not set".to_string() } else { state.federation.settings.network_id.clone() }; vec![ summary_line("Node", "stopped".to_string()), summary_line("Network", network), summary_line( "Problem", status .last_error .as_deref() .map(first_line) .unwrap_or_else(|| "disabled".to_string()), ), ] } Some(status) => vec![ summary_line("Node", format!("running on {}", status.network)), summary_line( "Peers", format!( "{} connected / {} contacts", status.connected_peers.len(), status.known_contacts ), ), summary_line( "Library", format!( "{} published / {} DHT records", status.published_items, status .stored_dht_records .map(|count| count.to_string()) .unwrap_or_else(|| "n/a".to_string()) ), ), ], } } fn transport_summary_lines(state: &AppState) -> Vec> { let Some(status) = &state.federation.status else { return vec![ summary_line("Traffic", "loading".to_string()), summary_line("Streams", "loading".to_string()), summary_line("Path", "loading".to_string()), ]; }; let transport = &status.transport; if transport.total_samples == 0 { return vec![ summary_line("Traffic", "no samples yet".to_string()), summary_line("Streams", format!("{} active", transport.active_streams)), summary_line("Path", "waiting for a stream".to_string()), ]; } let runtime_total = transport .runtime_tx_bytes .saturating_add(transport.runtime_rx_bytes); let last_path = transport .last .first() .map(|sample| { format!( "{} / {} / {}", sample.protocol, sample.selected_path, rtt_label(sample.selected_rtt_ms) ) }) .unwrap_or_else(|| "no recent stream".to_string()); vec![ summary_line( "Traffic", format!( "{} (tx {} / rx {})", short_bytes_label(runtime_total), short_bytes_label(transport.runtime_tx_bytes), short_bytes_label(transport.runtime_rx_bytes) ), ), summary_line( "Streams", format!( "{} active / {} samples", transport.active_streams, transport.total_samples ), ), summary_line( "Paths", format!( "direct {} / relay {} / custom {}", transport.direct_samples, transport.relay_samples, transport.custom_samples ), ), summary_line("Last", last_path), ] } fn local_data_summary_lines(state: &AppState) -> Vec> { match &state.local_library_stats { None | Some(crate::app::state::Loadable::Loading) => vec![ summary_line("Counts", "loading".to_string()), summary_line("Media", "loading".to_string()), summary_line("SQLite", "loading".to_string()), summary_line("Total", "loading".to_string()), ], Some(crate::app::state::Loadable::Failed(message)) => vec![ summary_line("Counts", "unavailable".to_string()), summary_line("Problem", first_line(message)), ], Some(crate::app::state::Loadable::Ready(stats)) => { let media = if stats.tracks_without_size > 0 { format!( "{} · {} unknown", short_bytes_label(stats.audio_bytes), stats.tracks_without_size ) } else { short_bytes_label(stats.audio_bytes) }; vec![ summary_line( "Counts", format!( "{} artists / {} releases / {} tracks", stats.artist_count, stats.release_count, stats.track_count ), ), summary_line("Media", media), summary_line("SQLite", short_bytes_label(stats.database_bytes)), summary_line( "Total", format!( "{} (covers {})", short_bytes_label(stats.total_bytes()), short_bytes_label(stats.cover_bytes) ), ), ] } } } fn device_summary_lines(state: &AppState) -> Vec> { if !state.connected_devices_enabled() { return vec![ summary_line("Sync", "disabled".to_string()), summary_line("This", state.device_playback.self_device_name.clone()), summary_line("Devices", "enable federation first".to_string()), ]; } let Some(status) = &state.federation.devices else { return vec![ summary_line("Sync", "loading".to_string()), summary_line("This", state.device_playback.self_device_name.clone()), summary_line("Devices", "waiting for device status".to_string()), ]; }; let (online, offline, revoked) = device_presence_counts(state); let this_name = if status.this_device_name.trim().is_empty() { short_id(&status.this_device_id) } else { format!( "{} / {}", status.this_device_name, short_id(&status.this_device_id) ) }; vec![ summary_line("This", this_name), summary_line( "Devices", format!( "{} active / {} online / {} pending", status.active_devices, online, status.pending_requests ), ), summary_line( "Sync", format!( "{} outbox / last {}", status.outbox_ops, status .last_sync .clone() .unwrap_or_else(|| "not yet".to_string()) ), ), summary_line( "Other", format!("{} offline / {} revoked", offline, revoked), ), ] } fn device_presence_counts(state: &AppState) -> (usize, usize, usize) { let Some(status) = &state.federation.devices else { return (0, 0, 0); }; let now = crate::app::state::unix_time_ms(); let mut online = 0usize; let mut offline = 0usize; let mut revoked = 0usize; for device in &status.devices { match crate::app::state::device_presence_section(state, device, now) { DevicePresenceSection::Online => online += 1, DevicePresenceSection::Offline => offline += 1, DevicePresenceSection::Revoked => revoked += 1, } } (online, offline, revoked) } fn first_line(value: &str) -> String { value.lines().next().unwrap_or(value).to_string() } pub(super) struct StatusDetailSections { pub status: Vec>, pub devices: Vec>, pub logs: Vec>, } pub(super) fn status_detail_sections( state: &AppState, status_cursor: usize, ) -> StatusDetailSections { StatusDetailSections { status: status_detail_status_lines(state, status_cursor), devices: status_detail_device_lines(state), logs: status_detail_transport_logs(state), } } fn status_detail_status_lines(state: &AppState, status_cursor: usize) -> Vec> { let mut lines: Vec = vec![Line::styled("Status", theme::header_for(state))]; match &state.federation.status { None => lines.push(Line::styled("loading…", theme::dim())), Some(status) if !status.running => { lines.push(status_line("Node", "stopped".to_string())); if let Some(error) = &status.last_error { lines.push(status_line("Error", first_line(error))); } lines.push(Line::default()); lines.push(Line::styled( "Set the same Network ID on each client.", theme::dim(), )); } Some(status) => { lines.push(status_line("Node", format!("running · {}", status.network))); lines.push(status_action_line( "Endpoint", format!( "{} · dht {}", short_id(&status.endpoint_id), short_id(&status.dht_node_id) ), state, status_cursor == 0, )); lines.push(status_line( "Peers", format!( "{} connected · {} contacts", status.connected_peers.len(), status.known_contacts ), )); if !status.connected_peers.is_empty() { let mut peers: Vec = status .connected_peers .iter() .take(4) .map(|peer| short_id(peer)) .collect(); if status.connected_peers.len() > peers.len() { peers.push(format!("+{}", status.connected_peers.len() - peers.len())); } lines.push(status_action_line( "Peer IDs", peers.join(", "), state, status_cursor == 1, )); } lines.push(status_line( "DHT", format!( "{} records · {}", status .stored_dht_records .map(|count| count.to_string()) .unwrap_or_else(|| "n/a".to_string()), status .stored_dht_bytes .map(short_bytes_label) .unwrap_or_else(|| "n/a".to_string()) ), )); lines.push(status_line( "Published", format!("{} items", status.published_items), )); lines.push(status_line( "Last sync", status .last_sync .clone() .unwrap_or_else(|| "not yet".to_string()), )); if let Some(error) = &status.last_error { lines.push(status_line("Error", first_line(error))); } push_transport_summary_status(&mut lines, state, status); } } lines } fn status_action_line( label: &str, value: String, state: &AppState, selected: bool, ) -> Line<'static> { let marker = if selected { "▶" } else { " " }; Line::from(vec![ Span::styled( format!("{marker} {label:<16}"), if selected { theme::accent_for(state) } else { theme::dim() }, ), Span::raw(value), Span::styled(" ↵", theme::dim()), ]) } fn push_transport_summary_status( lines: &mut Vec>, state: &AppState, status: &crate::federation::FedStatus, ) { lines.push(Line::default()); lines.push(Line::styled("Iroh Transport", theme::header_for(state))); let transport = &status.transport; let runtime_total = transport .runtime_tx_bytes .saturating_add(transport.runtime_rx_bytes); lines.push(status_line( "Traffic", format!( "{} · tx {} · rx {}", short_bytes_label(runtime_total), short_bytes_label(transport.runtime_tx_bytes), short_bytes_label(transport.runtime_rx_bytes) ), )); lines.push(status_line( "Active", format!("{} streams", transport.active_streams), )); if transport.runtime_lost_packets > 0 || transport.runtime_lost_bytes > 0 { lines.push(status_line( "Loss", format!( "{} pkts · {}", transport.runtime_lost_packets, short_bytes_label(transport.runtime_lost_bytes) ), )); } lines.push(status_line( "Samples", format!( "{} total · {} direct · {} relay · {} unknown", transport.total_samples, transport.direct_samples, transport.relay_samples, transport.unknown_samples ), )); lines.push(status_line( "Protocols", format!( "audio {} · catalog {} · sync {}", transport.audio_samples, transport.catalog_samples, transport.sync_samples ), )); if let Some(sample) = transport.last.first() { lines.push(status_line( "Last stream", format!( "{} {} {} · {} · {}", sample.protocol, sample.direction, sample.phase, sample.selected_path, rtt_label(sample.selected_rtt_ms) ), )); lines.push(status_line( "Last peer", format!( "{} · paths {}/{}/{}/{}", short_id(&sample.peer_id), sample.direct_paths, sample.relay_paths, sample.custom_paths, sample.open_paths ), )); lines.push(status_line( "Last bytes", format!( "sel {}/{} · total {}/{} · lost {}", short_bytes_label(sample.selected_tx_bytes), short_bytes_label(sample.selected_rx_bytes), short_bytes_label(sample.total_tx_bytes), short_bytes_label(sample.total_rx_bytes), short_bytes_label(sample.lost_bytes) ), )); } } pub(super) fn status_detail_transport_logs(state: &AppState) -> Vec> { let Some(status) = &state.federation.status else { return vec![Line::styled("transport status is loading", theme::dim())]; }; if status.transport.last.is_empty() { return vec![Line::styled("no connection samples yet", theme::dim())]; } status .transport .last .iter() .map(|sample| { Line::from(vec![ Span::styled(format!("{:<12}", sample.at), theme::dim()), Span::raw(format!( "{} {} {} · {} · {} · tx {} rx {}", sample.protocol, sample.direction, sample.phase, sample.selected_path, rtt_label(sample.selected_rtt_ms), short_bytes_label(sample.total_tx_bytes), short_bytes_label(sample.total_rx_bytes) )), ]) }) .collect() } fn status_detail_device_lines(state: &AppState) -> Vec> { let mut lines = vec![Line::styled("Connected Devices", theme::header_for(state))]; match &state.federation.devices { None => lines.push(Line::styled("loading…", theme::dim())), Some(status) => { lines.push(status_line( "This device", format!("{} · {}", status.this_device_name, status.this_device_id), )); lines.push(status_line("Sync group", status.group_id.clone())); lines.push(status_line( "Devices", format!( "{} active · {} revoked · {} pending", status.active_devices, status.revoked_devices, status.pending_requests ), )); lines.push(status_line( "Sync log", format!( "{} ops · {} outbox · {} tombstones", status.ops_total, status.outbox_ops, status.tombstone_ops ), )); lines.push(status_line( "Snapshot", format!( "{} likes · {} playlists · {} items", status.snapshot_likes, status.snapshot_playlists, status.snapshot_items ), )); lines.push(status_line( "Unresolved", status.unresolved_playlist_items.to_string(), )); lines.push(status_line("Ack floor", status.peer_ack_floor.clone())); if let Some(last_sync) = &status.last_sync { lines.push(status_line("Last sync", last_sync.clone())); } if let Some(last_error) = &status.last_error { lines.push(status_line("Error", first_line(last_error))); } lines.push(Line::default()); lines.push(Line::styled("Device List", theme::header_for(state))); if status.devices.is_empty() { lines.push(status_line("Devices", "none recorded".to_string())); } else { let ordered = crate::app::state::device_status_order(state); let now = crate::app::state::unix_time_ms(); let mut emitted = Vec::new(); for index in &ordered { if let Some(device) = status.devices.get(*index) { push_device_detail_compact( &mut lines, state, device, now, !emitted.is_empty(), ); emitted.push(*index); } } for (index, device) in status.devices.iter().enumerate() { if !emitted.contains(&index) { push_device_detail_compact( &mut lines, state, device, now, !emitted.is_empty(), ); emitted.push(index); } } } } } lines } fn push_device_detail_compact( lines: &mut Vec>, state: &AppState, device: &crate::devices::DeviceStatusRow, now_ms: i64, separator: bool, ) { if separator { lines.push(Line::styled( "────────────────────────────────", theme::dim(), )); } let presence = crate::app::state::device_presence_section(state, device, now_ms); let icon = match presence { DevicePresenceSection::Online => "●", DevicePresenceSection::Offline => "○", DevicePresenceSection::Revoked => "×", }; let mut badges = Vec::new(); if device.is_self { badges.push("this"); } match presence { DevicePresenceSection::Online => badges.push("online"), DevicePresenceSection::Offline => badges.push("offline"), DevicePresenceSection::Revoked => badges.push("revoked"), } let version = if device.client_version.trim().is_empty() { "v?".to_string() } else { format!("v{}", device.client_version) }; lines.push(Line::from(vec![ Span::styled(format!("{icon} "), theme::accent_for(state)), Span::raw(crate::app::state::device_display_name(device)), Span::styled( format!(" · {} · {}", version, badges.join(", ")), theme::dim(), ), ])); lines.push(status_line("Device ID", device.device_id.clone())); lines.push(status_line( "Endpoint", if device.endpoint_id.trim().is_empty() { "unavailable".to_string() } else { short_id(&device.endpoint_id) }, )); lines.push(status_line( "Last seen", relative_time_label(device.last_seen_ms, now_ms), )); } fn relative_time_label(value_ms: Option, now_ms: i64) -> String { let Some(value_ms) = value_ms else { return "unavailable".to_string(); }; let delta_ms = now_ms.saturating_sub(value_ms); if delta_ms < 0 { return "in the future".to_string(); } let seconds = delta_ms / 1000; if seconds < 5 { "just now".to_string() } else if seconds < 60 { format!("{seconds}s ago") } else if seconds < 60 * 60 { format!("{}m ago", seconds / 60) } else if seconds < 24 * 60 * 60 { format!("{}h ago", seconds / 60 / 60) } else { format!("{}d ago", seconds / 60 / 60 / 24) } }