Browse Source

cdk-ldk web ui updates (#1027)

Erik 2 months ago
parent
commit
39f256a648

+ 1 - 4
crates/cdk-ldk-node/Cargo.toml

@@ -15,7 +15,7 @@ async-trait.workspace = true
 axum.workspace = true
 axum.workspace = true
 cdk-common = { workspace = true, features = ["mint"] }
 cdk-common = { workspace = true, features = ["mint"] }
 futures.workspace = true
 futures.workspace = true
-tokio.workspace = true 
+tokio.workspace = true
 tokio-util.workspace = true
 tokio-util.workspace = true
 tracing.workspace = true
 tracing.workspace = true
 thiserror.workspace = true
 thiserror.workspace = true
@@ -29,6 +29,3 @@ tower-http.workspace = true
 rust-embed = "8.5.0"
 rust-embed = "8.5.0"
 serde_urlencoded = "0.7"
 serde_urlencoded = "0.7"
 urlencoding = "2.1"
 urlencoding = "2.1"
-
-
-

+ 61 - 29
crates/cdk-ldk-node/src/web/handlers/channels.rs

@@ -213,7 +213,7 @@ pub async fn post_open_channel(
 }
 }
 
 
 pub async fn close_channel_page(
 pub async fn close_channel_page(
-    State(_state): State<AppState>,
+    State(state): State<AppState>,
     query: Query<HashMap<String, String>>,
     query: Query<HashMap<String, String>>,
 ) -> Result<Html<String>, StatusCode> {
 ) -> Result<Html<String>, StatusCode> {
     let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
     let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
@@ -229,24 +229,40 @@ pub async fn close_channel_page(
         return Ok(Html(layout("Close Channel Error", content).into_string()));
         return Ok(Html(layout("Close Channel Error", content).into_string()));
     }
     }
 
 
+    // Get channel information for amount display
+    let channels = state.node.inner.list_channels();
+    let channel = channels
+        .iter()
+        .find(|c| c.user_channel_id.0.to_string() == channel_id);
+
     let content = form_card(
     let content = form_card(
         "Close Channel",
         "Close Channel",
         html! {
         html! {
-            p { "Are you sure you want to close this channel?" }
-            div class="info-item" {
-                span class="info-label" { "User Channel ID:" }
-                span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel_id) }
-            }
-            div class="info-item" {
-                span class="info-label" { "Node ID:" }
-                span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
+            p style="margin-bottom: 1.5rem;" { "Are you sure you want to close this channel?" }
+
+            // Channel details in consistent format
+            div class="channel-details" {
+                div class="detail-row" {
+                    span class="detail-label" { "User Channel ID" }
+                    span class="detail-value-amount" { (channel_id) }
+                }
+                div class="detail-row" {
+                    span class="detail-label" { "Node ID" }
+                    span class="detail-value-amount" { (node_id) }
+                }
+                @if let Some(ch) = channel {
+                    div class="detail-row" {
+                        span class="detail-label" { "Channel Amount" }
+                        span class="detail-value-amount" { (format_sats_as_btc(ch.channel_value_sats)) }
+                    }
+                }
             }
             }
-            form method="post" action="/channels/close" style="margin-top: 1rem;" {
+
+            form method="post" action="/channels/close" style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;" {
                 input type="hidden" name="channel_id" value=(channel_id) {}
                 input type="hidden" name="channel_id" value=(channel_id) {}
                 input type="hidden" name="node_id" value=(node_id) {}
                 input type="hidden" name="node_id" value=(node_id) {}
-                button type="submit" style="background: #dc3545;" { "Close Channel" }
-                " "
-                a href="/balance" { button type="button" { "Cancel" } }
+                a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
+                button type="submit" class="button-destructive" { "Close Channel" }
             }
             }
         },
         },
     );
     );
@@ -255,7 +271,7 @@ pub async fn close_channel_page(
 }
 }
 
 
 pub async fn force_close_channel_page(
 pub async fn force_close_channel_page(
-    State(_state): State<AppState>,
+    State(state): State<AppState>,
     query: Query<HashMap<String, String>>,
     query: Query<HashMap<String, String>>,
 ) -> Result<Html<String>, StatusCode> {
 ) -> Result<Html<String>, StatusCode> {
     let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
     let channel_id = query.get("channel_id").unwrap_or(&"".to_string()).clone();
@@ -273,32 +289,48 @@ pub async fn force_close_channel_page(
         ));
         ));
     }
     }
 
 
