dashboard.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. use axum::extract::State;
  2. use axum::http::StatusCode;
  3. use axum::response::Html;
  4. use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
  5. use maud::html;
  6. use crate::web::handlers::AppState;
  7. use crate::web::templates::{format_sats_as_btc, layout};
  8. #[derive(Debug)]
  9. pub struct UsageMetrics {
  10. pub lightning_inflow_24h: u64,
  11. pub lightning_outflow_24h: u64,
  12. pub lightning_inflow_all_time: u64,
  13. pub lightning_outflow_all_time: u64,
  14. pub onchain_inflow_24h: u64,
  15. pub onchain_outflow_24h: u64,
  16. pub onchain_inflow_all_time: u64,
  17. pub onchain_outflow_all_time: u64,
  18. }
  19. /// Calculate usage metrics from payment history
  20. fn calculate_usage_metrics(payments: &[ldk_node::payment::PaymentDetails]) -> UsageMetrics {
  21. use std::time::{SystemTime, UNIX_EPOCH};
  22. let now = SystemTime::now()
  23. .duration_since(UNIX_EPOCH)
  24. .unwrap_or_default()
  25. .as_secs();
  26. let twenty_four_hours_ago = now.saturating_sub(24 * 60 * 60);
  27. let mut metrics = UsageMetrics {
  28. lightning_inflow_24h: 0,
  29. lightning_outflow_24h: 0,
  30. lightning_inflow_all_time: 0,
  31. lightning_outflow_all_time: 0,
  32. onchain_inflow_24h: 0,
  33. onchain_outflow_24h: 0,
  34. onchain_inflow_all_time: 0,
  35. onchain_outflow_all_time: 0,
  36. };
  37. for payment in payments {
  38. if payment.status != PaymentStatus::Succeeded {
  39. continue;
  40. }
  41. let amount_sats = payment.amount_msat.unwrap_or(0) / 1000;
  42. let is_recent = payment.latest_update_timestamp >= twenty_four_hours_ago;
  43. match &payment.kind {
  44. PaymentKind::Bolt11 { .. }
  45. | PaymentKind::Bolt12Offer { .. }
  46. | PaymentKind::Bolt12Refund { .. }
  47. | PaymentKind::Spontaneous { .. }
  48. | PaymentKind::Bolt11Jit { .. } => match payment.direction {
  49. PaymentDirection::Inbound => {
  50. metrics.lightning_inflow_all_time += amount_sats;
  51. if is_recent {
  52. metrics.lightning_inflow_24h += amount_sats;
  53. }
  54. }
  55. PaymentDirection::Outbound => {
  56. metrics.lightning_outflow_all_time += amount_sats;
  57. if is_recent {
  58. metrics.lightning_outflow_24h += amount_sats;
  59. }
  60. }
  61. },
  62. PaymentKind::Onchain { .. } => match payment.direction {
  63. PaymentDirection::Inbound => {
  64. metrics.onchain_inflow_all_time += amount_sats;
  65. if is_recent {
  66. metrics.onchain_inflow_24h += amount_sats;
  67. }
  68. }
  69. PaymentDirection::Outbound => {
  70. metrics.onchain_outflow_all_time += amount_sats;
  71. if is_recent {
  72. metrics.onchain_outflow_24h += amount_sats;
  73. }
  74. }
  75. },
  76. }
  77. }
  78. metrics
  79. }
  80. pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
  81. let node = &state.node.inner;
  82. let _node_id = node.node_id().to_string();
  83. let alias = node
  84. .node_alias()
  85. .map(|a| a.to_string())
  86. .unwrap_or_else(|| "No alias set".to_string());
  87. let listening_addresses: Vec<String> = state
  88. .node
  89. .inner
  90. .announcement_addresses()
  91. .as_ref()
  92. .unwrap_or(&vec![])
  93. .iter()
  94. .map(|a| a.to_string())
  95. .collect();
  96. let (num_peers, num_connected_peers) =
  97. node.list_peers()
  98. .iter()
  99. .fold((0, 0), |(mut peers, mut connected), p| {
  100. if p.is_connected {
  101. connected += 1;
  102. }
  103. peers += 1;
  104. (peers, connected)
  105. });
  106. let (num_active_channels, num_inactive_channels) =
  107. node.list_channels()
  108. .iter()
  109. .fold((0, 0), |(mut active, mut inactive), c| {
  110. if c.is_usable {
  111. active += 1;
  112. } else {
  113. inactive += 1;
  114. }
  115. (active, inactive)
  116. });
  117. let balances = node.list_balances();
  118. // Calculate payment metrics for dashboard
  119. let all_payments = node.list_payments_with_filter(|_| true);
  120. let metrics = calculate_usage_metrics(&all_payments);
  121. let content = html! {
  122. h2 style="text-align: center; margin-bottom: 3rem;" { "Dashboard" }
  123. // Balance Summary as metric cards
  124. div class="card" {
  125. h2 { "Balance Summary" }
  126. div class="metrics-container" {
  127. div class="metric-card" {
  128. div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) }
  129. div class="metric-label" { "Lightning Balance" }
  130. }
  131. div class="metric-card" {
  132. div class="metric-value" { (format_sats_as_btc(balances.total_onchain_balance_sats)) }
  133. div class="metric-label" { "On-chain Balance" }
  134. }
  135. div class="metric-card" {
  136. div class="metric-value" { (format_sats_as_btc(balances.spendable_onchain_balance_sats)) }
  137. div class="metric-label" { "Spendable Balance" }
  138. }
  139. div class="metric-card" {
  140. div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats + balances.total_onchain_balance_sats)) }
  141. div class="metric-label" { "Combined Total" }
  142. }
  143. }
  144. }
  145. // Node Information - new layout based on Figma design
  146. section class="node-info-section" {
  147. div class="node-info-main-container" {
  148. // Left side - Node avatar and info
  149. div class="node-info-left" {
  150. div class="node-avatar" {
  151. img src="/static/images/nut.png" alt="Node Avatar" class="avatar-image";
  152. }
  153. div class="node-details" {
  154. h2 class="node-name" { (alias.clone()) }
  155. p class="node-address" {
  156. "Listening Address: "
  157. (listening_addresses.first().unwrap_or(&"127.0.0.1:8090".to_string()))
  158. }
  159. }
  160. }
  161. // Middle - Gray container with spinning globe animation
  162. div class="node-content-box" {
  163. div class="globe-container" {
  164. svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" {
  165. defs {
  166. symbol id="icon-world" viewBox="0 0 216 100" {
  167. title { "world" }
  168. g fill-rule="nonzero" {
  169. path d="M48 94l-3-4-2-14c0-3-1-5-3-8-4-5-6-9-4-11l1-4 1-3c2-1 9 0 11 1l3 2 2 3 1 2 8 2c1 1 2 2 0 7-1 5-2 7-4 7l-2 3-2 4-2 3-2 1c-2 2-2 9 0 10v1l-3-2zM188 90l3-2h1l-4 2zM176 87h2l-1 1-1-1zM195 86l3-2-2 2h-1zM175 83l-1-2-2-1-6 1c-5 1-5 1-5-2l1-4 2-2 4-3c5-4 9-5 9-3 0 3 3 3 4 1s1-2 1 0l3 4c2 4 1 6-2 10-4 3-7 4-8 1zM100 80c-2-4-4-11-3-14l-1-6c-1-1-2-3-1-4 0-2-4-3-9-3-4 0-5 0-7-3-1-2-2-4-1-7l3-6 3-3c1-2 10-4 11-2l6 3 5-1c3 1 4 0 5-1s-1-2-2-2l-4-1c0-1 3-3 6-2 3 0 3 0 2-2-2-2-6-2-7 0l-2 2-1 2-3-2-3-3c-1 0-1 1 1 2l1 2-2-1c-4-3-6-2-8 1-2 2-4 3-5 1-1-1 0-4 2-4l2-2 1-2 3-2 3-2 2 1c3 0 7-3 5-4l-1-3h-1l-1 3-2 2h-1l-2-1c-2-1-2-1 1-4 5-4 6-4 11-3 4 1 4 1 2 2v1l3-1 6-1c5 0 6-1 5-2l2 1c1 2 2 2 2 1-2-4 12-7 14-4l11 1 29 3 1 2-3 3c-2 0-2 0-1 1l1 3h-2c-1-1-2-3-1-4h-4l-6 2c-1 1-1 1 2 2 3 2 4 6 1 8v3c1 3 0 3-3 0s-4-1-2 3c3 4 3 7-2 8-5 2-4 1-2 5 2 3 0 5-3 4l-2-1-2-2-1-1-1-1-2-2c-1-2-1-2-4 0-2 1-3 4-3 5-1 3-1 3-3 1l-2-4c0-2-1-3-2-3l-1-1-4-2-6-1-4-2c-1 1 3 4 5 4h2c1 1 0 2-1 4-3 2-7 4-8 3l-7-10 5 10c2 2 3 3 5 2 3 0 2 1-2 7-4 4-4 5-4 8 1 3 1 4-1 6l-2 3c0 2-6 9-8 9l-3-2zm22-51l-2-3-1-1v-1c-2 0-2 2-1 4 2 3 4 4 4 1z" {}
  170. path d="M117 75c-1-2 0-6 2-7h2l-2 5c0 2-1 3-2 1zM186 64h-3c-2 0-6-3-5-5 1-1 6 1 7 3l2 3-2-1zM160 62h2c1 1 0 1-1 1l-1-1zM154 57l-1-2c2 2 3 1 2-2l-2-3 2 2 1 4 1 3v2l-3-4zM161 59c-1-1-1-2 1-4 3-3 4-3 4 0 0 4-2 6-5 4zM167 59l1-1 1 1-1 1-1-1zM176 59l1-1v2l-1-1zM141 52l1-1v2l-1-1zM170 52l1-1v2l-1-1zM32 50c-1-2-4-3-6-4-4-1-5-3-7-6l-3-5-2-2c-1-3-1-6 2-9 1-1 2-3 1-5 0-4-3-5-8-4H4l2-2 1-1 1-1 2-1c1-2 7-2 23-1 12 1 12 1 12-1h1c1 1 2 2 3 1l1 1-3 1c-2 0-8 4-8 5l2 1 2 3 4-3c3-4 4-4 5-3l3 1 1 2 1 2c3 0-1 2-4 2-2 0-2 0-2 2 1 1 0 2-2 2-4 1-12 9-12 12 0 2 0 2-1 1 0-2-2-3-6-2-3 0-4 1-4 3-2 4 0 6 3 4 3-1 3-1 2 1s-1 2 1 2l1 2 1 3 1 1-3-2zm8-24l1-1c0-1-4-3-5-2l1 1v2c-1 1-1 1 0 0h3zM167 47v-3l1 2c1 2 0 3-1 1z" {}
  171. path d="M41 43h2l-1 1-1-1zM37 42v-1l2 1h-2zM16 38l1-1v2l-1-1zM172 32l2-3h1c1 2 0 4-3 4v-1zM173 26h2l-1 1-1-1zM56 22h2l-2 1v-1zM87 19l1-2 1 3-1 1-1-2zM85 19l1-1v1l-1 1v-1zM64 12l1-3c2 0-1-4-3-4s-2 0 0-1V3l-6 2c-3 1-3 1-2-1 2-1 4-2 15-2h14c0 2-6 7-10 9l-5 2-2 1-2-2zM53 12l1-1c2 0-1-3-3-3-2-1-1-1 1-1l4 2c2 1 2 1 1 3-2 1-4 2-4 0zM80 12l1-1 1 1-1 1-1-1zM36 8h-2V7c1-1 7 0 7 1h-5zM116 7l1-1v1l-1 1V7zM50 5h2l-1 1-1-1zM97 5l2-1c0-1 1-1 0 0l-2 1z" {}
  172. }
  173. }
  174. symbol id="icon-repeated-world" viewBox="0 0 432 100" {
  175. use href="#icon-world" x="0" {}
  176. use href="#icon-world" x="189" {}
  177. }
  178. }
  179. }
  180. span class="world" {
  181. span class="images" {
  182. svg { use href="#icon-repeated-world" {} }
  183. }
  184. }
  185. }
  186. }
  187. }
  188. // Right side - Connections metrics
  189. aside class="node-metrics" {
  190. div class="card" {
  191. h3 { "Connections" }
  192. div class="metrics-container" {
  193. div class="metric-card" {
  194. div class="metric-value" { (format!("{}/{}", num_connected_peers, num_peers)) }
  195. div class="metric-label" { "Connected Peers" }
  196. }
  197. div class="metric-card" {
  198. div class="metric-value" { (format!("{}/{}", num_active_channels, num_active_channels + num_inactive_channels)) }
  199. div class="metric-label" { "Active Channels" }
  200. }
  201. }
  202. }
  203. }
  204. }
  205. // Lightning Network Activity as metric cards
  206. div class="card" {
  207. h2 { "Lightning Network Activity" }
  208. div class="metrics-container" {
  209. div class="metric-card" {
  210. div class="metric-value" { (format_sats_as_btc(metrics.lightning_inflow_24h)) }
  211. div class="metric-label" { "24h LN Inflow" }
  212. }
  213. div class="metric-card" {
  214. div class="metric-value" { (format_sats_as_btc(metrics.lightning_outflow_24h)) }
  215. div class="metric-label" { "24h LN Outflow" }
  216. }
  217. div class="metric-card" {
  218. div class="metric-value" { (format_sats_as_btc(metrics.lightning_inflow_all_time)) }
  219. div class="metric-label" { "All-time LN Inflow" }
  220. }
  221. div class="metric-card" {
  222. div class="metric-value" { (format_sats_as_btc(metrics.lightning_outflow_all_time)) }
  223. div class="metric-label" { "All-time LN Outflow" }
  224. }
  225. }
  226. }
  227. // On-chain Activity as metric cards
  228. div class="card" {
  229. h2 { "On-chain Activity" }
  230. div class="metrics-container" {
  231. div class="metric-card" {
  232. div class="metric-value" { (format_sats_as_btc(metrics.onchain_inflow_24h)) }
  233. div class="metric-label" { "24h On-chain Inflow" }
  234. }
  235. div class="metric-card" {
  236. div class="metric-value" { (format_sats_as_btc(metrics.onchain_outflow_24h)) }
  237. div class="metric-label" { "24h On-chain Outflow" }
  238. }
  239. div class="metric-card" {
  240. div class="metric-value" { (format_sats_as_btc(metrics.onchain_inflow_all_time)) }
  241. div class="metric-label" { "All-time On-chain Inflow" }
  242. }
  243. div class="metric-card" {
  244. div class="metric-value" { (format_sats_as_btc(metrics.onchain_outflow_all_time)) }
  245. div class="metric-label" { "All-time On-chain Outflow" }
  246. }
  247. }
  248. }
  249. };
  250. Ok(Html(layout("Dashboard", content).into_string()))
  251. }