1use super::footer::NodesToStart;
10use super::header::SelectedMenuItem;
11use super::popup::manage_nodes::GB;
12use super::utils::centered_rect_fixed;
13use super::{Component, Frame, footer::Footer, header::Header, popup::manage_nodes::GB_PER_NODE};
14use crate::action::OptionsActions;
15use crate::components::popup::manage_nodes::MAX_NODE_COUNT;
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::{
22 FIXED_INTERVAL, MaintainNodesArgs, NODES_ALL, NodeManagement, NodeManagementTask,
23 UpgradeNodesArgs,
24};
25use crate::node_mgmt::{PORT_MAX, PORT_MIN};
26use crate::style::{COOL_GREY, INDIGO, SIZZLING_RED, clear_area};
27use crate::system::{get_available_space_b, get_drive_name};
28use crate::tui::Event;
29use crate::upnp::UpnpSupport;
30use crate::{
31 action::{Action, StatusActions},
32 config::Config,
33 mode::{InputMode, Scene},
34 node_stats::NodeStats,
35 style::{EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE},
36};
37use ant_bootstrap::InitialPeersConfig;
38use ant_node_manager::add_services::config::PortRange;
39use ant_node_manager::config::get_node_registry_path;
40use ant_service_management::{
41 NodeRegistryManager, NodeServiceData, ServiceStatus, control::ServiceController,
42};
43use color_eyre::eyre::{Ok, OptionExt, Result};
44use crossterm::event::KeyEvent;
45use ratatui::text::Span;
46use ratatui::{prelude::*, widgets::*};
47use std::fmt;
48use std::{
49 path::PathBuf,
50 time::{Duration, Instant},
51 vec,
52};
53use throbber_widgets_tui::{self, Throbber, ThrobberState};
54use tokio::sync::mpsc::UnboundedSender;
55
56pub const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5);
57pub const NODE_REGISTRY_UPDATE_INTERVAL: Duration = Duration::from_secs(180); const MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION: usize = 3;
60
61const NODE_WIDTH: usize = 10;
63const VERSION_WIDTH: usize = 7;
64const ATTOS_WIDTH: usize = 5;
65const MEMORY_WIDTH: usize = 7;
66const MBPS_WIDTH: usize = 13;
67const RECORDS_WIDTH: usize = 4;
68const PEERS_WIDTH: usize = 5;
69const CONNS_WIDTH: usize = 5;
70const MODE_WIDTH: usize = 7;
71const STATUS_WIDTH: usize = 8;
72const FAILURE_WIDTH: usize = 64;
73const SPINNER_WIDTH: usize = 1;
74
75#[derive(Clone)]
76pub struct Status<'a> {
77 active: bool,
79 action_sender: Option<UnboundedSender<Action>>,
80 config: Config,
81 is_nat_status_determined: bool,
83 error_while_running_nat_detection: usize,
84 nat_detection_in_progress: bool,
86 node_stats: NodeStats,
88 node_stats_last_update: Instant,
89 node_services: Vec<NodeServiceData>,
91 items: Option<StatefulTable<NodeItem<'a>>>,
92 network_id: Option<u8>,
94 node_management: NodeManagement,
96 nodes_to_start: usize,
98 rewards_address: String,
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 storage_mountpoint: PathBuf,
115 available_disk_space_gb: usize,
116 error_popup: Option<ErrorPopup>,
117 node_registry_last_update: Instant,
119}
120
121pub struct StatusConfig {
122 pub allocated_disk_space: usize,
123 pub antnode_path: Option<PathBuf>,
124 pub connection_mode: ConnectionMode,
125 pub upnp_support: UpnpSupport,
126 pub data_dir_path: PathBuf,
127 pub network_id: Option<u8>,
128 pub init_peers_config: InitialPeersConfig,
129 pub port_from: Option<u32>,
130 pub port_to: Option<u32>,
131 pub storage_mountpoint: PathBuf,
132 pub rewards_address: String,
133}
134
135impl Status<'_> {
136 pub async fn new(config: StatusConfig) -> Result<Self> {
137 let node_registry = NodeRegistryManager::load(&get_node_registry_path()?).await?;
138 let mut status = Self {
139 init_peers_config: config.init_peers_config,
140 action_sender: Default::default(),
141 config: Default::default(),
142 active: true,
143 is_nat_status_determined: false,
144 error_while_running_nat_detection: 0,
145 nat_detection_in_progress: false,
146 network_id: config.network_id,
147 node_stats: NodeStats::default(),
148 node_stats_last_update: Instant::now(),
149 node_services: Default::default(),
150 node_management: NodeManagement::new(node_registry.clone())?,
151 items: None,
152 nodes_to_start: config.allocated_disk_space,
153 rewards_address: config.rewards_address,
154 antnode_path: config.antnode_path,
155 data_dir_path: config.data_dir_path,
156 connection_mode: config.connection_mode,
157 upnp_support: config.upnp_support,
158 port_from: config.port_from,
159 port_to: config.port_to,
160 error_popup: None,
161 storage_mountpoint: config.storage_mountpoint.clone(),
162 available_disk_space_gb: (get_available_space_b(&config.storage_mountpoint)? / GB)
163 as usize,
164 node_registry_last_update: Instant::now(),
165 };
166
167 let now = Instant::now();
169 debug!("Refreshing node registry states on startup");
170
171 ant_node_manager::refresh_node_registry(
172 node_registry.clone(),
173 &ServiceController {},
174 false,
175 true,
176 ant_node_manager::VerbosityLevel::Minimal,
177 )
178 .await?;
179 node_registry.save().await?;
180 debug!("Node registry states refreshed in {:?}", now.elapsed());
181 status.update_node_state(
182 node_registry.get_node_service_data().await,
183 node_registry.nat_status.read().await.is_some(),
184 )?;
185
186 Ok(status)
187 }
188
189 fn set_lock(&mut self, service_name: &str, locked: bool) {
190 if let Some(ref mut items) = self.items {
191 for item in &mut items.items {
192 if item.name == *service_name {
193 item.locked = locked;
194 }
195 }
196 }
197 }
198
199 fn _lock_service(&mut self, service_name: &str) {
201 self.set_lock(service_name, true);
202 }
203
204 fn unlock_service(&mut self, service_name: &str) {
205 self.set_lock(service_name, false);
206 }
207
208 fn update_item(&mut self, service_name: String, status: NodeStatus) -> Result<()> {
215 if let Some(ref mut items) = self.items {
216 for item in &mut items.items {
217 if item.name == service_name {
218 item.status = status;
219 }
220 }
221 }
222 Ok(())
223 }
224
225 fn update_node_items(&mut self, new_status: Option<NodeStatus>) -> Result<()> {
226 if let Some(ref mut items) = self.items {
228 for node_item in self.node_services.iter() {
229 if let Some(item) = items
231 .items
232 .iter_mut()
233 .find(|i| i.name == node_item.service_name)
234 {
235 if let Some(status) = new_status {
236 item.status = status;
237 } else if item.status == NodeStatus::Updating
238 || item.status == NodeStatus::Starting
239 {
240 item.spinner_state.calc_next();
241 } else if new_status != Some(NodeStatus::Updating) {
242 item.status = match node_item.status {
244 ServiceStatus::Running => {
245 item.spinner_state.calc_next();
246 NodeStatus::Running
247 }
248 ServiceStatus::Stopped => NodeStatus::Stopped,
249 ServiceStatus::Added => NodeStatus::Added,
250 ServiceStatus::Removed => NodeStatus::Removed,
251 };
252 }
253
254 item.version = node_item.version.to_string();
255 item.peers = match node_item.connected_peers {
256 Some(ref peers) => peers.len(),
257 None => 0,
258 };
259
260 if let Some(stats) = self
262 .node_stats
263 .individual_stats
264 .iter()
265 .find(|s| s.service_name == node_item.service_name)
266 {
267 item.attos = stats.rewards_wallet_balance;
268 item.memory = stats.memory_usage_mb;
269 item.mbps = format!(
270 "↓{:0>5.0} ↑{:0>5.0}",
271 (stats.bandwidth_inbound_rate * 8) as f64 / 1_000_000.0,
272 (stats.bandwidth_outbound_rate * 8) as f64 / 1_000_000.0,
273 );
274 item.records = stats.max_records;
275 item.connections = stats.connections;
276 }
277 } else {
278 let new_item = NodeItem {
280 name: node_item.service_name.clone(),
281 version: node_item.version.to_string(),
282 attos: 0,
283 memory: 0,
284 mbps: "-".to_string(),
285 records: 0,
286 peers: 0,
287 connections: 0,
288 mode: NodeConnectionMode::from(node_item),
289 locked: false,
290 status: NodeStatus::Added, failure: node_item.get_critical_failure(),
292 spinner: Throbber::default(),
293 spinner_state: ThrobberState::default(),
294 };
295 items.items.push(new_item);
296 }
297 }
298 } else {
299 let node_items: Vec<NodeItem> = self
301 .node_services
302 .iter()
303 .filter_map(|node_item| {
304 if node_item.status == ServiceStatus::Removed {
305 return None;
306 }
307 let status = match node_item.status {
309 ServiceStatus::Running => NodeStatus::Running,
310 ServiceStatus::Stopped => NodeStatus::Stopped,
311 ServiceStatus::Added => NodeStatus::Added,
312 ServiceStatus::Removed => NodeStatus::Removed,
313 };
314
315 Some(NodeItem {
317 name: node_item.service_name.clone().to_string(),
318 version: node_item.version.to_string(),
319 attos: 0,
320 memory: 0,
321 mbps: "-".to_string(),
322 records: 0,
323 peers: 0,
324 connections: 0,
325 locked: false,
326 mode: NodeConnectionMode::from(node_item),
327 status,
328 failure: node_item.get_critical_failure(),
329 spinner: Throbber::default(),
330 spinner_state: ThrobberState::default(),
331 })
332 })
333 .collect();
334 self.items = Some(StatefulTable::with_items(node_items));
335 }
336 Ok(())
337 }
338
339 fn clear_node_items(&mut self) {
340 debug!("Cleaning items on Status page");
341 if let Some(items) = self.items.as_mut() {
342 items.items.clear();
343 debug!("Cleared the items on status page");
344 }
345 }
346
347 fn try_update_node_stats(&mut self, force_update: bool) -> Result<()> {
350 if self.node_stats_last_update.elapsed() > NODE_STAT_UPDATE_INTERVAL || force_update {
351 self.node_stats_last_update = Instant::now();
352
353 NodeStats::fetch_all_node_stats(&self.node_services, self.get_actions_sender()?);
354 }
355 Ok(())
356 }
357
358 fn try_update_node_registry(&mut self, force_update: bool) -> Result<()> {
362 if self.node_registry_last_update.elapsed() > NODE_REGISTRY_UPDATE_INTERVAL || force_update
363 {
364 self.node_registry_last_update = Instant::now();
365 let action_sender = self.get_actions_sender()?;
366
367 tokio::spawn(async move {
368 debug!("Starting periodic node registry refresh");
369
370 let node_registry =
373 match NodeRegistryManager::load(&get_node_registry_path().unwrap()).await {
374 core::result::Result::Ok(registry) => registry,
375 Err(err) => {
376 error!("Failed to load node registry for periodic refresh: {err:?}");
377 return;
378 }
379 };
380
381 if let Err(err) = ant_node_manager::refresh_node_registry(
382 node_registry.clone(),
383 &ServiceController {},
384 false, true, ant_node_manager::VerbosityLevel::Minimal,
387 )
388 .await
389 {
390 error!("Failed to refresh node registry: {err:?}");
391 return;
392 }
393
394 if let Err(err) = node_registry.save().await {
395 error!("Failed to save node registry after periodic refresh: {err:?}");
396 return;
397 }
398
399 debug!("Node registry refreshed and saved successfully");
400 let _ =
401 action_sender.send(Action::StatusActions(StatusActions::RegistryRefreshed {
402 all_nodes_data: node_registry.get_node_service_data().await,
403 is_nat_status_determined: node_registry.nat_status.read().await.is_some(),
404 }));
405 });
406 }
407 Ok(())
408 }
409
410 fn get_actions_sender(&self) -> Result<UnboundedSender<Action>> {
411 self.action_sender
412 .clone()
413 .ok_or_eyre("Action sender not registered")
414 }
415
416 fn update_node_state(
417 &mut self,
418 all_nodes_data: Vec<NodeServiceData>,
419 is_nat_status_determined: bool,
420 ) -> Result<()> {
421 self.is_nat_status_determined = is_nat_status_determined;
422
423 self.node_services = all_nodes_data
424 .into_iter()
425 .filter(|node| node.status != ServiceStatus::Removed)
426 .collect();
427
428 info!(
429 "Updated state from the data passed from NodeRegistryManager. Maintaining {:?} nodes.",
430 self.node_services.len()
431 );
432
433 Ok(())
434 }
435
436 fn should_we_run_nat_detection(&self) -> bool {
438 self.connection_mode == ConnectionMode::Automatic
439 && !self.is_nat_status_determined
440 && self.error_while_running_nat_detection < MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION
441 }
442
443 fn _nodes_starting(&self) -> bool {
444 if let Some(items) = &self.items {
445 items
446 .items
447 .iter()
448 .any(|item| item.status == NodeStatus::Starting)
449 } else {
450 false
451 }
452 }
453
454 fn get_running_nodes(&self) -> Vec<String> {
455 self.node_services
456 .iter()
457 .filter_map(|node| {
458 if node.status == ServiceStatus::Running {
459 Some(node.service_name.clone())
460 } else {
461 None
462 }
463 })
464 .collect()
465 }
466
467 fn get_service_names_and_peer_ids(&self) -> (Vec<String>, Vec<String>) {
468 let mut service_names = Vec::new();
469 let mut peers_ids = Vec::new();
470
471 for node in &self.node_services {
472 if let Some(peer_id) = &node.peer_id {
474 service_names.push(node.service_name.clone());
475 peers_ids.push(peer_id.to_string().clone());
476 }
477 }
478
479 (service_names, peers_ids)
480 }
481}
482
483impl Component for Status<'_> {
484 fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
485 self.action_sender = Some(tx);
486
487 self.try_update_node_stats(true)?;
489
490 Ok(())
491 }
492
493 fn register_config_handler(&mut self, config: Config) -> Result<()> {
494 self.config = config;
495 Ok(())
496 }
497
498 fn handle_events(&mut self, event: Option<Event>) -> Result<Vec<Action>> {
499 let r = match event {
500 Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
501 _ => vec![],
502 };
503 Ok(r)
504 }
505
506 fn update(&mut self, action: Action) -> Result<Option<Action>> {
507 match action {
508 Action::Tick => {
509 self.try_update_node_stats(false)?;
510 self.try_update_node_registry(false)?;
511 let _ = self.update_node_items(None);
512 }
513 Action::SwitchScene(scene) => match scene {
514 Scene::Status
515 | Scene::StatusRewardsAddressPopUp
516 | Scene::RemoveNodePopUp
517 | Scene::UpgradeLaunchpadPopUp => {
518 self.active = true;
519 return Ok(Some(Action::SwitchInputMode(InputMode::Navigation)));
521 }
522 Scene::ManageNodesPopUp { .. } => self.active = true,
523 _ => self.active = false,
524 },
525 Action::StoreNodesToStart(count) => {
526 self.nodes_to_start = count;
527 if self.nodes_to_start == 0 {
528 info!("Nodes to start set to 0. Sending command to stop all nodes.");
529 return Ok(Some(Action::StatusActions(StatusActions::StopNodes)));
530 } else {
531 info!("Nodes to start set to: {count}. Sending command to start nodes");
532 return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
533 }
534 }
535 Action::StoreRewardsAddress(rewards_address) => {
536 debug!("Storing rewards address: {rewards_address:?}");
537 let has_changed = self.rewards_address != rewards_address;
538 let we_have_nodes = !self.node_services.is_empty();
539
540 self.rewards_address = rewards_address;
541
542 if we_have_nodes && has_changed {
543 info!("Resetting antnode services because the Rewards Address was reset.");
544 let action_sender = self.get_actions_sender()?;
545 self.node_management
546 .send_task(NodeManagementTask::ResetNodes {
547 start_nodes_after_reset: false,
548 action_sender,
549 })?;
550 }
551 }
552 Action::StoreStorageDrive(ref drive_mountpoint, ref _drive_name) => {
553 info!("Resetting antnode services because the Storage Drive was changed.");
554 let action_sender = self.get_actions_sender()?;
555 self.node_management
556 .send_task(NodeManagementTask::ResetNodes {
557 start_nodes_after_reset: false,
558 action_sender,
559 })?;
560 self.data_dir_path =
561 get_launchpad_nodes_data_dir_path(&drive_mountpoint.to_path_buf(), false)?;
562 }
563 Action::StoreConnectionMode(connection_mode) => {
564 self.connection_mode = connection_mode;
565 info!("Resetting antnode services because the Connection Mode range was changed.");
566 let action_sender = self.get_actions_sender()?;
567 self.node_management
568 .send_task(NodeManagementTask::ResetNodes {
569 start_nodes_after_reset: false,
570 action_sender,
571 })?;
572 }
573 Action::StorePortRange(port_from, port_range) => {
574 self.port_from = Some(port_from);
575 self.port_to = Some(port_range);
576 info!("Resetting antnode services because the Port Range was changed.");
577 let action_sender = self.get_actions_sender()?;
578 self.node_management
579 .send_task(NodeManagementTask::ResetNodes {
580 start_nodes_after_reset: false,
581 action_sender,
582 })?;
583 }
584 Action::SetUpnpSupport(ref upnp_support) => {
585 debug!("Setting UPnP support: {upnp_support:?}");
586 self.upnp_support = upnp_support.clone();
587 }
588 Action::StatusActions(status_action) => match status_action {
589 StatusActions::NodesStatsObtained(stats) => {
590 self.node_stats = stats;
591 }
592 StatusActions::RegistryRefreshed {
593 all_nodes_data,
594 is_nat_status_determined,
595 } => {
596 debug!("Processing periodic registry refresh results");
597 self.update_node_state(all_nodes_data, is_nat_status_determined)?;
598 let _ = self.update_node_items(None);
599 }
600 StatusActions::StartNodesCompleted {
601 service_name,
602 all_nodes_data,
603 is_nat_status_determined,
604 } => {
605 if service_name == *NODES_ALL {
606 if let Some(items) = &self.items {
607 let items_clone = items.clone();
608 for item in &items_clone.items {
609 self.unlock_service(item.name.as_str());
610 self.update_item(item.name.clone(), NodeStatus::Running)?;
611 }
612 }
613 } else {
614 self.unlock_service(service_name.as_str());
615 self.update_item(service_name, NodeStatus::Running)?;
616 }
617 self.update_node_state(all_nodes_data, is_nat_status_determined)?;
618 }
619 StatusActions::StopNodesCompleted {
620 service_name,
621 all_nodes_data,
622 is_nat_status_determined,
623 } => {
624 self.unlock_service(service_name.as_str());
625 self.update_item(service_name, NodeStatus::Stopped)?;
626 self.update_node_state(all_nodes_data, is_nat_status_determined)?;
627 }
628 StatusActions::UpdateNodesCompleted {
629 all_nodes_data,
630 is_nat_status_determined,
631 } => {
632 if let Some(items) = &self.items {
633 let items_clone = items.clone();
634 for item in &items_clone.items {
635 self.unlock_service(item.name.as_str());
636 }
637 }
638 self.clear_node_items();
639 self.update_node_state(all_nodes_data, is_nat_status_determined)?;
640
641 let _ = self.update_node_items(None);
642 debug!("Update nodes completed");
643 }
644 StatusActions::ResetNodesCompleted {
645 trigger_start_node,
646 all_nodes_data,
647 is_nat_status_determined,
648 } => {
649 if let Some(items) = &self.items {
650 let items_clone = items.clone();
651 for item in &items_clone.items {
652 self.unlock_service(item.name.as_str());
653 }
654 }
655 self.update_node_state(all_nodes_data, is_nat_status_determined)?;
656
657 self.clear_node_items();
658
659 if trigger_start_node {
660 debug!("Reset nodes completed. Triggering start nodes.");
661 return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
662 }
663 debug!("Reset nodes completed");
664 }
665 StatusActions::AddNodesCompleted {
666 service_name,
667
668 all_nodes_data,
669 is_nat_status_determined,
670 } => {
671 self.unlock_service(service_name.as_str());
672 self.update_item(service_name.clone(), NodeStatus::Stopped)?;
673 self.update_node_state(all_nodes_data, is_nat_status_determined)?;
674
675 debug!("Adding {:?} completed", service_name.clone());
676 }
677 StatusActions::RemoveNodesCompleted {
678 service_name,
679 all_nodes_data,
680 is_nat_status_determined,
681 } => {
682 self.unlock_service(service_name.as_str());
683 self.update_item(service_name, NodeStatus::Removed)?;
684 self.update_node_state(all_nodes_data, is_nat_status_determined)?;
685
686 let _ = self.update_node_items(None);
687 debug!("Removing nodes completed");
688 }
689 StatusActions::SuccessfullyDetectedNatStatus => {
690 debug!(
691 "Successfully detected nat status, is_nat_status_determined set to true"
692 );
693 self.is_nat_status_determined = true;
694 self.nat_detection_in_progress = false;
695 }
696 StatusActions::NatDetectionStarted => {
697 debug!("NAT detection started");
698 self.nat_detection_in_progress = true;
699 }
700 StatusActions::ErrorWhileRunningNatDetection => {
701 self.error_while_running_nat_detection += 1;
702 self.nat_detection_in_progress = false;
703 debug!(
704 "Error while running nat detection. Error count: {}",
705 self.error_while_running_nat_detection
706 );
707 }
708 StatusActions::ErrorLoadingNodeRegistry { raw_error }
709 | StatusActions::ErrorGettingNodeRegistryPath { raw_error } => {
710 self.error_popup = Some(ErrorPopup::new(
711 "Error".to_string(),
712 "Error getting node registry path".to_string(),
713 raw_error,
714 ));
715 if let Some(error_popup) = &mut self.error_popup {
716 error_popup.show();
717 }
718 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
720 }
721 StatusActions::ErrorScalingUpNodes { raw_error } => {
722 self.error_popup = Some(ErrorPopup::new(
723 "Error".to_string(),
724 "Error adding new nodes".to_string(),
725 raw_error,
726 ));
727 if let Some(error_popup) = &mut self.error_popup {
728 error_popup.show();
729 }
730 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
732 }
733 StatusActions::ErrorStoppingNodes {
734 services,
735 raw_error,
736 } => {
737 for service_name in services {
738 self.unlock_service(service_name.as_str());
739 }
740 self.error_popup = Some(ErrorPopup::new(
741 "Error".to_string(),
742 "Error stopping nodes".to_string(),
743 raw_error,
744 ));
745 if let Some(error_popup) = &mut self.error_popup {
746 error_popup.show();
747 }
748 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
750 }
751 StatusActions::ErrorUpdatingNodes { raw_error } => {
752 if let Some(items) = &self.items {
753 let items_clone = items.clone();
754 for item in &items_clone.items {
755 self.unlock_service(item.name.as_str());
756 }
757 }
758 self.error_popup = Some(ErrorPopup::new(
759 "Error".to_string(),
760 "Error upgrading nodes".to_string(),
761 raw_error,
762 ));
763 if let Some(error_popup) = &mut self.error_popup {
764 error_popup.show();
765 }
766 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
768 }
769 StatusActions::ErrorResettingNodes { raw_error } => {
770 self.error_popup = Some(ErrorPopup::new(
771 "Error".to_string(),
772 "Error resetting nodes".to_string(),
773 raw_error,
774 ));
775 if let Some(error_popup) = &mut self.error_popup {
776 error_popup.show();
777 }
778 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
780 }
781 StatusActions::ErrorAddingNodes { raw_error } => {
782 self.error_popup = Some(ErrorPopup::new(
783 "Error".to_string(),
784 "Error adding node".to_string(),
785 raw_error,
786 ));
787 if let Some(error_popup) = &mut self.error_popup {
788 error_popup.show();
789 }
790 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
792 }
793 StatusActions::ErrorRemovingNodes {
794 services,
795 raw_error,
796 } => {
797 for service_name in services {
798 self.unlock_service(service_name.as_str());
799 }
800 self.error_popup = Some(ErrorPopup::new(
801 "Error".to_string(),
802 "Error removing node".to_string(),
803 raw_error,
804 ));
805 if let Some(error_popup) = &mut self.error_popup {
806 error_popup.show();
807 }
808 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
810 }
811 StatusActions::ErrorStartingNodes {
812 services,
813 raw_error,
814 } => {
815 for service_name in services {
816 self.unlock_service(service_name.as_str());
817 }
818 self.error_popup = Some(ErrorPopup::new(
819 "Error".to_string(),
820 "Error starting node. Please try again.".to_string(),
821 raw_error,
822 ));
823 if let Some(error_popup) = &mut self.error_popup {
824 error_popup.show();
825 }
826 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
828 }
829 StatusActions::TriggerManageNodes => {
830 let mut amount_of_nodes = 0;
831 if let Some(items) = &mut self.items {
832 amount_of_nodes = items.items.len();
833 }
834
835 return Ok(Some(Action::SwitchScene(Scene::ManageNodesPopUp {
836 amount_of_nodes,
837 })));
838 }
839 StatusActions::TriggerRemoveNode => {
840 if let Some(_node) = self.items.as_ref().and_then(|items| items.selected_item())
841 {
842 return Ok(Some(Action::SwitchScene(Scene::RemoveNodePopUp)));
843 } else {
844 debug!("No items to be removed");
845 return Ok(None);
846 }
847 }
848 StatusActions::PreviousTableItem => {
849 if let Some(items) = &mut self.items {
850 items.previous();
851 }
852 }
853 StatusActions::NextTableItem => {
854 if let Some(items) = &mut self.items {
855 items.next();
856 }
857 }
858 StatusActions::StartStopNode => {
859 debug!("Start/Stop node");
860
861 if let Some(node) = self.items.as_ref().and_then(|items| items.selected_item())
863 {
864 let node_index = self
865 .items
866 .as_ref()
867 .unwrap()
868 .items
869 .iter()
870 .position(|item| item.name == node.name)
871 .unwrap();
872 let action_sender = self.get_actions_sender()?;
873 let node = &mut self.items.as_mut().unwrap().items[node_index];
874
875 if node.status == NodeStatus::Removed {
876 debug!("Node is removed. Cannot be started.");
877 return Ok(None);
878 }
879
880 if node.locked {
881 debug!("Node still performing operation");
882 return Ok(None);
883 }
884 node.locked = true; let service_name = vec![node.name.clone()];
887
888 match node.status {
889 NodeStatus::Stopped | NodeStatus::Added => {
890 debug!("Starting Node {:?}", node.name);
891 self.node_management
892 .send_task(NodeManagementTask::StartNode {
893 services: service_name,
894 action_sender,
895 })?;
896 node.status = NodeStatus::Starting;
897 }
898 NodeStatus::Running => {
899 debug!("Stopping Node {:?}", node.name);
900 self.node_management
901 .send_task(NodeManagementTask::StopNodes {
902 services: service_name,
903 action_sender,
904 })?;
905 }
906 _ => {
907 debug!("Cannot Start/Stop node. Node status is {:?}", node.status);
908 }
909 }
910 } else {
911 debug!("Got action to Start/Stop node but no node was selected.");
912 return Ok(None);
913 }
914 }
915 StatusActions::StartNodes => {
916 debug!("Got action to start nodes");
917
918 if self.rewards_address.is_empty() {
919 info!("Rewards address is not set. Ask for input.");
920 return Ok(Some(Action::StatusActions(
921 StatusActions::TriggerRewardsAddress,
922 )));
923 }
924
925 if self.nodes_to_start == 0 {
926 info!("Nodes to start not set. Ask for input.");
927 return Ok(Some(Action::StatusActions(
928 StatusActions::TriggerManageNodes,
929 )));
930 }
931
932 if let Some(ref mut items) = self.items {
934 for item in &mut items.items {
935 if item.status == NodeStatus::Added
936 || item.status == NodeStatus::Stopped
937 {
938 item.status = NodeStatus::Starting;
939 item.locked = true;
940 }
941 }
942 }
943
944 let port_range = PortRange::Range(
945 self.port_from.unwrap_or(PORT_MIN) as u16,
946 self.port_to.unwrap_or(PORT_MAX) as u16,
947 );
948
949 let action_sender = self.get_actions_sender()?;
950
951 let maintain_nodes_args = MaintainNodesArgs {
952 action_sender: action_sender.clone(),
953 antnode_path: self.antnode_path.clone(),
954 connection_mode: self.connection_mode,
955 count: self.nodes_to_start as u16,
956 data_dir_path: Some(self.data_dir_path.clone()),
957 network_id: self.network_id,
958 owner: self.rewards_address.clone(),
959 init_peers_config: self.init_peers_config.clone(),
960 port_range: Some(port_range),
961 rewards_address: self.rewards_address.clone(),
962 run_nat_detection: self.should_we_run_nat_detection(),
963 };
964
965 if maintain_nodes_args.run_nat_detection {
967 self.nat_detection_in_progress = true;
968 }
969
970 debug!("Calling maintain_n_running_nodes");
971
972 self.node_management
973 .send_task(NodeManagementTask::MaintainNodes {
974 args: maintain_nodes_args,
975 })?;
976 }
977 StatusActions::StopNodes => {
978 debug!("Got action to stop nodes");
979
980 let running_nodes = self.get_running_nodes();
981 let action_sender = self.get_actions_sender()?;
982 info!("Stopping node service: {running_nodes:?}");
983
984 self.node_management
985 .send_task(NodeManagementTask::StopNodes {
986 services: running_nodes,
987 action_sender,
988 })?;
989 }
990 StatusActions::AddNode => {
991 debug!("Got action to Add node");
992
993 if (GB_PER_NODE as usize) > self.available_disk_space_gb {
995 self.error_popup = Some(ErrorPopup::new(
996 "Cannot Add Node".to_string(),
997 format!("\nEach Node requires {GB_PER_NODE}GB of available space."),
998 format!(
999 "{} has only {}GB remaining.\n\nYou can free up some space or change to different drive in the options.",
1000 get_drive_name(&self.storage_mountpoint)?,
1001 self.available_disk_space_gb
1002 ),
1003 ));
1004 if let Some(error_popup) = &mut self.error_popup {
1005 error_popup.show();
1006 }
1007 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
1009 }
1010
1011 let amount_of_nodes = if let Some(ref items) = self.items {
1013 items.items.len()
1014 } else {
1015 0
1016 };
1017
1018 if amount_of_nodes + 1 > MAX_NODE_COUNT {
1019 self.error_popup = Some(ErrorPopup::new(
1020 "Cannot Add Node".to_string(),
1021 format!(
1022 "There are not enough ports available in your\ncustom port range to start another node ({MAX_NODE_COUNT})."
1023 ),
1024 "\nVisit autonomi.com/support/port-error for help".to_string(),
1025 ));
1026 if let Some(error_popup) = &mut self.error_popup {
1027 error_popup.show();
1028 }
1029 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
1031 }
1032
1033 if self.rewards_address.is_empty() {
1034 info!("Rewards address is not set. Ask for input.");
1035 return Ok(Some(Action::StatusActions(
1036 StatusActions::TriggerRewardsAddress,
1037 )));
1038 }
1039
1040 if self.nodes_to_start == 0 {
1041 info!("Nodes to start not set. Ask for input.");
1042 return Ok(Some(Action::StatusActions(
1043 StatusActions::TriggerManageNodes,
1044 )));
1045 }
1046
1047 let port_range = PortRange::Range(
1048 self.port_from.unwrap_or(PORT_MIN) as u16,
1049 self.port_to.unwrap_or(PORT_MAX) as u16,
1050 );
1051
1052 let action_sender = self.get_actions_sender()?;
1053
1054 let add_node_args = MaintainNodesArgs {
1055 action_sender: action_sender.clone(),
1056 antnode_path: self.antnode_path.clone(),
1057 connection_mode: self.connection_mode,
1058 count: 1,
1059 data_dir_path: Some(self.data_dir_path.clone()),
1060 network_id: self.network_id,
1061 owner: self.rewards_address.clone(),
1062 init_peers_config: self.init_peers_config.clone(),
1063 port_range: Some(port_range),
1064 rewards_address: self.rewards_address.clone(),
1065 run_nat_detection: self.should_we_run_nat_detection(),
1066 };
1067
1068 if add_node_args.run_nat_detection {
1070 self.nat_detection_in_progress = true;
1071 }
1072
1073 self.node_management
1074 .send_task(NodeManagementTask::AddNode {
1075 args: add_node_args,
1076 })?;
1077 }
1078 StatusActions::RemoveNodes => {
1079 debug!("Got action to remove node");
1080 if self
1082 .items
1083 .as_ref()
1084 .and_then(|items| items.selected_item())
1085 .is_none()
1086 {
1087 debug!("Got action to Start/Stop node but no node was selected.");
1088 return Ok(None);
1089 }
1090
1091 let node_index =
1092 self.items
1093 .as_ref()
1094 .and_then(|items| {
1095 items.items.iter().position(|item| {
1096 item.name == items.selected_item().unwrap().name
1097 })
1098 })
1099 .unwrap();
1100
1101 let action_sender = self.get_actions_sender()?;
1102
1103 let node = &mut self.items.as_mut().unwrap().items[node_index];
1104
1105 if node.locked {
1106 debug!("Node still performing operation");
1107 return Ok(None);
1108 } else {
1109 node.locked = true;
1111 }
1112
1113 let service_name = vec![node.name.clone()];
1114
1115 self.node_management
1117 .send_task(NodeManagementTask::RemoveNodes {
1118 services: service_name,
1119 action_sender,
1120 })?;
1121 }
1122 StatusActions::TriggerRewardsAddress => {
1123 if self.rewards_address.is_empty() {
1124 return Ok(Some(Action::SwitchScene(Scene::StatusRewardsAddressPopUp)));
1125 } else {
1126 return Ok(None);
1127 }
1128 }
1129 StatusActions::TriggerNodeLogs => {
1130 if let Some(node) = self.items.as_ref().and_then(|items| items.selected_item())
1131 {
1132 debug!("Got action to open node logs {:?}", node.name);
1133 open_logs(Some(node.name.clone()))?;
1134 } else {
1135 debug!("Got action to open node logs but no node was selected.");
1136 }
1137 }
1138 },
1139 Action::OptionsActions(OptionsActions::UpdateNodes) => {
1140 debug!("Got action to Update Nodes");
1141 let action_sender = self.get_actions_sender()?;
1142 info!("Got action to update nodes");
1143 let _ = self.update_node_items(Some(NodeStatus::Updating));
1144 let (service_names, peer_ids) = self.get_service_names_and_peer_ids();
1145
1146 let upgrade_nodes_args = UpgradeNodesArgs {
1147 action_sender,
1148 connection_timeout_s: 5,
1149 do_not_start: true,
1150 custom_bin_path: self.antnode_path.clone(),
1151 force: false,
1152 fixed_interval: Some(FIXED_INTERVAL),
1153 peer_ids,
1154 provided_env_variables: None,
1155 service_names,
1156 url: None,
1157 version: None,
1158 };
1159 self.node_management
1160 .send_task(NodeManagementTask::UpgradeNodes {
1161 args: upgrade_nodes_args,
1162 })?;
1163 }
1164 Action::OptionsActions(OptionsActions::ResetNodes) => {
1165 debug!("Got action to reset nodes");
1166 let action_sender = self.get_actions_sender()?;
1167 info!("Got action to reset nodes");
1168 self.node_management
1169 .send_task(NodeManagementTask::ResetNodes {
1170 start_nodes_after_reset: false,
1171 action_sender,
1172 })?;
1173 }
1174 Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, _drive_name)) => {
1175 self.storage_mountpoint.clone_from(&mountpoint);
1176 self.available_disk_space_gb = (get_available_space_b(&mountpoint)? / GB) as usize;
1177 }
1178 _ => {}
1179 }
1180 Ok(None)
1181 }
1182
1183 fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
1184 if !self.active {
1185 return Ok(());
1186 }
1187
1188 let layout = Layout::new(
1189 Direction::Vertical,
1190 [
1191 Constraint::Length(1),
1193 Constraint::Max(6),
1195 Constraint::Min(3),
1197 Constraint::Length(3),
1199 ],
1200 )
1201 .split(area);
1202
1203 let header = Header::new();
1206 f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Status);
1207
1208 let combined_block = Block::default()
1214 .title(" Device Status ")
1215 .bold()
1216 .title_style(Style::default().fg(GHOST_WHITE))
1217 .borders(Borders::ALL)
1218 .padding(Padding::horizontal(1))
1219 .style(Style::default().fg(VERY_LIGHT_AZURE));
1220
1221 f.render_widget(combined_block.clone(), layout[1]);
1222
1223 let storage_allocated_row = Row::new(vec![
1224 Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE),
1225 Cell::new(format!("{} GB", (self.nodes_to_start as u64) * GB_PER_NODE)).fg(GHOST_WHITE),
1226 ]);
1227 let memory_use_val = if self.node_stats.total_memory_usage_mb as f64 / 1024_f64 > 1.0 {
1228 format!(
1229 "{:.2} GB",
1230 self.node_stats.total_memory_usage_mb as f64 / 1024_f64
1231 )
1232 } else {
1233 format!("{} MB", self.node_stats.total_memory_usage_mb)
1234 };
1235
1236 let memory_use_row = Row::new(vec![
1237 Cell::new("Memory Use".to_string()).fg(GHOST_WHITE),
1238 Cell::new(memory_use_val).fg(GHOST_WHITE),
1239 ]);
1240
1241 let connection_mode_string = match self.connection_mode {
1242 ConnectionMode::HomeNetwork => "Home Network".to_string(),
1243 ConnectionMode::UPnP => "UPnP".to_string(),
1244 ConnectionMode::CustomPorts => format!(
1245 "Custom Ports {}-{}",
1246 self.port_from.unwrap_or(PORT_MIN),
1247 self.port_to.unwrap_or(PORT_MIN + PORT_ALLOCATION)
1248 ),
1249 ConnectionMode::Automatic => "Automatic".to_string(),
1250 };
1251
1252 let mut connection_mode_line = vec![Span::styled(
1253 connection_mode_string,
1254 Style::default().fg(GHOST_WHITE),
1255 )];
1256
1257 if matches!(
1258 self.connection_mode,
1259 ConnectionMode::Automatic | ConnectionMode::UPnP
1260 ) {
1261 connection_mode_line.push(Span::styled(" (", Style::default().fg(GHOST_WHITE)));
1262
1263 if self.connection_mode == ConnectionMode::Automatic {
1264 connection_mode_line.push(Span::styled("UPnP: ", Style::default().fg(GHOST_WHITE)));
1265 }
1266
1267 let span = match self.upnp_support {
1268 UpnpSupport::Supported => {
1269 Span::styled("supported", Style::default().fg(EUCALYPTUS))
1270 }
1271 UpnpSupport::Unsupported => {
1272 Span::styled("disabled / unsupported", Style::default().fg(SIZZLING_RED))
1273 }
1274 UpnpSupport::Loading => {
1275 Span::styled("loading..", Style::default().fg(LIGHT_PERIWINKLE))
1276 }
1277 UpnpSupport::Unknown => {
1278 Span::styled("unknown", Style::default().fg(LIGHT_PERIWINKLE))
1279 }
1280 };
1281
1282 connection_mode_line.push(span);
1283
1284 connection_mode_line.push(Span::styled(")", Style::default().fg(GHOST_WHITE)));
1285 }
1286
1287 let connection_mode_row = Row::new(vec![
1288 Cell::new("Connection".to_string()).fg(GHOST_WHITE),
1289 Cell::new(Line::from(connection_mode_line)),
1290 ]);
1291
1292 let stats_rows = vec![storage_allocated_row, memory_use_row, connection_mode_row];
1293 let stats_width = [Constraint::Length(5)];
1294 let column_constraints = [Constraint::Length(23), Constraint::Fill(1)];
1295 let stats_table = Table::new(stats_rows, stats_width).widths(column_constraints);
1296
1297 let wallet_not_set = if self.rewards_address.is_empty() {
1298 vec![
1299 Span::styled("Press ".to_string(), Style::default().fg(VIVID_SKY_BLUE)),
1300 Span::styled("[Ctrl+B] ".to_string(), Style::default().fg(GHOST_WHITE)),
1301 Span::styled(
1302 "to add your ".to_string(),
1303 Style::default().fg(VIVID_SKY_BLUE),
1304 ),
1305 Span::styled(
1306 "Wallet Address".to_string(),
1307 Style::default().fg(VIVID_SKY_BLUE).bold(),
1308 ),
1309 ]
1310 } else {
1311 vec![]
1312 };
1313
1314 let total_attos_earned_and_wallet_row = Row::new(vec![
1315 Cell::new("Attos Earned".to_string()).fg(VIVID_SKY_BLUE),
1316 Cell::new(format!(
1317 "{:?}",
1318 self.node_stats.total_rewards_wallet_balance
1319 ))
1320 .fg(VIVID_SKY_BLUE)
1321 .bold(),
1322 Cell::new(Line::from(wallet_not_set).alignment(Alignment::Right)),
1323 ]);
1324
1325 let attos_wallet_rows = vec![total_attos_earned_and_wallet_row];
1326 let attos_wallet_width = [Constraint::Length(5)];
1327 let column_constraints = [
1328 Constraint::Length(23),
1329 Constraint::Fill(1),
1330 Constraint::Length(if self.rewards_address.is_empty() {
1331 41 } else {
1333 0
1334 }),
1335 ];
1336 let attos_wallet_table =
1337 Table::new(attos_wallet_rows, attos_wallet_width).widths(column_constraints);
1338
1339 let inner_area = combined_block.inner(layout[1]);
1340 let device_layout = Layout::new(
1341 Direction::Vertical,
1342 vec![Constraint::Length(5), Constraint::Length(1)],
1343 )
1344 .split(inner_area);
1345
1346 f.render_widget(stats_table, device_layout[0]);
1348 f.render_widget(attos_wallet_table, device_layout[1]);
1349
1350 if let Some(ref items) = self.items {
1354 if items.items.is_empty() || self.rewards_address.is_empty() {
1355 let line1 = Line::from(vec![
1356 Span::styled("Press ", Style::default().fg(LIGHT_PERIWINKLE)),
1357 Span::styled("[+] ", Style::default().fg(GHOST_WHITE).bold()),
1358 Span::styled("to Add and ", Style::default().fg(LIGHT_PERIWINKLE)),
1359 Span::styled(
1360 "Start your first node ",
1361 Style::default().fg(GHOST_WHITE).bold(),
1362 ),
1363 Span::styled("on this device", Style::default().fg(LIGHT_PERIWINKLE)),
1364 ]);
1365
1366 let line2 = Line::from(vec![Span::styled(
1367 format!(
1368 "Each node will use {GB_PER_NODE}GB of storage and a small amount of memory, \
1369 CPU, and Network bandwidth. Most computers can run many nodes at once, \
1370 but we recommend you add them gradually"
1371 ),
1372 Style::default().fg(LIGHT_PERIWINKLE),
1373 )]);
1374
1375 f.render_widget(
1376 Paragraph::new(vec![Line::raw(""), line1, Line::raw(""), line2])
1377 .wrap(Wrap { trim: false })
1378 .fg(LIGHT_PERIWINKLE)
1379 .block(
1380 Block::default()
1381 .title(Line::from(vec![
1382 Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
1383 Span::styled(" (0) ", Style::default().fg(LIGHT_PERIWINKLE)),
1384 ]))
1385 .title_style(Style::default().fg(LIGHT_PERIWINKLE))
1386 .borders(Borders::ALL)
1387 .border_style(style::Style::default().fg(EUCALYPTUS))
1388 .padding(Padding::horizontal(1)),
1389 ),
1390 layout[2],
1391 );
1392 } else {
1393 let block_nodes = Block::default()
1395 .title(Line::from(vec![
1396 Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
1397 Span::styled(
1398 format!(
1399 " ({}) ",
1400 if let Some(ref items) = self.items {
1401 items.items.len()
1402 } else {
1403 0
1404 }
1405 ),
1406 Style::default().fg(LIGHT_PERIWINKLE),
1407 ),
1408 ]))
1409 .padding(Padding::new(1, 1, 0, 0))
1410 .title_style(Style::default().fg(GHOST_WHITE))
1411 .borders(Borders::ALL)
1412 .border_style(Style::default().fg(EUCALYPTUS));
1413
1414 let inner_area = block_nodes.inner(layout[2]);
1416
1417 let node_widths = [
1419 Constraint::Min(NODE_WIDTH as u16),
1420 Constraint::Min(VERSION_WIDTH as u16),
1421 Constraint::Min(ATTOS_WIDTH as u16),
1422 Constraint::Min(MEMORY_WIDTH as u16),
1423 Constraint::Min(MBPS_WIDTH as u16),
1424 Constraint::Min(RECORDS_WIDTH as u16),
1425 Constraint::Min(PEERS_WIDTH as u16),
1426 Constraint::Min(CONNS_WIDTH as u16),
1427 Constraint::Min(MODE_WIDTH as u16),
1428 Constraint::Min(STATUS_WIDTH as u16),
1429 Constraint::Fill(FAILURE_WIDTH as u16),
1430 Constraint::Max(SPINNER_WIDTH as u16),
1431 ];
1432
1433 let header_row = Row::new(vec![
1435 Cell::new("Node").fg(COOL_GREY),
1436 Cell::new("Version").fg(COOL_GREY),
1437 Cell::new("Attos").fg(COOL_GREY),
1438 Cell::new("Memory").fg(COOL_GREY),
1439 Cell::new(
1440 format!("{}{}", " ".repeat(MBPS_WIDTH - "Mbps".len()), "Mbps")
1441 .fg(COOL_GREY),
1442 ),
1443 Cell::new("Recs").fg(COOL_GREY),
1444 Cell::new("Peers").fg(COOL_GREY),
1445 Cell::new("Conns").fg(COOL_GREY),
1446 Cell::new("Mode").fg(COOL_GREY),
1447 Cell::new("Status").fg(COOL_GREY),
1448 Cell::new("Failure").fg(COOL_GREY),
1449 Cell::new(" ").fg(COOL_GREY), ])
1451 .style(Style::default().add_modifier(Modifier::BOLD));
1452
1453 let mut items: Vec<Row> = Vec::new();
1454 if let Some(ref mut items_table) = self.items {
1455 for (i, node_item) in items_table.items.iter_mut().enumerate() {
1456 let is_selected = items_table.state.selected() == Some(i);
1457 items.push(node_item.render_as_row(i, layout[2], f, is_selected));
1458 }
1459 }
1460
1461 let table = Table::new(items, node_widths)
1463 .header(header_row)
1464 .column_spacing(1)
1465 .row_highlight_style(Style::default().bg(INDIGO))
1466 .highlight_spacing(HighlightSpacing::Always);
1467
1468 f.render_widget(table, inner_area);
1469
1470 f.render_widget(block_nodes, layout[2]);
1471 }
1472 }
1473
1474 let selected = self
1477 .items
1478 .as_ref()
1479 .and_then(|items| items.selected_item())
1480 .is_some();
1481
1482 let footer = Footer::default();
1483 let footer_state = if let Some(ref items) = self.items {
1484 if !items.items.is_empty() || !self.rewards_address.is_empty() {
1485 if !self.get_running_nodes().is_empty() {
1486 if selected {
1487 &mut NodesToStart::RunningSelected
1488 } else {
1489 &mut NodesToStart::Running
1490 }
1491 } else if selected {
1492 &mut NodesToStart::NotRunningSelected
1493 } else {
1494 &mut NodesToStart::NotRunning
1495 }
1496 } else {
1497 &mut NodesToStart::NotRunning
1498 }
1499 } else {
1500 &mut NodesToStart::NotRunning
1501 };
1502 f.render_stateful_widget(footer, layout[3], footer_state);
1503
1504 if let Some(error_popup) = &self.error_popup
1508 && error_popup.is_visible()
1509 {
1510 error_popup.draw_error(f, area);
1511
1512 return Ok(());
1513 }
1514
1515 if self.nat_detection_in_progress {
1516 let popup_text = vec![
1517 Line::raw("NAT Detection Running..."),
1518 Line::raw(""),
1519 Line::raw(""),
1520 Line::raw("Please wait, performing NAT detection"),
1521 Line::raw("This may take a couple minutes."),
1522 ];
1523
1524 let popup_area = centered_rect_fixed(50, 12, area);
1525 clear_area(f, popup_area);
1526
1527 let popup_border = Paragraph::new("").block(
1528 Block::default()
1529 .borders(Borders::ALL)
1530 .title(" NAT Detection ")
1531 .bold()
1532 .title_style(Style::new().fg(VIVID_SKY_BLUE))
1533 .padding(Padding::uniform(2))
1534 .border_style(Style::new().fg(GHOST_WHITE)),
1535 );
1536
1537 let centred_area = Layout::new(
1538 Direction::Vertical,
1539 vec![
1540 Constraint::Length(2),
1542 Constraint::Min(5),
1544 Constraint::Length(1),
1546 ],
1547 )
1548 .split(popup_area);
1549 let text = Paragraph::new(popup_text)
1550 .block(Block::default().padding(Padding::horizontal(2)))
1551 .wrap(Wrap { trim: false })
1552 .alignment(Alignment::Center)
1553 .fg(EUCALYPTUS);
1554 f.render_widget(text, centred_area[1]);
1555
1556 f.render_widget(popup_border, popup_area);
1557 }
1558
1559 Ok(())
1560 }
1561
1562 fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
1563 debug!("Key received in Status: {:?}", key);
1564 if let Some(error_popup) = &mut self.error_popup
1565 && error_popup.is_visible()
1566 {
1567 error_popup.handle_input(key);
1568 return Ok(vec![Action::SwitchInputMode(InputMode::Navigation)]);
1569 }
1570 Ok(vec![])
1571 }
1572}
1573
1574#[allow(dead_code)]
1575#[derive(Default, Clone)]
1576struct StatefulTable<T> {
1577 state: TableState,
1578 items: Vec<T>,
1579 last_selected: Option<usize>,
1580}
1581
1582#[allow(dead_code)]
1583impl<T> StatefulTable<T> {
1584 fn with_items(items: Vec<T>) -> Self {
1585 StatefulTable {
1586 state: TableState::default(),
1587 items,
1588 last_selected: None,
1589 }
1590 }
1591
1592 fn next(&mut self) {
1593 let i = match self.state.selected() {
1594 Some(i) => {
1595 if !self.items.is_empty() {
1596 if i >= self.items.len() - 1 { 0 } else { i + 1 }
1597 } else {
1598 0
1599 }
1600 }
1601 None => self.last_selected.unwrap_or(0),
1602 };
1603 self.state.select(Some(i));
1604 self.last_selected = Some(i);
1605 }
1606
1607 fn previous(&mut self) {
1608 let i = match self.state.selected() {
1609 Some(i) => {
1610 if !self.items.is_empty() {
1611 if i == 0 { self.items.len() - 1 } else { i - 1 }
1612 } else {
1613 0
1614 }
1615 }
1616 None => self.last_selected.unwrap_or(0),
1617 };
1618 self.state.select(Some(i));
1619 self.last_selected = Some(i);
1620 }
1621
1622 fn selected_item(&self) -> Option<&T> {
1623 self.state
1624 .selected()
1625 .and_then(|index| self.items.get(index))
1626 }
1627}
1628
1629#[derive(Default, Debug, Copy, Clone, PartialEq)]
1630enum NodeStatus {
1631 #[default]
1632 Added,
1633 Running,
1634 Starting,
1635 Stopped,
1636 Removed,
1637 Updating,
1638}
1639
1640impl fmt::Display for NodeStatus {
1641 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1642 match *self {
1643 NodeStatus::Added => write!(f, "Added"),
1644 NodeStatus::Running => write!(f, "Running"),
1645 NodeStatus::Starting => write!(f, "Starting"),
1646 NodeStatus::Stopped => write!(f, "Stopped"),
1647 NodeStatus::Removed => write!(f, "Removed"),
1648 NodeStatus::Updating => write!(f, "Updating"),
1649 }
1650 }
1651}
1652
1653#[derive(Default, Debug, Clone)]
1654pub struct NodeItem<'a> {
1655 name: String,
1656 version: String,
1657 attos: usize,
1658 memory: usize,
1659 mbps: String,
1660 records: usize,
1661 peers: usize,
1662 connections: usize,
1663 locked: bool, mode: NodeConnectionMode,
1665 status: NodeStatus,
1666 failure: Option<(chrono::DateTime<chrono::Utc>, String)>,
1667 spinner: Throbber<'a>,
1668 spinner_state: ThrobberState,
1669}
1670
1671impl NodeItem<'_> {
1672 fn render_as_row(
1673 &mut self,
1674 index: usize,
1675 area: Rect,
1676 f: &mut Frame<'_>,
1677 is_selected: bool,
1678 ) -> Row<'_> {
1679 let mut row_style = if is_selected {
1680 Style::default().fg(GHOST_WHITE).bg(INDIGO)
1681 } else {
1682 Style::default().fg(GHOST_WHITE)
1683 };
1684 let mut spinner_state = self.spinner_state.clone();
1685 match self.status {
1686 NodeStatus::Running => {
1687 self.spinner = self
1688 .spinner
1689 .clone()
1690 .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1691 .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1692 .use_type(throbber_widgets_tui::WhichUse::Spin);
1693 row_style = if is_selected {
1694 Style::default().fg(EUCALYPTUS).bg(INDIGO)
1695 } else {
1696 Style::default().fg(EUCALYPTUS)
1697 };
1698 }
1699 NodeStatus::Starting => {
1700 self.spinner = self
1701 .spinner
1702 .clone()
1703 .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1704 .throbber_set(throbber_widgets_tui::BOX_DRAWING)
1705 .use_type(throbber_widgets_tui::WhichUse::Spin);
1706 }
1707 NodeStatus::Stopped => {
1708 self.spinner = self
1709 .spinner
1710 .clone()
1711 .throbber_style(
1712 Style::default()
1713 .fg(GHOST_WHITE)
1714 .add_modifier(Modifier::BOLD),
1715 )
1716 .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1717 .use_type(throbber_widgets_tui::WhichUse::Full);
1718 }
1719 NodeStatus::Updating => {
1720 self.spinner = self
1721 .spinner
1722 .clone()
1723 .throbber_style(
1724 Style::default()
1725 .fg(GHOST_WHITE)
1726 .add_modifier(Modifier::BOLD),
1727 )
1728 .throbber_set(throbber_widgets_tui::VERTICAL_BLOCK)
1729 .use_type(throbber_widgets_tui::WhichUse::Spin);
1730 }
1731 _ => {}
1732 };
1733
1734 let failure = self.failure.as_ref().map_or_else(
1735 || "-".to_string(),
1736 |(_dt, msg)| {
1737 if self.status == NodeStatus::Stopped {
1738 msg.clone()
1739 } else {
1740 "-".to_string()
1741 }
1742 },
1743 );
1744
1745 let row = vec![
1746 self.name.clone().to_string(),
1747 self.version.to_string(),
1748 format!(
1749 "{}{}",
1750 " ".repeat(ATTOS_WIDTH.saturating_sub(self.attos.to_string().len())),
1751 self.attos.to_string()
1752 ),
1753 format!(
1754 "{}{} MB",
1755 " ".repeat(MEMORY_WIDTH.saturating_sub(self.memory.to_string().len() + 4)),
1756 self.memory.to_string()
1757 ),
1758 format!(
1759 "{}{}",
1760 " ".repeat(MBPS_WIDTH.saturating_sub(self.mbps.to_string().len())),
1761 self.mbps.to_string()
1762 ),
1763 format!(
1764 "{}{}",
1765 " ".repeat(RECORDS_WIDTH.saturating_sub(self.records.to_string().len())),
1766 self.records.to_string()
1767 ),
1768 format!(
1769 "{}{}",
1770 " ".repeat(PEERS_WIDTH.saturating_sub(self.peers.to_string().len())),
1771 self.peers.to_string()
1772 ),
1773 format!(
1774 "{}{}",
1775 " ".repeat(CONNS_WIDTH.saturating_sub(self.connections.to_string().len())),
1776 self.connections.to_string()
1777 ),
1778 self.mode.to_string(),
1779 self.status.to_string(),
1780 failure,
1781 ];
1782 let throbber_area = Rect::new(area.width - 3, area.y + 2 + index as u16, 1, 1);
1783
1784 f.render_stateful_widget(self.spinner.clone(), throbber_area, &mut spinner_state);
1785
1786 Row::new(row).style(row_style)
1787 }
1788}