+    // Get channel information for amount display
+    let channels = state.node.inner.list_channels();
+    let channel = channels
+        .iter()
+        .find(|c| c.user_channel_id.0.to_string() == channel_id);
+
     let content = form_card(
     let content = form_card(
         "Force Close Channel",
         "Force Close Channel",
         html! {
         html! {
-            div style="border: 2px solid #d63384; background-color: rgba(214, 51, 132, 0.1); padding: 1rem; margin-bottom: 1rem; border-radius: 0.5rem;" {
-                h4 style="color: #d63384; margin: 0 0 0.5rem 0;" { "⚠️ Warning: Force Close" }
-                p style="color: #d63384; margin: 0; font-size: 0.9rem;" {
+            div style="border: 2px solid #f97316; background-color: rgba(249, 115, 22, 0.1); padding: 1rem; margin-bottom: 1rem; border-radius: 0.5rem;" {
+                h4 style="color: #f97316; margin: 0 0 0.5rem 0;" { "⚠️ Warning: Force Close" }
+                p style="color: #f97316; margin: 0; font-size: 0.9rem;" {
                     "Force close should NOT be used if normal close is preferred. "
                     "Force close should NOT be used if normal close is preferred. "
                     "Force close will immediately broadcast the latest commitment transaction and may result in delayed fund recovery. "
                     "Force close will immediately broadcast the latest commitment transaction and may result in delayed fund recovery. "
                     "Only use this if the channel counterparty is unresponsive or there are other issues preventing normal closure."
                     "Only use this if the channel counterparty is unresponsive or there are other issues preventing normal closure."
                 }
                 }
             }
             }
-            p { "Are you sure you want to force close this channel?" }
-            div class="info-item" {
-                span class="info-label" { "User Channel ID:" }
-                span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel_id) }
-            }
-            div class="info-item" {
-                span class="info-label" { "Node ID:" }
-                span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
+            p style="margin-bottom: 1.5rem;" { "Are you sure you want to force close this channel?" }
+
+            // Channel details in consistent format
+            div class="channel-details" {
+                div class="detail-row" {
+                    span class="detail-label" { "User Channel ID" }
+                    span class="detail-value-amount" { (channel_id) }
+                }
+                div class="detail-row" {
+                    span class="detail-label" { "Node ID" }
+                    span class="detail-value-amount" { (node_id) }
+                }
+                @if let Some(ch) = channel {
+                    div class="detail-row" {
+                        span class="detail-label" { "Channel Amount" }
+                        span class="detail-value-amount" { (format_sats_as_btc(ch.channel_value_sats)) }
+                    }
+                }
             }
             }
-            form method="post" action="/channels/force-close" style="margin-top: 1rem;" {
+
+            form method="post" action="/channels/force-close" style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;" {
                 input type="hidden" name="channel_id" value=(channel_id) {}
                 input type="hidden" name="channel_id" value=(channel_id) {}
                 input type="hidden" name="node_id" value=(node_id) {}
                 input type="hidden" name="node_id" value=(node_id) {}
-                button type="submit" style="background: #d63384;" { "Force Close Channel" }
-                " "
-                a href="/balance" { button type="button" { "Cancel" } }
+                a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
+                button type="submit" class="button-destructive" { "Force Close Channel" }
             }
             }
         },
         },
     );
     );

+ 103 - 54
crates/cdk-ldk-node/src/web/handlers/lightning.rs

@@ -3,7 +3,7 @@ use axum::http::StatusCode;
 use axum::response::Html;
 use axum::response::Html;
 use maud::html;
 use maud::html;
 
 
-use crate::web::handlers::AppState;
+use crate::web::handlers::utils::AppState;
 use crate::web::templates::{format_sats_as_btc, layout};
 use crate::web::templates::{format_sats_as_btc, layout};
 
 
 pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
 pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
@@ -26,18 +26,35 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
         html! {
         html! {
             h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
             h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
 
 
-            // Quick Actions section - matching dashboard style
+            // Quick Actions section - individual cards
             div class="card" style="margin-bottom: 2rem;" {
             div class="card" style="margin-bottom: 2rem;" {
                 h2 { "Quick Actions" }
                 h2 { "Quick Actions" }
-                div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                    a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Open Channel" }
+                div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                    // Open Channel Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning Network channel to connect with another node." }
+                        a href="/channels/open" style="text-decoration: none;" {
+                            button class="button-outline" { "Open Channel" }
+                        }
                     }
                     }
-                    a href="/invoices" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Create Invoice" }
+
+                    // Create Invoice Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments from other users or services." }
+                        a href="/invoices" style="text-decoration: none;" {
+                            button class="button-outline" { "Create Invoice" }
+                        }
                     }
                     }
