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