use net_sdk::deck::{MeshOsSnapshot, PeerHealthSnapshot};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
Frame,
};
use crate::{nodes, theme};
#[derive(Clone, Debug)]
pub enum PickNodePurpose {
ForceCutoverTarget { chain: u64 },
ForceEvictHolder { chain: u64 },
}
impl PickNodePurpose {
pub fn headline(&self) -> String {
match self {
Self::ForceCutoverTarget { chain } => {
format!("ICE pick cutover target for chain.0x{chain:x}")
}
Self::ForceEvictHolder { chain } => {
format!("ICE pick holder to evict on chain.0x{chain:x}")
}
}
}
pub fn hint(&self) -> &'static str {
match self {
Self::ForceCutoverTarget { .. } => {
"the chain's elected leader emits RequestPlacement → target on commit"
}
Self::ForceEvictHolder { .. } => {
"the picked holder drops its replica; the chain falls under desired_count"
}
}
}
pub fn candidates(&self, snapshot: &MeshOsSnapshot, this_node: u64) -> Vec<u64> {
match self {
Self::ForceCutoverTarget { .. } => snapshot
.peers
.keys()
.copied()
.filter(|id| *id != this_node)
.collect(),
Self::ForceEvictHolder { chain } => snapshot
.replicas
.get(chain)
.map(|r| r.holders.clone())
.unwrap_or_default(),
}
}
}
pub fn render(
frame: &mut Frame<'_>,
area: Rect,
purpose: &PickNodePurpose,
snapshot: &MeshOsSnapshot,
this_node: u64,
cursor: usize,
) {
let peers = purpose.candidates(snapshot, this_node);
let modal_area = center(area, 64, 22);
frame.render_widget(Clear, modal_area);
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme::red())
.title(Line::from(vec![
Span::styled(" ❄ ", theme::red()),
Span::styled(
"ICE PICK NODE",
Style::default().fg(theme::RED).add_modifier(Modifier::BOLD),
),
Span::raw(" "),
]))
.title_alignment(Alignment::Left);
let inner = block.inner(modal_area);
frame.render_widget(block, modal_area);
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), Constraint::Length(1), ])
.split(inner);
frame.render_widget(
Paragraph::new(Line::from(vec![Span::styled(
purpose.headline(),
Style::default().fg(theme::RED).add_modifier(Modifier::BOLD),
)]))
.alignment(Alignment::Center),
rows[0],
);
frame.render_widget(
Paragraph::new(Line::from(vec![Span::styled(purpose.hint(), theme::dim())]))
.alignment(Alignment::Center),
rows[1],
);
let list_height = rows[3].height as usize;
let lines = peer_lines(snapshot, &peers, cursor, list_height);
frame.render_widget(Paragraph::new(lines), rows[3]);
let bindings = Line::from(vec![
Span::styled("[j/k]", theme::green_hi()),
Span::styled(" cursor ", theme::dim()),
Span::styled("[Enter]", theme::red()),
Span::styled(" select ", theme::dim()),
Span::styled("[Esc]", theme::dim()),
Span::styled(" cancel", theme::dim()),
]);
frame.render_widget(
Paragraph::new(bindings).alignment(Alignment::Center),
rows[4],
);
}
fn peer_lines(
snapshot: &MeshOsSnapshot,
peers: &[u64],
cursor: usize,
height: usize,
) -> Vec<Line<'static>> {
if peers.is_empty() {
return vec![Line::from(vec![Span::styled(
"no peers to pick from",
theme::dim(),
)])];
}
let cursor = cursor.min(peers.len() - 1);
if height == 0 {
return Vec::new();
}
let half = height / 2;
let start = cursor.saturating_sub(half);
let end = (start + height).min(peers.len());
let start = end.saturating_sub(height);
peers[start..end]
.iter()
.enumerate()
.map(|(i, peer_id)| {
let abs = start + i;
let is_cursor = abs == cursor;
let marker = if is_cursor { "▶ " } else { " " };
let id_style = if is_cursor {
theme::green_hi()
} else {
theme::text()
};
let mut spans = vec![Span::styled(marker, theme::green_hi())];
spans.extend(nodes::id_spans_styled(&format!("0x{peer_id:x}"), id_style));
if let Some(p) = snapshot.peers.get(peer_id) {
let (health_style, health_text) = match p.health {
Some(PeerHealthSnapshot::Healthy) => (theme::green(), " · Healthy"),
Some(PeerHealthSnapshot::Degraded) => (theme::amber(), " · Degraded"),
Some(PeerHealthSnapshot::Unreachable) => (theme::red(), " · Unreachable"),
_ => (theme::chrome(), " · —"),
};
spans.push(Span::styled(health_text, health_style));
if let Some(ms) = p.rtt_ms {
spans.push(Span::styled(format!(" RTT {ms}ms"), theme::dim()));
}
}
Line::from(spans)
})
.collect()
}
use super::center;