-                    a href="/payments/send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Make Lightning Payment" }
+
+                    // Make Payment Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices. BOLT 11 & 12 supported." }
+                        a href="/invoices" style="text-decoration: none;" {
+                            button class="button-outline" { "Make Payment" }
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -73,18 +90,35 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
         html! {
         html! {
             h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
             h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
 
 
-            // Quick Actions section - matching dashboard style
+            // Quick Actions section - individual cards
             div class="card" style="margin-bottom: 2rem;" {
             div class="card" style="margin-bottom: 2rem;" {
                 h2 { "Quick Actions" }
                 h2 { "Quick Actions" }
-                div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                    a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Open Channel" }
+                div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                    // Open Channel Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning channel by connecting with another node." }
+                        a href="/channels/open" style="text-decoration: none;" {
+                            button class="button-outline" { "Open Channel" }
+                        }
                     }
                     }
-                    a href="/invoices" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Create Invoice" }
+
+                    // Create Invoice Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments." }
+                        a href="/invoices" style="text-decoration: none;" {
+                            button class="button-outline" { "Create Invoice" }
+                        }
                     }
                     }
-                    a href="/payments/send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                        button class="button-primary" style="width: 100%;" { "Make Lightning Payment" }
+
+                    // Make Payment Card
+                    div class="quick-action-card" {
+                        h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" }
+                        p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices." }
+                        a href="/payments/send" style="text-decoration: none;" {
+                            button class="button-outline" { "Make Payment" }
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -112,57 +146,72 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
                 }
                 }
             }
             }
 
 
-            div class="card" {
-                h2 { "Channel Details" }
+            // Channel Details header (outside card)
+            h2 class="section-header" { "Channel Details" }
+
+            // Channels list
+            @for (index, channel) in channels.iter().enumerate() {
+                @let node_id = channel.counterparty_node_id.to_string();
+                @let channel_number = index + 1;
 
 
-                // Channels list
-                @for channel in &channels {
-                    div class="channel-item" {
-                        div class="channel-header" {
-                            span class="channel-id" { "Channel ID: " (channel.channel_id.to_string()) }
+                div class="channel-box" {
+                    // Channel number as prominent header
+                    div class="channel-alias" { (format!("Channel {}", channel_number)) }
+
+                    // Channel details in left-aligned format
+                    div class="channel-details" {
+                        div class="detail-row" {
+                            span class="detail-label" { "Channel ID" }
+                            span class="detail-value-amount" { (channel.channel_id.to_string()) }
+                        }
+                        @if let Some(short_channel_id) = channel.short_channel_id {
+                            div class="detail-row" {
+                                span class="detail-label" { "Short Channel ID" }
+                                span class="detail-value-amount" { (short_channel_id.to_string()) }
+                            }
+                        }
+                        div class="detail-row" {
+                            span class="detail-label" { "Node ID" }
+                            span class="detail-value-amount" { (node_id) }
+                        }
+                        div class="detail-row" {
+                            span class="detail-label" { "Status" }
                             @if channel.is_usable {
                             @if channel.is_usable {
                                 span class="status-badge status-active" { "Active" }
                                 span class="status-badge status-active" { "Active" }
                             } @else {
                             } @else {
                                 span class="status-badge status-inactive" { "Inactive" }
                                 span class="status-badge status-inactive" { "Inactive" }
                             }
                             }
                         }
                         }
-                        div class="info-item" {
-                            span class="info-label" { "Counterparty" }
-                            span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel.counterparty_node_id.to_string()) }
+                    }
+
+                    // Balance information cards (keeping existing style)
+                    div class="balance-info" {
+                        div class="balance-item" {
+                            div class="balance-amount" { (format_sats_as_btc(channel.outbound_capacity_msat / 1000)) }
+                            div class="balance-label" { "Outbound" }
                         }
                         }
-                        @if let Some(short_channel_id) = channel.short_channel_id {
-                            div class="info-item" {
-                                span class="info-label" { "Short Channel ID" }
-                                span class="info-value" { (short_channel_id.to_string()) }
-                            }
+                        div class="balance-item" {
+                            div class="balance-amount" { (format_sats_as_btc(channel.inbound_capacity_msat / 1000)) }
+                            div class="balance-label" { "Inbound" }
                         }
                         }
-                        div class="balance-info" {
-                            div class="balance-item" {
-                                div class="balance-amount" { (format_sats_as_btc(channel.outbound_capacity_msat / 1000)) }
-                                div class="balance-label" { "Outbound" }
-                            }
-                            div class="balance-item" {
-                                div class="balance-amount" { (format_sats_as_btc(channel.inbound_capacity_msat / 1000)) }
-                                div class="balance-label" { "Inbound" }
-                            }
-                            div class="balance-item" {
-                                div class="balance-amount" { (format_sats_as_btc(channel.channel_value_sats)) }
-                                div class="balance-label" { "Total" }
-                            }
+                        div class="balance-item" {
+                            div class="balance-amount" { (format_sats_as_btc(channel.channel_value_sats)) }
+                            div class="balance-label" { "Total" }
                         }
                         }
-                        @if channel.is_usable {
-                            div style="margin-top: 1rem; display: flex; gap: 0.5rem;" {
-                                a href=(format!("/channels/close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
-                                    button style="background: #dc3545;" { "Close Channel" }
-                                }
-                                a href=(format!("/channels/force-close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
-                                    button style="background: #d63384;" title="Force close should not be used if normal close is preferred. Force close will broadcast the latest commitment transaction immediately." { "Force Close" }
-                                }
+                    }
+
+                    // Action buttons
+                    @if channel.is_usable {
+                        div class="channel-actions" {
+                            a href=(format!("/channels/close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
+                                button class="button-secondary" { "Close Channel" }
+                            }
+                            a href=(format!("/channels/force-close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) {
+                                button class="button-destructive" title="Force close should not be used if normal close is preferred. Force close will broadcast the latest commitment transaction immediately." { "Force Close" }
                             }
                             }
                         }
                         }
                     }
                     }
                 }
                 }
-
             }
             }
         }
         }
     };
     };

