//! chrs-observer: Real-time TUI dashboard for CHORUS cluster monitoring. use ratatui::{ backend::CrosstermBackend, widgets::{Block, Borders, Paragraph, List, ListItem, Gauge, BorderType}, layout::{Layout, Constraint, Direction, Alignment}, style::{Color, Modifier, Style}, Terminal, }; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use std::{error::Error, io, time::{Duration, Instant}}; use tokio::sync::mpsc; use chrs_discovery::{SwarmManager, BusMessage}; use chrs_backbeat::BeatFrame; use chrs_council::Peer; use std::collections::HashMap; struct Project { name: String, status: String, } /// State of the TUI application. struct App { pulse_bpm: u32, beat_index: u32, peers: HashMap, logs: Vec, projects: Vec, } impl App { fn new() -> App { App { pulse_bpm: 30, beat_index: 0, peers: HashMap::new(), logs: vec!["[OBSERVER] Initialized. Waiting for P2P events...".into()], projects: Vec::new(), } } async fn update_projects(&mut self) { if let Ok(res) = reqwest::get("http://127.0.0.1:3001/api/status").await { if let Ok(projs) = res.json::().await { if let Some(arr) = projs.as_array() { self.projects = arr.iter().map(|v| Project { name: v["name"].as_str().unwrap_or("Unknown").to_string(), status: v["status"].as_str().unwrap_or("Unknown").to_string(), }).collect(); } } } } fn add_log(&mut self, log: String) { let now = chrono::Local::now().format("%H:%M:%S").to_string(); self.logs.push(format!("[{}] {}", now, log)); if self.logs.len() > 100 { self.logs.remove(0); } } } #[tokio::main] async fn main() -> Result<(), Box> { // 1. Setup LibP2P Bus let (bus_tx, mut bus_rx) = mpsc::unbounded_channel::(); let _bus_handle = SwarmManager::start_bus(bus_tx).await?; // 2. Setup Terminal enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; // 3. App State let mut app = App::new(); // 4. Main Loop let tick_rate = Duration::from_millis(100); let mut last_tick = Instant::now(); let mut last_status_update = Instant::now(); loop { terminal.draw(|f| ui(f, &app))?; // Periodically fetch project status from GUI API if last_status_update.elapsed() > Duration::from_secs(5) { app.update_projects().await; last_status_update = Instant::now(); } // Handle P2P Messages while let Ok(msg) = bus_rx.try_recv() { if msg.topic == "chorus-heartbeat" { if let Ok(peer) = serde_json::from_slice::(&msg.payload) { if !app.peers.contains_key(&peer.id) { app.add_log(format!("Discovered Agent: {} ({:?})", peer.id, peer.role)); } app.peers.insert(peer.id.clone(), peer); } } else if msg.topic == "chorus-global" || msg.topic == "beat_frame" { if let Ok(frame) = serde_json::from_slice::(&msg.payload) { app.beat_index = frame.beat_index; app.pulse_bpm = frame.tempo_bpm; } } } // Handle Input let timeout = tick_rate .checked_sub(last_tick.elapsed()) .unwrap_or_else(|| Duration::from_secs(0)); if crossterm::event::poll(timeout)? { if let Event::Key(key) = event::read()? { if let KeyCode::Char('q') = key.code { break; } } } if last_tick.elapsed() >= tick_rate { last_tick = Instant::now(); } } // Restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; Ok(()) } fn ui(f: &mut ratatui::Frame, app: &App) { let size = f.size(); let chunks = Layout::default() .direction(Direction::Vertical) .constraints( [ Constraint::Length(3), // Pulse and Status Constraint::Min(0), // Main Body ] .as_ref(), ) .split(size); // --- Header / Pulse --- let header_chunks = Layout::default() .direction(Direction::Horizontal) .constraints( [ Constraint::Percentage(40), // BPM & Status Constraint::Percentage(60), // Rhythm Progress ] .as_ref(), ) .split(chunks[0]); let status_text = format!(" CHORUS Cluster | {} BPM | Agents: {}", app.pulse_bpm, app.peers.len()); let status_para = Paragraph::new(status_text) .block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded)) .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) .alignment(Alignment::Left); f.render_widget(status_para, header_chunks[0]); // Visual Beat Counter: [ 1 2 3 4 5 6 7 8 ] let mut beat_viz = String::from(" ["); for i in 1..=8 { if i == app.beat_index { beat_viz.push_str(&format!(" {} ", i)); } else { beat_viz.push_str(" . "); } } beat_viz.push(']'); let beat_progress = (app.beat_index as f32 / 8.0) * 100.0; let pulse_gauge = Gauge::default() .block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded).title(" Cluster Pulse ")) .gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray)) .percent(beat_progress as u16) .label(beat_viz); f.render_widget(pulse_gauge, header_chunks[1]); // --- Body (Peers, Projects, and Logs) --- let body_chunks = Layout::default() .direction(Direction::Horizontal) .constraints( [ Constraint::Percentage(25), // Peer List Constraint::Percentage(25), // Project List Constraint::Percentage(50), // Logs ] .as_ref(), ) .split(chunks[1]); // Peer List let peers: Vec = app.peers.values() .map(|p| { let content = format!(" {} [{:?}] W: {:.2}", p.id, p.role, p.resource_score); ListItem::new(content).style(Style::default().fg(Color::Yellow)) }) .collect(); let peer_list = List::new(peers) .block(Block::default().borders(Borders::ALL).title(" Active Agents ")) .highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_symbol(">> "); f.render_widget(peer_list, body_chunks[0]); // Project List let projects: Vec = app.projects.iter() .map(|p| { let content = format!(" {} [{}]", p.name, p.status); ListItem::new(content).style(Style::default().fg(Color::Green)) }) .collect(); let project_list = List::new(projects) .block(Block::default().borders(Borders::ALL).title(" Local Projects ")); f.render_widget(project_list, body_chunks[1]); // Logs let logs: Vec = app.logs.iter().rev() .map(|s| ListItem::new(s.as_str())) .collect(); let log_list = List::new(logs) .block(Block::default().borders(Borders::ALL).title(" Live Event Bus ")); f.render_widget(log_list, body_chunks[2]); }