| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- use axum::extract::State;
- use axum::http::StatusCode;
- use axum::response::Html;
- use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
- use maud::html;
- use crate::web::handlers::AppState;
- use crate::web::templates::{format_sats_as_btc, layout};
- #[derive(Debug)]
- pub struct UsageMetrics {
- pub lightning_inflow_24h: u64,
- pub lightning_outflow_24h: u64,
- pub lightning_inflow_all_time: u64,
- pub lightning_outflow_all_time: u64,
- pub onchain_inflow_24h: u64,
- pub onchain_outflow_24h: u64,
- pub onchain_inflow_all_time: u64,
- pub onchain_outflow_all_time: u64,
- }
- /// Calculate usage metrics from payment history
- fn calculate_usage_metrics(payments: &[ldk_node::payment::PaymentDetails]) -> UsageMetrics {
- use std::time::{SystemTime, UNIX_EPOCH};
- let now = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs();
- let twenty_four_hours_ago = now.saturating_sub(24 * 60 * 60);
- let mut metrics = UsageMetrics {
- lightning_inflow_24h: 0,
- lightning_outflow_24h: 0,
- lightning_inflow_all_time: 0,
- lightning_outflow_all_time: 0,
- onchain_inflow_24h: 0,
- onchain_outflow_24h: 0,
- onchain_inflow_all_time: 0,
- onchain_outflow_all_time: 0,
- };
- for payment in payments {
- if payment.status != PaymentStatus::Succeeded {
- continue;
- }
- let amount_sats = payment.amount_msat.unwrap_or(0) / 1000;
- let is_recent = payment.latest_update_timestamp >= twenty_four_hours_ago;
- match &payment.kind {
- PaymentKind::Bolt11 { .. }
- | PaymentKind::Bolt12Offer { .. }
- | PaymentKind::Bolt12Refund { .. }
- | PaymentKind::Spontaneous { .. }
- | PaymentKind::Bolt11Jit { .. } => match payment.direction {
- PaymentDirection::Inbound => {
- metrics.lightning_inflow_all_time += amount_sats;
- if is_recent {
- metrics.lightning_inflow_24h += amount_sats;
- }
- }
- PaymentDirection::Outbound => {
- metrics.lightning_outflow_all_time += amount_sats;
- if is_recent {
- metrics.lightning_outflow_24h += amount_sats;
- }
- }
- },
- PaymentKind::Onchain { .. } => match payment.direction {
- PaymentDirection::Inbound => {
- metrics.onchain_inflow_all_time += amount_sats;
- if is_recent {
- metrics.onchain_inflow_24h += amount_sats;
- }
- }
- PaymentDirection::Outbound => {
- metrics.onchain_outflow_all_time += amount_sats;
- if is_recent {
- metrics.onchain_outflow_24h += amount_sats;
- }
- }
- },
- }
- }
- metrics
- }
- pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
- let node = &state.node.inner;
- let _node_id = node.node_id().to_string();
- let alias = node
- .node_alias()
- .map(|a| a.to_string())
- .unwrap_or_else(|| "No alias set".to_string());
- let listening_addresses: Vec<String> = state
- .node
- .inner
- .announcement_addresses()
- .as_ref()
- .unwrap_or(&vec![])
- .iter()
- .map(|a| a.to_string())
- .collect();
- let (num_peers, num_connected_peers) =
- node.list_peers()
- .iter()
- .fold((0, 0), |(mut peers, mut connected), p| {
- if p.is_connected {
- connected += 1;
- }
- peers += 1;
- (peers, connected)
- });
- let (num_active_channels, num_inactive_channels) =
- node.list_channels()
- .iter()
- .fold((0, 0), |(mut active, mut inactive), c| {
- if c.is_usable {
- active += 1;
- } else {
- inactive += 1;
- }
- (active, inactive)
- });
- let balances = node.list_balances();
- // Calculate payment metrics for dashboard
- let all_payments = node.list_payments_with_filter(|_| true);
- let metrics = calculate_usage_metrics(&all_payments);
- let content = html! {
- h2 style="text-align: center; margin-bottom: 3rem;" { "Dashboard" }
- // Balance Summary as metric cards
- div class="card" {
- h2 { "Balance Summary" }
- div class="metrics-container" {
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) }
- div class="metric-label" { "Lightning Balance" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(balances.total_onchain_balance_sats)) }
- div class="metric-label" { "On-chain Balance" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(balances.spendable_onchain_balance_sats)) }
- div class="metric-label" { "Spendable Balance" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats + balances.total_onchain_balance_sats)) }
- div class="metric-label" { "Combined Total" }
- }
- }
- }
- // Node Information - new layout based on Figma design
- section class="node-info-section" {
- div class="node-info-main-container" {
- // Left side - Node avatar and info
- div class="node-info-left" {
- div class="node-avatar" {
- img src="/static/images/nut.png" alt="Node Avatar" class="avatar-image";
- }
- div class="node-details" {
- h2 class="node-name" { (alias.clone()) }
- p class="node-address" {
- "Listening Address: "
- (listening_addresses.first().unwrap_or(&"127.0.0.1:8090".to_string()))
- }
- }
- }
- // Middle - Gray container with spinning globe animation
- div class="node-content-box" {
- div class="globe-container" {
- 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" {
- defs {
- symbol id="icon-world" viewBox="0 0 216 100" {
- title { "world" }
- g fill-rule="nonzero" {
- 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" {}
- 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" {}
- 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" {}
- }
- }
- symbol id="icon-repeated-world" viewBox="0 0 432 100" {
- use href="#icon-world" x="0" {}
- use href="#icon-world" x="189" {}
- }
- }
- }
- span class="world" {
- span class="images" {
- svg { use href="#icon-repeated-world" {} }
- }
- }
- }
- }
- }
- // Right side - Connections metrics
- aside class="node-metrics" {
- div class="card" {
- h3 { "Connections" }
- div class="metrics-container" {
- div class="metric-card" {
- div class="metric-value" { (format!("{}/{}", num_connected_peers, num_peers)) }
- div class="metric-label" { "Connected Peers" }
- }
- div class="metric-card" {
- div class="metric-value" { (format!("{}/{}", num_active_channels, num_active_channels + num_inactive_channels)) }
- div class="metric-label" { "Active Channels" }
- }
- }
- }
- }
- }
- // Lightning Network Activity as metric cards
- div class="card" {
- h2 { "Lightning Network Activity" }
- div class="metrics-container" {
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(metrics.lightning_inflow_24h)) }
- div class="metric-label" { "24h LN Inflow" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(metrics.lightning_outflow_24h)) }
- div class="metric-label" { "24h LN Outflow" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(metrics.lightning_inflow_all_time)) }
- div class="metric-label" { "All-time LN Inflow" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(metrics.lightning_outflow_all_time)) }
- div class="metric-label" { "All-time LN Outflow" }
- }
- }
- }
- // On-chain Activity as metric cards
- div class="card" {
- h2 { "On-chain Activity" }
- div class="metrics-container" {
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(metrics.onchain_inflow_24h)) }
- div class="metric-label" { "24h On-chain Inflow" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(metrics.onchain_outflow_24h)) }
- div class="metric-label" { "24h On-chain Outflow" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(metrics.onchain_inflow_all_time)) }
- div class="metric-label" { "All-time On-chain Inflow" }
- }
- div class="metric-card" {
- div class="metric-value" { (format_sats_as_btc(metrics.onchain_outflow_all_time)) }
- div class="metric-label" { "All-time On-chain Outflow" }
- }
- }
- }
- };
- Ok(Html(layout("Dashboard", content).into_string()))
- }
|