+ 54 - 21
crates/cdk-ldk-node/src/web/handlers/onchain.rs

@@ -79,15 +79,26 @@ pub async fn onchain_page(
     let mut content = html! {
     let mut content = html! {
         h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
         h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
 
-        // Quick Actions section - matching dashboard style
+        // Quick Actions section - individual cards
         div class="card" style="margin-bottom: 2rem;" {
         div class="card" style="margin-bottom: 2rem;" {
             h2 { "Quick Actions" }
             h2 { "Quick Actions" }
-            div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                    button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
+            div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                // Receive Bitcoin Card
+                div class="quick-action-card" {
+                    h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
+                    p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
+                    a href="/onchain?action=receive" style="text-decoration: none;" {
+                        button class="button-outline" { "Receive Bitcoin" }
+                    }
                 }
                 }
-                a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                    button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
+
+                // Send Bitcoin Card
+                div class="quick-action-card" {
+                    h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
+                    p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
+                    a href="/onchain?action=send" style="text-decoration: none;" {
+                        button class="button-outline" { "Send Bitcoin" }
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -113,15 +124,26 @@ pub async fn onchain_page(
             content = html! {
             content = html! {
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
 
-                // Quick Actions section - matching dashboard style
+                // Quick Actions section - individual cards
                 div class="card" style="margin-bottom: 2rem;" {
                 div class="card" style="margin-bottom: 2rem;" {
                     h2 { "Quick Actions" }
                     h2 { "Quick Actions" }
-                    div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                        a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                            button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
+                    div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                        // Receive Bitcoin Card
+                        div class="quick-action-card" {
+                            h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
+                            p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
+                            a href="/onchain?action=receive" style="text-decoration: none;" {
+                                button class="button-outline" { "Receive Bitcoin" }
+                            }
                         }
                         }
-                        a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                            button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
+
+                        // Send Bitcoin Card
+                        div class="quick-action-card" {
+                            h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
+                            p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
+                            a href="/onchain?action=send" style="text-decoration: none;" {
+                                button class="button-outline" { "Send Bitcoin" }
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
@@ -141,7 +163,7 @@ pub async fn onchain_page(
                             }
                             }
                             input type="hidden" id="send_action" name="send_action" value="send" {}
                             input type="hidden" id="send_action" name="send_action" value="send" {}
                             div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
                             div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
-                                a href="/onchain" { button type="button" { "Cancel" } }
+                                a href="/onchain" { button type="button" class="button-secondary" { "Cancel" } }
                                 div style="display: flex; gap: 0.5rem;" {
                                 div style="display: flex; gap: 0.5rem;" {
                                     button type="submit" onclick="document.getElementById('send_action').value='send'" { "Send Payment" }
                                     button type="submit" onclick="document.getElementById('send_action').value='send'" { "Send Payment" }
                                     button type="submit" onclick="document.getElementById('send_action').value='send_all'; document.getElementById('amount_sat').value=''" { "Send All" }
                                     button type="submit" onclick="document.getElementById('send_action').value='send_all'; document.getElementById('amount_sat').value=''" { "Send All" }
@@ -171,15 +193,26 @@ pub async fn onchain_page(
             content = html! {
             content = html! {
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
 
-                // Quick Actions section - matching dashboard style
+                // Quick Actions section - individual cards
                 div class="card" style="margin-bottom: 2rem;" {
                 div class="card" style="margin-bottom: 2rem;" {
                     h2 { "Quick Actions" }
                     h2 { "Quick Actions" }
-                    div style="display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap;" {
-                        a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                            button class="button-primary" style="width: 100%;" { "Receive Bitcoin" }
+                    div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
+                        // Receive Bitcoin Card
+                        div class="quick-action-card" {
+                            h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
+                            p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
+                            a href="/onchain?action=receive" style="text-decoration: none;" {
+                                button class="button-outline" { "Receive Bitcoin" }
+                            }
                         }
                         }
-                        a href="/onchain?action=send" style="text-decoration: none; flex: 1; min-width: 200px;" {
-                            button class="button-primary" style="width: 100%;" { "Send Bitcoin" }
+
+                        // Send Bitcoin Card
+                        div class="quick-action-card" {
+                            h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
+                            p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
+                            a href="/onchain?action=send" style="text-decoration: none;" {
+                                button class="button-outline" { "Send Bitcoin" }
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
@@ -191,7 +224,7 @@ pub async fn onchain_page(
                         form method="post" action="/onchain/new-address" {
                         form method="post" action="/onchain/new-address" {
                             p style="margin-bottom: 2rem;" { "Click the button below to generate a new Bitcoin address for receiving on-chain payments." }
                             p style="margin-bottom: 2rem;" { "Click the button below to generate a new Bitcoin address for receiving on-chain payments." }
                             div style="display: flex; justify-content: space-between; gap: 1rem;" {
                             div style="display: flex; justify-content: space-between; gap: 1rem;" {
-                                a href="/onchain" { button type="button" { "Cancel" } }
+                                a href="/onchain" { button type="button" class="button-secondary" { "Cancel" } }
                                 button class="button-primary" type="submit" { "Generate New Address" }
                                 button class="button-primary" type="submit" { "Generate New Address" }
                             }
                             }
                         }
                         }
@@ -345,7 +378,7 @@ pub async fn onchain_confirm_page(
         div class="card" {
         div class="card" {
             div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
             div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
                 a href="/onchain?action=send" {
                 a href="/onchain?action=send" {
-                    button type="button" class="button-secondary" { "Cancel" }
+                    button type="button" class="button-secondary" { "Cancel" }
                 }
                 }
                 div style="display: flex; gap: 0.5rem;" {
                 div style="display: flex; gap: 0.5rem;" {
                     a href=(confirmation_url) {
                     a href=(confirmation_url) {

File diff suppressed because it is too large
+ 447 - 144
crates/cdk-ldk-node/src/web/templates/layout.rs


+ 1 - 1
crates/cdk-ldk-node/src/web/templates/payments.rs

@@ -17,7 +17,7 @@ pub fn payment_list_item(
     let status_class = match status {
     let status_class = match status {
         "Succeeded" => "status-active",
         "Succeeded" => "status-active",
         "Failed" => "status-inactive",
         "Failed" => "status-inactive",
-        "Pending" => "status-badge",
+        "Pending" => "status-pending",
         _ => "status-badge",
         _ => "status-badge",
     };
     };
 
 

BIN
crates/cdk-ldk-node/static/images/bg-dark.jpg


Some files were not shown because too many files changed in this diff