1use super::footer::NodesToStart;
10use super::header::SelectedMenuItem;
11use super::{
12 footer::Footer, header::Header, popup::manage_nodes::GB_PER_NODE, utils::centered_rect_fixed,
13 Component, Frame,
14};
15use crate::action::OptionsActions;
16use crate::components::popup::port_range::PORT_ALLOCATION;
17use crate::components::utils::open_logs;
18use crate::config::get_launchpad_nodes_data_dir_path;
19use crate::connection_mode::{ConnectionMode, NodeConnectionMode};
20use crate::error::ErrorPopup;
21use crate::node_mgmt::{MaintainNodesArgs, NodeManagement, NodeManagementTask, UpgradeNodesArgs};
22use crate::node_mgmt::{PORT_MAX, PORT_MIN};
23use crate::style::{COOL_GREY, INDIGO, SIZZLING_RED};
24use crate::tui::Event;
25use crate::upnp::UpnpSupport;
26use crate::{
27 action::{Action, StatusActions},
28 config::Config,
29 mode::{InputMode, Scene},
30 node_stats::NodeStats,
31 style::{
32 clear_area, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE,
33 },
34};
35use ant_bootstrap::InitialPeersConfig;
36use ant_node_manager::add_services::config::PortRange;
37use ant_node_manager::config::get_node_registry_path;
38use ant_service_management::{
39 control::ServiceController, NodeRegistry, NodeServiceData, ServiceStatus,
40};
41use color_eyre::eyre::{Ok, OptionExt, Result};
42use crossterm::event::KeyEvent;
43use ratatui::text::Span;
44use ratatui::{prelude::*, widgets::*};
45use std::fmt;
46use std::{
47 path::PathBuf,
48 time::{Duration, Instant},
49 vec,
50};
51use strum::Display;
52use throbber_widgets_tui::{self, Throbber, ThrobberState};
53use tokio::sync::mpsc::UnboundedSender;
54
55pub const FIXED_INTERVAL: u64 = 60_000;
56pub const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5);
57const MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION: usize = 3;
59
60const NODE_WIDTH: usize = 10;
62const VERSION_WIDTH: usize = 7;
63const ATTOS_WIDTH: usize = 5;
64const MEMORY_WIDTH: usize = 7;
65const MBPS_WIDTH: usize = 13;
66const RECORDS_WIDTH: usize = 4;
67const PEERS_WIDTH: usize = 5;
68const CONNS_WIDTH: usize = 5;
69const MODE_WIDTH: usize = 7;
70const STATUS_WIDTH: usize = 8;
71const FAILURE_WIDTH: usize = 64;
72const SPINNER_WIDTH: usize = 1;
73
74#[derive(Clone)]
75pub struct Status<'a> {
76 active: bool,
78 action_sender: Option<UnboundedSender<Action>>,
79 config: Config,
80 is_nat_status_determined: bool,
82 error_while_running_nat_detection: usize,
83 node_stats: NodeStats,
85 node_stats_last_update: Instant,
86 node_services: Vec<NodeServiceData>,
88 items: Option<StatefulTable<NodeItem<'a>>>,
89 network_id: Option<u8>,
91 node_management: NodeManagement,
93 nodes_to_start: usize,
95 rewards_address: String,
97 lock_registry: Option<LockRegistryState>,
100 init_peers_config: InitialPeersConfig,
102 antnode_path: Option<PathBuf>,
104 data_dir_path: PathBuf,
106 connection_mode: ConnectionMode,
108 upnp_support: UpnpSupport,
110 port_from: Option<u32>,
112 port_to: Option<u32>,
114 error_popup: Option<ErrorPopup>,
115}
116
117#[derive(Clone, Display, Debug)]
118pub enum LockRegistryState {
119 StartingNodes,
120 StoppingNodes,
121 ResettingNodes,
122 UpdatingNodes,
123}
124
125pub struct StatusConfig {
126 pub allocated_disk_space: usize,
127 pub antnode_path: Option<PathBuf>,
128 pub connection_mode: ConnectionMode,
129 pub upnp_support: UpnpSupport,
130 pub data_dir_path: PathBuf,
131 pub network_id: Option<u8>,
132 pub init_peers_config: InitialPeersConfig,
133 pub port_from: Option<u32>,
134 pub port_to: Option<u32>,
135 pub rewards_address: String,
136}
137
138impl Status<'_> {
139 pub async fn new(config: StatusConfig) -> Result<Self> {
140 let mut status = Self {
141 init_peers_config: config.init_peers_config,
142 action_sender: Default::default(),
143 config: Default::default(),
144 active: true,
145 is_nat_status_determined: false,
146 error_while_running_nat_detection: 0,
147 network_id: config.network_id,
148 node_stats: NodeStats::default(),
149 node_stats_last_update: Instant::now(),
150 node_services: Default::default(),
151 node_management: NodeManagement::new()?,
152 items: None,
153 nodes_to_start: config.allocated_disk_space,
154 lock_registry: None,
155 rewards_address: config.rewards_address,
156 antnode_path: config.antnode_path,
157 data_dir_path: config.data_dir_path,
158 connection_mode: config.connection_mode,
159 upnp_support: config.upnp_support,
160 port_from: config.port_from,
161 port_to: config.port_to,
162 error_popup: None,
163 };
164
165 let now = Instant::now();
167 debug!("Refreshing node registry states on startup");
168 let mut node_registry = NodeRegistry::load(&get_node_registry_path()?)?;
169 ant_node_manager::refresh_node_registry(
170 &mut node_registry,
171 &ServiceController {},
172 false,
173 true,
174 false,
175 )
176 .await?;
177 node_registry.save()?;
178 debug!("Node registry states refreshed in {:?}", now.elapsed());
179 status.load_node_registry_and_update_states()?;
180
181 Ok(status)
182 }
183
184 fn update_node_items(&mut self, new_status: Option<NodeStatus>) -> Result<()> {
185 if let Some(ref mut items) = self.items {
187 for node_item in self.node_services.iter() {
188 if let Some(item) = items
190 .items
191 .iter_mut()
192 .find(|i| i.name == node_item.service_name)
193 {
194 if let Some(status) = new_status {
195 item.status = status;
196 } else if item.status == NodeStatus::Updating {
197 item.spinner_state.calc_next();
198 } else if new_status != Some(NodeStatus::Updating) {
199 item.status = match node_item.status {
201 ServiceStatus::Running => {
202 item.spinner_state.calc_next();
203 NodeStatus::Running
204 }
205 ServiceStatus::Stopped => NodeStatus::Stopped,
206 ServiceStatus::Added => NodeStatus::Added,
207 ServiceStatus::Removed => NodeStatus::Removed,
208 };
209
210 if let Some(LockRegistryState::StartingNodes) = self.lock_registry {
212 item.spinner_state.calc_next();
213 if item.status != NodeStatus::Running {
214 item.status = NodeStatus::Starting;
215 }
216 }
217 }
218
219 item.peers = match node_item.connected_peers {
221 Some(ref peers) => peers.len(),
222 None => 0,
223 };
224
225 if let Some(stats) = self
227 .node_stats
228 .individual_stats
229 .iter()
230 .find(|s| s.service_name == node_item.service_name)
231 {
232 item.attos = stats.rewards_wallet_balance;
233 item.memory = stats.memory_usage_mb;
234 item.mbps = format!(
235 "↓{:0>5.0} ↑{:0>5.0}",
236 (stats.bandwidth_inbound_rate * 8) as f64 / 1_000_000.0,
237 (stats.bandwidth_outbound_rate * 8) as f64 / 1_000_000.0,
238 );
239 item.records = stats.max_records;
240 item.connections = stats.connections;
241 }
242 } else {
243 let new_item = NodeItem {
245 name: node_item.service_name.clone(),
246 version: node_item.version.to_string(),
247 attos: 0,
248 memory: 0,
249 mbps: "-".to_string(),
250 records: 0,
251 peers: 0,
252 connections: 0,
253 mode: NodeConnectionMode::from(node_item),
254 status: NodeStatus::Added, failure: node_item.get_critical_failure(),
256 spinner: Throbber::default(),
257 spinner_state: ThrobberState::default(),
258 };
259 items.items.push(new_item);
260 }
261 }
262 } else {
263 let node_items: Vec<NodeItem> = self
265 .node_services
266 .iter()
267 .filter_map(|node_item| {
268 if node_item.status == ServiceStatus::Removed {
269 return None;
270 }
271 let status = match node_item.status {
273 ServiceStatus::Running => NodeStatus::Running,
274 ServiceStatus::Stopped => NodeStatus::Stopped,
275 ServiceStatus::Added => NodeStatus::Added,
276 ServiceStatus::Removed => NodeStatus::Removed,
277 };
278
279 Some(NodeItem {
281 name: node_item.service_name.clone().to_string(),
282 version: node_item.version.to_string(),
283 attos: 0,
284 memory: 0,
285 mbps: "-".to_string(),
286 records: 0,
287 peers: 0,
288 connections: 0,
289 mode: NodeConnectionMode::from(node_item),
290 status,
291 failure: node_item.get_critical_failure(),
292 spinner: Throbber::default(),
293 spinner_state: ThrobberState::default(),
294 })
295 })
296 .collect();
297 self.items = Some(StatefulTable::with_items(node_items));
298 }
299 Ok(())
300 }
301
302 fn clear_node_items(&mut self) {
303 debug!("Cleaning items on Status page");
304 if let Some(items) = self.items.as_mut() {
305 items.items.clear();
306 debug!("Cleared the items on status page");
307 }
308 }
309
310 fn try_update_node_stats(&mut self, force_update: bool) -> Result<()> {
313 if self.node_stats_last_update.elapsed() > NODE_STAT_UPDATE_INTERVAL || force_update {
314 self.node_stats_last_update = Instant::now();
315
316 NodeStats::fetch_all_node_stats(&self.node_services, self.get_actions_sender()?);
317 }
318 Ok(())
319 }
320 fn get_actions_sender(&self) -> Result<UnboundedSender<Action>> {
321 self.action_sender
322 .clone()
323 .ok_or_eyre("Action sender not registered")
324 }
325
326 fn load_node_registry_and_update_states(&mut self) -> Result<()> {
327 let node_registry = NodeRegistry::load(&get_node_registry_path()?)?;
328 self.is_nat_status_determined = node_registry.nat_status.is_some();
329 self.node_services = node_registry
330 .nodes
331 .into_iter()
332 .filter(|node| node.status != ServiceStatus::Removed)
333 .collect();
334 info!(
335 "Loaded node registry. Maintaining {:?} nodes.",
336 self.node_services.len()
337 );
338
339 Ok(())
340 }
341
342 fn should_we_run_nat_detection(&self) -> bool {
344 self.connection_mode == ConnectionMode::Automatic
345 && !self.is_nat_status_determined
346 && self.error_while_running_nat_detection < MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION
347 }
348
349 fn get_running_nodes(&self) -> Vec<String> {
350 self.node_services
351 .iter()
352 .filter_map(|node| {
353 if node.status == ServiceStatus::Running {
354 Some(node.service_name.clone())
355 } else {
356 None
357 }
358 })
359 .collect()
360 }
361
362 fn get_service_names_and_peer_ids(&self) -> (Vec<String>, Vec<String>) {
363 let mut service_names = Vec::new();
364 let mut peers_ids = Vec::new();
365
366 for node in &self.node_services {
367 if let Some(peer_id) = &node.peer_id {
369 service_names.push(node.service_name.clone());
370 peers_ids.push(peer_id.to_string().clone());
371 }
372 }
373
374 (service_names, peers_ids)
375 }
376}
377
378impl Component for Status<'_> {
379 fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
380 self.action_sender = Some(tx);
381
382 self.try_update_node_stats(true)?;
384
385 Ok(())
386 }
387
388 fn register_config_handler(&mut self, config: Config) -> Result<()> {
389 self.config = config;
390 Ok(())
391 }
392
393 fn handle_events(&mut self, event: Option<Event>) -> Result<Vec<Action>> {
394 let r = match event {
395 Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
396 _ => vec![],
397 };
398 Ok(r)
399 }
400
401 fn update(&mut self, action: Action) -> Result<Option<Action>> {
402 match action {
403 Action::Tick => {
404 self.try_update_node_stats(false)?;
405 let _ = self.update_node_items(None);
406 }
407 Action::SwitchScene(scene) => match scene {
408 Scene::Status | Scene::StatusRewardsAddressPopUp => {
409 self.active = true;
410 return Ok(Some(Action::SwitchInputMode(InputMode::Navigation)));
412 }
413 Scene::ManageNodesPopUp => self.active = true,
414 _ => self.active = false,
415 },
416 Action::StoreNodesToStart(count) => {
417 self.nodes_to_start = count;
418 if self.nodes_to_start == 0 {
419 info!("Nodes to start set to 0. Sending command to stop all nodes.");
420 return Ok(Some(Action::StatusActions(StatusActions::StopNodes)));
421 } else {
422 info!("Nodes to start set to: {count}. Sending command to start nodes");
423 return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
424 }
425 }
426 Action::StoreRewardsAddress(rewards_address) => {
427 debug!("Storing rewards address: {rewards_address:?}");
428 let has_changed = self.rewards_address != rewards_address;
429 let we_have_nodes = !self.node_services.is_empty();
430
431 self.rewards_address = rewards_address;
432
433 if we_have_nodes && has_changed {
434 debug!("Setting lock_registry to ResettingNodes");
435 self.lock_registry = Some(LockRegistryState::ResettingNodes);
436 info!("Resetting antnode services because the Rewards Address was reset.");
437 let action_sender = self.get_actions_sender()?;
438 self.node_management
439 .send_task(NodeManagementTask::ResetNodes {
440 start_nodes_after_reset: false,
441 action_sender,
442 })?;
443 }
444 }
445 Action::StoreStorageDrive(ref drive_mountpoint, ref _drive_name) => {
446 debug!("Setting lock_registry to ResettingNodes");
447 self.lock_registry = Some(LockRegistryState::ResettingNodes);
448 info!("Resetting antnode services because the Storage Drive was changed.");
449 let action_sender = self.get_actions_sender()?;
450 self.node_management
451 .send_task(NodeManagementTask::ResetNodes {
452 start_nodes_after_reset: false,
453 action_sender,
454 })?;
455 self.data_dir_path =
456 get_launchpad_nodes_data_dir_path(&drive_mountpoint.to_path_buf(), false)?;
457 }
458 Action::StoreConnectionMode(connection_mode) => {
459 debug!("Setting lock_registry to ResettingNodes");
460 self.lock_registry = Some(LockRegistryState::ResettingNodes);
461 self.connection_mode = connection_mode;
462 info!("Resetting antnode services because the Connection Mode range was changed.");
463 let action_sender = self.get_actions_sender()?;
464 self.node_management
465 .send_task(NodeManagementTask::ResetNodes {
466 start_nodes_after_reset: false,
467 action_sender,
468 })?;
469 }
470 Action::StorePortRange(port_from, port_range) => {
471 debug!("Setting lock_registry to ResettingNodes");
472 self.lock_registry = Some(LockRegistryState::ResettingNodes);
473 self.port_from = Some(port_from);
474 self.port_to = Some(port_range);
475 info!("Resetting antnode services because the Port Range was changed.");
476 let action_sender = self.get_actions_sender()?;
477 self.node_management
478 .send_task(NodeManagementTask::ResetNodes {
479 start_nodes_after_reset: false,
480 action_sender,
481 })?;
482 }
483 Action::SetUpnpSupport(ref upnp_support) => {
484 debug!("Setting UPnP support: {upnp_support:?}");
485 self.upnp_support = upnp_support.clone();
486 }
487 Action::StatusActions(status_action) => match status_action {
488 StatusActions::NodesStatsObtained(stats) => {
489 self.node_stats = stats;
490 }
491 StatusActions::StartNodesCompleted | StatusActions::StopNodesCompleted => {
492 self.lock_registry = None;
493 self.load_node_registry_and_update_states()?;
494 }
495 StatusActions::UpdateNodesCompleted => {
496 self.lock_registry = None;
497 self.clear_node_items();
498 self.load_node_registry_and_update_states()?;
499 let _ = self.update_node_items(None);
500 debug!("Update nodes completed");
501 }
502 StatusActions::ResetNodesCompleted { trigger_start_node } => {
503 self.lock_registry = None;
504 self.load_node_registry_and_update_states()?;
505 self.clear_node_items();
506
507 if trigger_start_node {
508 debug!("Reset nodes completed. Triggering start nodes.");
509 return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
510 }
511 debug!("Reset nodes completed");
512 }
513 StatusActions::SuccessfullyDetectedNatStatus => {
514 debug!(
515 "Successfully detected nat status, is_nat_status_determined set to true"
516 );
517 self.is_nat_status_determined = true;
518 }
519 StatusActions::ErrorWhileRunningNatDetection => {
520 self.error_while_running_nat_detection += 1;
521 debug!(
522 "Error while running nat detection. Error count: {}",
523 self.error_while_running_nat_detection
524 );
525 }
526 StatusActions::ErrorLoadingNodeRegistry { raw_error }
527 | StatusActions::ErrorGettingNodeRegistryPath { raw_error } => {
528 self.error_popup = Some(ErrorPopup::new(
529 "Error".to_string(),
530 "Error getting node registry path".to_string(),
531 raw_error,
532 ));
533 if let Some(error_popup) = &mut self.error_popup {
534 error_popup.show();
535 }
536 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
538 }
539 StatusActions::ErrorScalingUpNodes { raw_error } => {
540 self.error_popup = Some(ErrorPopup::new(
541 "Error".to_string(),
542 "Error adding new nodes".to_string(),
543 raw_error,
544 ));
545 if let Some(error_popup) = &mut self.error_popup {
546 error_popup.show();
547 }
548 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
550 }
551 StatusActions::ErrorStoppingNodes { raw_error } => {
552 self.error_popup = Some(ErrorPopup::new(
553 "Error".to_string(),
554 "Error stopping nodes".to_string(),
555 raw_error,
556 ));
557 if let Some(error_popup) = &mut self.error_popup {
558 error_popup.show();
559 }
560 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
562 }
563 StatusActions::ErrorUpdatingNodes { raw_error } => {
564 self.error_popup = Some(ErrorPopup::new(
565 "Error".to_string(),
566 "Error upgrading nodes".to_string(),
567 raw_error,
568 ));
569 if let Some(error_popup) = &mut self.error_popup {
570 error_popup.show();
571 }
572 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
574 }
575 StatusActions::ErrorResettingNodes { raw_error } => {
576 self.error_popup = Some(ErrorPopup::new(
577 "Error".to_string(),
578 "Error resetting nodes".to_string(),
579 raw_error,
580 ));
581 if let Some(error_popup) = &mut self.error_popup {
582 error_popup.show();
583 }
584 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
586 }
587 StatusActions::TriggerManageNodes => {
588 return Ok(Some(Action::SwitchScene(Scene::ManageNodesPopUp)));
589 }
590 StatusActions::PreviousTableItem => {
591 if let Some(items) = &mut self.items {
592 items.previous();
593 }
594 }
595 StatusActions::NextTableItem => {
596 if let Some(items) = &mut self.items {
597 items.next();
598 }
599 }
600 StatusActions::StartNodes => {
601 debug!("Got action to start nodes");
602
603 if self.rewards_address.is_empty() {
604 info!("Rewards address is not set. Ask for input.");
605 return Ok(Some(Action::StatusActions(
606 StatusActions::TriggerRewardsAddress,
607 )));
608 }
609
610 if self.nodes_to_start == 0 {
611 info!("Nodes to start not set. Ask for input.");
612 return Ok(Some(Action::StatusActions(
613 StatusActions::TriggerManageNodes,
614 )));
615 }
616
617 if self.lock_registry.is_some() {
618 error!(
619 "Registry is locked ({:?}) Cannot Start nodes now.",
620 self.lock_registry
621 );
622 return Ok(None);
623 }
624
625 debug!("Setting lock_registry to StartingNodes");
626 self.lock_registry = Some(LockRegistryState::StartingNodes);
627
628 let port_range = PortRange::Range(
629 self.port_from.unwrap_or(PORT_MIN) as u16,
630 self.port_to.unwrap_or(PORT_MAX) as u16,
631 );
632
633 let action_sender = self.get_actions_sender()?;
634
635 let maintain_nodes_args = MaintainNodesArgs {
636 action_sender: action_sender.clone(),
637 antnode_path: self.antnode_path.clone(),
638 connection_mode: self.connection_mode,
639 count: self.nodes_to_start as u16,
640 data_dir_path: Some(self.data_dir_path.clone()),
641 network_id: self.network_id,
642 owner: self.rewards_address.clone(),
643 init_peers_config: self.init_peers_config.clone(),
644 port_range: Some(port_range),
645 rewards_address: self.rewards_address.clone(),
646 run_nat_detection: self.should_we_run_nat_detection(),
647 };
648
649 debug!("Calling maintain_n_running_nodes");
650
651 self.node_management
652 .send_task(NodeManagementTask::MaintainNodes {
653 args: maintain_nodes_args,
654 })?;
655 }
656 StatusActions::StopNodes => {
657 debug!("Got action to stop nodes");
658 if self.lock_registry.is_some() {
659 error!(
660 "Registry is locked ({:?}) Cannot Stop nodes now.",
661 self.lock_registry
662 );
663 return Ok(None);
664 }
665
666 let running_nodes = self.get_running_nodes();
667 debug!("Setting lock_registry to StoppingNodes");
668 self.lock_registry = Some(LockRegistryState::StoppingNodes);
669 let action_sender = self.get_actions_sender()?;
670 info!("Stopping node service: {running_nodes:?}");
671
672 self.node_management
673 .send_task(NodeManagementTask::StopNodes {
674 services: running_nodes,
675 action_sender,
676 })?;
677 }
678 StatusActions::TriggerRewardsAddress => {
679 if self.rewards_address.is_empty() {
680 return Ok(Some(Action::SwitchScene(Scene::StatusRewardsAddressPopUp)));
681 } else {
682 return Ok(None);
683 }
684 }
685 StatusActions::TriggerNodeLogs => {
686 if let Some(node) = self.items.as_ref().and_then(|items| items.selected_item())
687 {
688 debug!("Got action to open node logs {:?}", node.name);
689 open_logs(Some(node.name.clone()))?;
690 } else {
691 debug!("Got action to open node logs but no node was selected.");
692 }
693 }
694 },
695 Action::OptionsActions(OptionsActions::UpdateNodes) => {
696 debug!("Got action to Update Nodes");
697 self.load_node_registry_and_update_states()?;
698 if self.lock_registry.is_some() {
699 error!(
700 "Registry is locked ({:?}) Cannot Update nodes now. Stop them first.",
701 self.lock_registry
702 );
703 return Ok(None);
704 } else {
705 debug!("Lock registry ({:?})", self.lock_registry);
706 };
707 debug!("Setting lock_registry to UpdatingNodes");
708 self.lock_registry = Some(LockRegistryState::UpdatingNodes);
709 let action_sender = self.get_actions_sender()?;
710 info!("Got action to update nodes");
711 let _ = self.update_node_items(Some(NodeStatus::Updating));
712 let (service_names, peer_ids) = self.get_service_names_and_peer_ids();
713
714 let upgrade_nodes_args = UpgradeNodesArgs {
715 action_sender,
716 connection_timeout_s: 5,
717 do_not_start: true,
718 custom_bin_path: None,
719 force: false,
720 fixed_interval: Some(FIXED_INTERVAL),
721 peer_ids,
722 provided_env_variables: None,
723 service_names,
724 url: None,
725 version: None,
726 };
727 self.node_management
728 .send_task(NodeManagementTask::UpgradeNodes {
729 args: upgrade_nodes_args,
730 })?;
731 }
732 Action::OptionsActions(OptionsActions::ResetNodes) => {
733 debug!("Got action to reset nodes");
734 if self.lock_registry.is_some() {
735 error!(
736 "Registry is locked ({:?}) Cannot Reset nodes now.",
737 self.lock_registry
738 );
739 return Ok(None);
740 }
741
742 debug!("Setting lock_registry to ResettingNodes");
743 self.lock_registry = Some(LockRegistryState::ResettingNodes);
744 let action_sender = self.get_actions_sender()?;
745 info!("Got action to reset nodes");
746 self.node_management
747 .send_task(NodeManagementTask::ResetNodes {
748 start_nodes_after_reset: false,
749 action_sender,
750 })?;
751 }
752 _ => {}
753 }
754 Ok(None)
755 }
756
757 fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
758 if !self.active {
759 return Ok(());
760 }
761
762 let layout = Layout::new(
763 Direction::Vertical,
764 [
765 Constraint::Length(1),
767 Constraint::Max(6),
769 Constraint::Min(3),
771 Constraint::Length(3),
773 ],
774 )
775 .split(area);
776
777 let header = Header::new();
780 f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Status);
781
782 let combined_block = Block::default()
788 .title(" Device Status ")
789 .bold()
790 .title_style(Style::default().fg(GHOST_WHITE))
791 .borders(Borders::ALL)
792 .padding(Padding::horizontal(1))
793 .style(Style::default().fg(VERY_LIGHT_AZURE));
794
795 f.render_widget(combined_block.clone(), layout[1]);
796
797 let storage_allocated_row = Row::new(vec![
798 Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE),
799 Cell::new(format!("{} GB", self.nodes_to_start * GB_PER_NODE)).fg(GHOST_WHITE),
800 ]);
801 let memory_use_val = if self.node_stats.total_memory_usage_mb as f64 / 1024_f64 > 1.0 {
802 format!(
803 "{:.2} GB",
804 self.node_stats.total_memory_usage_mb as f64 / 1024_f64
805 )
806 } else {
807 format!("{} MB", self.node_stats.total_memory_usage_mb)
808 };
809
810 let memory_use_row = Row::new(vec![
811 Cell::new("Memory Use".to_string()).fg(GHOST_WHITE),
812 Cell::new(memory_use_val).fg(GHOST_WHITE),
813 ]);
814
815 let connection_mode_string = match self.connection_mode {
816 ConnectionMode::HomeNetwork => "Home Network".to_string(),
817 ConnectionMode::UPnP => "UPnP".to_string(),
818 ConnectionMode::CustomPorts => format!(
819 "Custom Ports {}-{}",
820 self.port_from.unwrap_or(PORT_MIN),
821 self.port_to.unwrap_or(PORT_MIN + PORT_ALLOCATION)
822 ),
823 ConnectionMode::Automatic => "Automatic".to_string(),
824 };
825
826 let mut connection_mode_line = vec![Span::styled(
827 connection_mode_string,
828 Style::default().fg(GHOST_WHITE),
829 )];
830
831 if matches!(
832 self.connection_mode,
833 ConnectionMode::Automatic | ConnectionMode::UPnP
834 ) {
835 connection_mode_line.push(Span::styled(" (", Style::default().fg(GHOST_WHITE)));
836
837 if self.connection_mode == ConnectionMode::Automatic {
838 connection_mode_line.push(Span::styled("UPnP: ", Style::default().fg(GHOST_WHITE)));
839 }
840
841 let span = match self.upnp_support {
842 UpnpSupport::Supported => {
843 Span::styled("supported", Style::default().fg(EUCALYPTUS))
844 }
845 UpnpSupport::Unsupported => {
846 Span::styled("disabled / unsupported", Style::default().fg(SIZZLING_RED))
847 }
848 UpnpSupport::Loading => {
849 Span::styled("loading..", Style::default().fg(LIGHT_PERIWINKLE))
850 }
851 UpnpSupport::Unknown => {
852 Span::styled("unknown", Style::default().fg(LIGHT_PERIWINKLE))
853 }
854 };
855
856 connection_mode_line.push(span);
857
858 connection_mode_line.push(Span::styled(")", Style::default().fg(GHOST_WHITE)));
859 }
860
861 let connection_mode_row = Row::new(vec![
862 Cell::new("Connection".to_string()).fg(GHOST_WHITE),
863 Cell::new(Line::from(connection_mode_line)),
864 ]);
865
866 let stats_rows = vec![storage_allocated_row, memory_use_row, connection_mode_row];
867 let stats_width = [Constraint::Length(5)];
868 let column_constraints = [Constraint::Length(23), Constraint::Fill(1)];
869 let stats_table = Table::new(stats_rows, stats_width).widths(column_constraints);
870
871 let wallet_not_set = if self.rewards_address.is_empty() {
872 vec![
873 Span::styled("Press ".to_string(), Style::default().fg(VIVID_SKY_BLUE)),
874 Span::styled("[Ctrl+B] ".to_string(), Style::default().fg(GHOST_WHITE)),
875 Span::styled(
876 "to add your ".to_string(),
877 Style::default().fg(VIVID_SKY_BLUE),
878 ),
879 Span::styled(
880 "Wallet Address".to_string(),
881 Style::default().fg(VIVID_SKY_BLUE).bold(),
882 ),
883 ]
884 } else {
885 vec![]
886 };
887
888 let total_attos_earned_and_wallet_row = Row::new(vec![
889 Cell::new("Attos Earned".to_string()).fg(VIVID_SKY_BLUE),
890 Cell::new(format!(
891 "{:?}",
892 self.node_stats.total_rewards_wallet_balance
893 ))
894 .fg(VIVID_SKY_BLUE)
895 .bold(),
896 Cell::new(Line::from(wallet_not_set).alignment(Alignment::Right)),
897 ]);
898
899 let attos_wallet_rows = vec![total_attos_earned_and_wallet_row];
900 let attos_wallet_width = [Constraint::Length(5)];
901 let column_constraints = [
902 Constraint::Length(23),
903 Constraint::Fill(1),
904 Constraint::Length(if self.rewards_address.is_empty() {
905 41 } else {
907 0
908 }),
909 ];
910 let attos_wallet_table =
911 Table::new(attos_wallet_rows, attos_wallet_width).widths(column_constraints);
912
913 let inner_area = combined_block.inner(layout[1]);
914 let device_layout = Layout::new(
915 Direction::Vertical,
916 vec![Constraint::Length(5), Constraint::Length(1)],
917 )
918 .split(inner_area);
919
920 f.render_widget(stats_table, device_layout[0]);
922 f.render_widget(attos_wallet_table, device_layout[1]);
923
924 if let Some(ref items) = self.items {
928 if items.items.is_empty() || self.rewards_address.is_empty() {
929 let line1 = Line::from(vec![
930 Span::styled("Press ", Style::default().fg(LIGHT_PERIWINKLE)),
931 Span::styled("[Ctrl+G] ", Style::default().fg(GHOST_WHITE).bold()),
932 Span::styled("to Add and ", Style::default().fg(LIGHT_PERIWINKLE)),
933 Span::styled("Start Nodes ", Style::default().fg(GHOST_WHITE).bold()),
934 Span::styled("on this device", Style::default().fg(LIGHT_PERIWINKLE)),
935 ]);
936
937 let line2 = Line::from(vec![Span::styled(
938 format!(
939 "Each node will use {}GB of storage and a small amount of memory, \
940 CPU, and Network bandwidth. Most computers can run many nodes at once, \
941 but we recommend you add them gradually",
942 GB_PER_NODE
943 ),
944 Style::default().fg(LIGHT_PERIWINKLE),
945 )]);
946
947 f.render_widget(
948 Paragraph::new(vec![Line::raw(""), line1, Line::raw(""), line2])
949 .wrap(Wrap { trim: false })
950 .fg(LIGHT_PERIWINKLE)
951 .block(
952 Block::default()
953 .title(Line::from(vec![
954 Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
955 Span::styled(" (0) ", Style::default().fg(LIGHT_PERIWINKLE)),
956 ]))
957 .title_style(Style::default().fg(LIGHT_PERIWINKLE))
958 .borders(Borders::ALL)
959 .border_style(style::Style::default().fg(EUCALYPTUS))
960 .padding(Padding::horizontal(1)),
961 ),
962 layout[2],
963 );
964 } else {
965 let block_nodes = Block::default()
967 .title(Line::from(vec![
968 Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
969 Span::styled(
970 format!(" ({}) ", self.nodes_to_start),
971 Style::default().fg(LIGHT_PERIWINKLE),
972 ),
973 ]))
974 .padding(Padding::new(1, 1, 0, 0))
975 .title_style(Style::default().fg(GHOST_WHITE))
976 .borders(Borders::ALL)
977 .border_style(Style::default().fg(EUCALYPTUS));
978
979 let inner_area = block_nodes.inner(layout[2]);
981
982 let node_widths = [
984 Constraint::Min(NODE_WIDTH as u16),
985 Constraint::Min(VERSION_WIDTH as u16),
986 Constraint::Min(ATTOS_WIDTH as u16),
987 Constraint::Min(MEMORY_WIDTH as u16),
988 Constraint::Min(MBPS_WIDTH as u16),
989 Constraint::Min(RECORDS_WIDTH as u16),
990 Constraint::Min(PEERS_WIDTH as u16),
991 Constraint::Min(CONNS_WIDTH as u16),
992 Constraint::Min(MODE_WIDTH as u16),
993 Constraint::Min(STATUS_WIDTH as u16),
994 Constraint::Fill(FAILURE_WIDTH as u16),
995 Constraint::Max(SPINNER_WIDTH as u16),
996 ];
997
998 let header_row = Row::new(vec![
1000 Cell::new("Node").fg(COOL_GREY),
1001 Cell::new("Version").fg(COOL_GREY),
1002 Cell::new("Attos").fg(COOL_GREY),
1003 Cell::new("Memory").fg(COOL_GREY),
1004 Cell::new(
1005 format!("{}{}", " ".repeat(MBPS_WIDTH - "Mbps".len()), "Mbps")
1006 .fg(COOL_GREY),
1007 ),
1008 Cell::new("Recs").fg(COOL_GREY),
1009 Cell::new("Peers").fg(COOL_GREY),
1010 Cell::new("Conns").fg(COOL_GREY),
1011 Cell::new("Mode").fg(COOL_GREY),
1012 Cell::new("Status").fg(COOL_GREY),
1013 Cell::new("Failure").fg(COOL_GREY),
1014 Cell::new(" ").fg(COOL_GREY), ])
1016 .style(Style::default().add_modifier(Modifier::BOLD));
1017
1018 let mut items: Vec<Row> = Vec::new();
1019 if let Some(ref mut items_table) = self.items {
1020 for (i, node_item) in items_table.items.iter_mut().enumerate() {
1021 let is_selected = items_table.state.selected() == Some(i);
1022 items.push(node_item.render_as_row(i, layout[2], f, is_selected));
1023 }
1024 }
1025
1026 let table = Table::new(items, node_widths)
1028 .header(header_row)
1029 .column_spacing(1)
1030 .row_highlight_style(Style::default().bg(INDIGO))
1031 .highlight_spacing(HighlightSpacing::Always);
1032
1033 f.render_widget(table, inner_area);
1034
1035 f.render_widget(block_nodes, layout[2]);
1036 }
1037 }
1038
1039 let footer = Footer::default();
1042 let footer_state = if let Some(ref items) = self.items {
1043 if !items.items.is_empty() || self.rewards_address.is_empty() {
1044 if !self.get_running_nodes().is_empty() {
1045 &mut NodesToStart::Running
1046 } else {
1047 &mut NodesToStart::Configured
1048 }
1049 } else {
1050 &mut NodesToStart::NotConfigured
1051 }
1052 } else {
1053 &mut NodesToStart::NotConfigured
1054 };
1055 f.render_stateful_widget(footer, layout[3], footer_state);
1056
1057 if let Some(error_popup) = &self.error_popup {
1061 if error_popup.is_visible() {
1062 error_popup.draw_error(f, area);
1063
1064 return Ok(());
1065 }
1066 }
1067
1068 if let Some(registry_state) = &self.lock_registry {
1070 let popup_text = match registry_state {
1071 LockRegistryState::StartingNodes => {
1072 if self.should_we_run_nat_detection() {
1073 vec![
1074 Line::raw("Starting nodes..."),
1075 Line::raw(""),
1076 Line::raw(""),
1077 Line::raw("Please wait, performing initial NAT detection"),
1078 Line::raw("This may take a couple minutes."),
1079 ]
1080 } else {
1081 return Ok(());
1083 }
1084 }
1085 LockRegistryState::StoppingNodes => {
1086 vec![
1087 Line::raw(""),
1088 Line::raw(""),
1089 Line::raw(""),
1090 Line::raw("Stopping nodes..."),
1091 ]
1092 }
1093 LockRegistryState::ResettingNodes => {
1094 vec![
1095 Line::raw(""),
1096 Line::raw(""),
1097 Line::raw(""),
1098 Line::raw("Resetting nodes..."),
1099 ]
1100 }
1101 LockRegistryState::UpdatingNodes => {
1102 return Ok(());
1103 }
1104 };
1105 if !popup_text.is_empty() {
1106 let popup_area = centered_rect_fixed(50, 12, area);
1107 clear_area(f, popup_area);
1108
1109 let popup_border = Paragraph::new("").block(
1110 Block::default()
1111 .borders(Borders::ALL)
1112 .title(" Manage Nodes ")
1113 .bold()
1114 .title_style(Style::new().fg(VIVID_SKY_BLUE))
1115 .padding(Padding::uniform(2))
1116 .border_style(Style::new().fg(GHOST_WHITE)),
1117 );
1118
1119 let centred_area = Layout::new(
1120 Direction::Vertical,
1121 vec![
1122 Constraint::Length(2),
1124 Constraint::Min(5),
1126 Constraint::Length(1),
1128 ],
1129 )
1130 .split(popup_area);
1131 let text = Paragraph::new(popup_text)
1132 .block(Block::default().padding(Padding::horizontal(2)))
1133 .wrap(Wrap { trim: false })
1134 .alignment(Alignment::Center)
1135 .fg(EUCALYPTUS);
1136 f.render_widget(text, centred_area[1]);
1137
1138 f.render_widget(popup_border, popup_area);
1139 }
1140 }
1141
1142 Ok(())
1143 }
1144
1145 fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
1146 debug!("Key received in Status: {:?}", key);
1147 if let Some(error_popup) = &mut self.error_popup {
1148 if error_popup.is_visible() {
1149 error_popup.handle_input(key);
1150 return Ok(vec![Action::SwitchInputMode(InputMode::Navigation)]);
1151 }
1152 }
1153 Ok(vec![])
1154 }
1155}
1156
1157#[allow(dead_code)]
1158#[derive(Default, Clone)]
1159struct StatefulTable<T> {
1160 state: TableState,
1161 items: Vec<T>,
1162 last_selected: Option<usize>,
1163}
1164
1165#[allow(dead_code)]
1166impl<T> StatefulTable<T> {
1167 fn with_items(items: Vec<T>) -> Self {
1168 StatefulTable {
1169 state: TableState::default(),
1170 items,
1171 last_selected: None,
1172 }
1173 }
1174
1175 fn next(&mut self) {
1176 let i = match self.state.selected() {
1177 Some(i) => {
1178 if i >= self.items.len() - 1 {
1179 0
1180 } else {
1181 i + 1
1182 }
1183 }
1184 None => self.last_selected.unwrap_or(0),
1185 };
1186 self.state.select(Some(i));
1187 self.last_selected = Some(i);
1188 }
1189
1190 fn previous(&mut self) {
1191 let i = match self.state.selected() {
1192 Some(i) => {
1193 if i == 0 {
1194 self.items.len() - 1
1195 } else {
1196 i - 1
1197 }
1198 }
1199 None => self.last_selected.unwrap_or(0),
1200 };
1201 self.state.select(Some(i));
1202 self.last_selected = Some(i);
1203 }
1204
1205 fn selected_item(&self) -> Option<&T> {
1206 self.state
1207 .selected()
1208 .and_then(|index| self.items.get(index))
1209 }
1210}
1211
1212#[derive(Default, Debug, Copy, Clone, PartialEq)]
1213enum NodeStatus {
1214 #[default]
1215 Added,
1216 Running,
1217 Starting,
1218 Stopped,
1219 Removed,
1220 Updating,
1221}
1222
1223impl fmt::Display for NodeStatus {
1224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1225 match *self {
1226 NodeStatus::Added => write!(f, "Added"),
1227 NodeStatus::Running => write!(f, "Running"),
1228 NodeStatus::Starting => write!(f, "Starting"),
1229 NodeStatus::Stopped => write!(f, "Stopped"),
1230 NodeStatus::Removed => write!(f, "Removed"),
1231 NodeStatus::Updating => write!(f, "Updating"),
1232 }
1233 }
1234}
1235
1236#[derive(Default, Debug, Clone)]
1237pub struct NodeItem<'a> {
1238 name: String,
1239 version: String,
1240 attos: usize,
1241 memory: usize,
1242 mbps: String,
1243 records: usize,
1244 peers: usize,
1245 connections: usize,
1246 mode: NodeConnectionMode,
1247 status: NodeStatus,
1248 failure: Option<(chrono::DateTime<chrono::Utc>, String)>,
1249 spinner: Throbber<'a>,
1250 spinner_state: ThrobberState,
1251}
1252
1253impl NodeItem<'_> {
1254 fn render_as_row(
1255 &mut self,
1256 index: usize,
1257 area: Rect,
1258 f: &mut Frame<'_>,
1259 is_selected: bool,
1260 ) -> Row {
1261 let mut row_style = if is_selected {
1262 Style::default().fg(GHOST_WHITE).bg(INDIGO)
1263 } else {
1264 Style::default().fg(GHOST_WHITE)
1265 };
1266 let mut spinner_state = self.spinner_state.clone();
1267 match self.status {
1268 NodeStatus::Running => {
1269 self.spinner = self
1270 .spinner
1271 .clone()
1272 .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1273 .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1274 .use_type(throbber_widgets_tui::WhichUse::Spin);
1275 row_style = if is_selected {
1276 Style::default().fg(EUCALYPTUS).bg(INDIGO)
1277 } else {
1278 Style::default().fg(EUCALYPTUS)
1279 };
1280 }
1281 NodeStatus::Starting => {
1282 self.spinner = self
1283 .spinner
1284 .clone()
1285 .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1286 .throbber_set(throbber_widgets_tui::BOX_DRAWING)
1287 .use_type(throbber_widgets_tui::WhichUse::Spin);
1288 }
1289 NodeStatus::Stopped => {
1290 self.spinner = self
1291 .spinner
1292 .clone()
1293 .throbber_style(
1294 Style::default()
1295 .fg(GHOST_WHITE)
1296 .add_modifier(Modifier::BOLD),
1297 )
1298 .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1299 .use_type(throbber_widgets_tui::WhichUse::Full);
1300 }
1301 NodeStatus::Updating => {
1302 self.spinner = self
1303 .spinner
1304 .clone()
1305 .throbber_style(
1306 Style::default()
1307 .fg(GHOST_WHITE)
1308 .add_modifier(Modifier::BOLD),
1309 )
1310 .throbber_set(throbber_widgets_tui::VERTICAL_BLOCK)
1311 .use_type(throbber_widgets_tui::WhichUse::Spin);
1312 }
1313 _ => {}
1314 };
1315
1316 let failure = self.failure.as_ref().map_or_else(
1317 || "-".to_string(),
1318 |(_dt, msg)| {
1319 if self.status == NodeStatus::Stopped {
1320 msg.clone()
1321 } else {
1322 "-".to_string()
1323 }
1324 },
1325 );
1326
1327 let row = vec![
1328 self.name.clone().to_string(),
1329 self.version.to_string(),
1330 format!(
1331 "{}{}",
1332 " ".repeat(ATTOS_WIDTH.saturating_sub(self.attos.to_string().len())),
1333 self.attos.to_string()
1334 ),
1335 format!(
1336 "{}{} MB",
1337 " ".repeat(MEMORY_WIDTH.saturating_sub(self.memory.to_string().len() + 4)),
1338 self.memory.to_string()
1339 ),
1340 format!(
1341 "{}{}",
1342 " ".repeat(MBPS_WIDTH.saturating_sub(self.mbps.to_string().len())),
1343 self.mbps.to_string()
1344 ),
1345 format!(
1346 "{}{}",
1347 " ".repeat(RECORDS_WIDTH.saturating_sub(self.records.to_string().len())),
1348 self.records.to_string()
1349 ),
1350 format!(
1351 "{}{}",
1352 " ".repeat(PEERS_WIDTH.saturating_sub(self.peers.to_string().len())),
1353 self.peers.to_string()
1354 ),
1355 format!(
1356 "{}{}",
1357 " ".repeat(CONNS_WIDTH.saturating_sub(self.connections.to_string().len())),
1358 self.connections.to_string()
1359 ),
1360 self.mode.to_string(),
1361 self.status.to_string(),
1362 failure,
1363 ];
1364 let throbber_area = Rect::new(area.width - 3, area.y + 2 + index as u16, 1, 1);
1365
1366 f.render_stateful_widget(self.spinner.clone(), throbber_area, &mut spinner_state);
1367
1368 Row::new(row).style(row_style)
1369 }
1370}