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