Эх сурвалжийг харах

Improve web interface with dynamic status, navigation, and mobile support (#1073)

* Improve transaction confirmation UI: reorder elements, move buttons to details card, shorten button text
* feat: real node status

---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
Erik 1 сар өмнө
parent
commit
7d78240da5

+ 33 - 20
crates/cdk-ldk-node/src/web/handlers/channels.rs

@@ -15,7 +15,8 @@ use serde::Deserialize;
 use crate::web::handlers::utils::deserialize_optional_u64;
 use crate::web::handlers::AppState;
 use crate::web::templates::{
-    error_message, form_card, format_sats_as_btc, info_card, layout, success_message,
+    error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status,
+    success_message,
 };
 
 #[derive(Deserialize)]
@@ -43,7 +44,7 @@ pub async fn channels_page(State(_state): State<AppState>) -> Result<Response, S
         .unwrap())
 }
 
-pub async fn open_channel_page(State(_state): State<AppState>) -> Result<Html<String>, StatusCode> {
+pub async fn open_channel_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
     let content = form_card(
         "Open New Channel",
         html! {
@@ -68,14 +69,18 @@ pub async fn open_channel_page(State(_state): State<AppState>) -> Result<Html<St
                     label for="push_btc" { "Push Amount (optional)" }
                     input type="number" id="push_btc" name="push_btc" placeholder="₿0" step="1" {}
                 }
-                button type="submit" { "Open Channel" }
-                " "
-                a href="/balance" { button type="button" { "Cancel" } }
+                div class="form-actions" {
+                    a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
+                    button type="submit" class="button-primary" { "Open Channel" }
+                }
             }
         },
     );
 
-    Ok(Html(layout("Open Channel", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("Open Channel", content, is_running).into_string(),
+    ))
 }
 
 pub async fn post_open_channel(
@@ -105,7 +110,7 @@ pub async fn post_open_channel(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Open Channel Error", content).into_string(),
+                    layout_with_status("Open Channel Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -125,7 +130,7 @@ pub async fn post_open_channel(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Open Channel Error", content).into_string(),
+                    layout_with_status("Open Channel Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -149,7 +154,7 @@ pub async fn post_open_channel(
             .status(StatusCode::INTERNAL_SERVER_ERROR)
             .header("content-type", "text/html")
             .body(Body::from(
-                layout("Open Channel Error", content).into_string(),
+                layout_with_status("Open Channel Error", content, true).into_string(),
             ))
             .unwrap());
     }
@@ -207,7 +212,7 @@ pub async fn post_open_channel(
     Ok(Response::builder()
         .header("content-type", "text/html")
         .body(Body::from(
-            layout("Open Channel Result", content).into_string(),
+            layout_with_status("Open Channel Result", content, true).into_string(),
         ))
         .unwrap())
 }
@@ -226,7 +231,9 @@ pub async fn close_channel_page(
                 a href="/balance" { button { "← Back to Lightning" } }
             }
         };
-        return Ok(Html(layout("Close Channel Error", content).into_string()));
+        return Ok(Html(
+            layout_with_status("Close Channel Error", content, true).into_string(),
+        ));
     }
 
     // Get channel information for amount display
@@ -267,7 +274,10 @@ pub async fn close_channel_page(
         },
     );
 
-    Ok(Html(layout("Close Channel", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("Close Channel", content, is_running).into_string(),
+    ))
 }
 
 pub async fn force_close_channel_page(
@@ -285,7 +295,7 @@ pub async fn force_close_channel_page(
             }
         };
         return Ok(Html(
-            layout("Force Close Channel Error", content).into_string(),
+            layout_with_status("Force Close Channel Error", content, true).into_string(),
         ));
     }
 
@@ -335,7 +345,10 @@ pub async fn force_close_channel_page(
         },
     );
 
-    Ok(Html(layout("Force Close Channel", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("Force Close Channel", content, is_running).into_string(),
+    ))
 }
 
 pub async fn post_close_channel(
@@ -365,7 +378,7 @@ pub async fn post_close_channel(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Close Channel Error", content).into_string(),
+                    layout_with_status("Close Channel Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -385,7 +398,7 @@ pub async fn post_close_channel(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Close Channel Error", content).into_string(),
+                    layout_with_status("Close Channel Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -436,7 +449,7 @@ pub async fn post_close_channel(
     Ok(Response::builder()
         .header("content-type", "text/html")
         .body(Body::from(
-            layout("Close Channel Result", content).into_string(),
+            layout_with_status("Close Channel Result", content, true).into_string(),
         ))
         .unwrap())
 }
@@ -468,7 +481,7 @@ pub async fn post_force_close_channel(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Force Close Channel Error", content).into_string(),
+                    layout_with_status("Force Close Channel Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -488,7 +501,7 @@ pub async fn post_force_close_channel(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Force Close Channel Error", content).into_string(),
+                    layout_with_status("Force Close Channel Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -541,7 +554,7 @@ pub async fn post_force_close_channel(
     Ok(Response::builder()
         .header("content-type", "text/html")
         .body(Body::from(
-            layout("Force Close Channel Result", content).into_string(),
+            layout_with_status("Force Close Channel Result", content, true).into_string(),
         ))
         .unwrap())
 }

+ 5 - 2
crates/cdk-ldk-node/src/web/handlers/dashboard.rs

@@ -5,7 +5,7 @@ use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
 use maud::html;
 
 use crate::web::handlers::AppState;
-use crate::web::templates::{format_sats_as_btc, layout};
+use crate::web::templates::{format_sats_as_btc, is_node_running, layout_with_status};
 
 #[derive(Debug)]
 pub struct UsageMetrics {
@@ -272,5 +272,8 @@ pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, St
         }
     };
 
-    Ok(Html(layout("Dashboard", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("Dashboard", content, is_running).into_string(),
+    ))
 }

+ 10 - 6
crates/cdk-ldk-node/src/web/handlers/invoices.rs

@@ -10,7 +10,8 @@ use serde::Deserialize;
 use crate::web::handlers::utils::{deserialize_optional_f64, deserialize_optional_u32};
 use crate::web::handlers::AppState;
 use crate::web::templates::{
-    error_message, form_card, format_sats_as_btc, info_card, layout, success_message,
+    error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status,
+    success_message,
 };
 
 #[derive(Deserialize)]
@@ -30,7 +31,7 @@ pub struct CreateBolt12Form {
     expiry_seconds: Option<u32>,
 }
 
-pub async fn invoices_page(State(_state): State<AppState>) -> Result<Html<String>, StatusCode> {
+pub async fn invoices_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
     let content = html! {
         h2 style="text-align: center; margin-bottom: 3rem;" { "Invoices" }
         div class="grid" {
@@ -78,7 +79,10 @@ pub async fn invoices_page(State(_state): State<AppState>) -> Result<Html<String
         }
     };
 
-    Ok(Html(layout("Create Invoices", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("Create Invoices", content, is_running).into_string(),
+    ))
 }
 
 pub async fn post_create_bolt11(
@@ -122,7 +126,7 @@ pub async fn post_create_bolt11(
                     .status(StatusCode::BAD_REQUEST)
                     .header("content-type", "text/html")
                     .body(Body::from(
-                        layout("Create Invoice Error", content).into_string(),
+                        layout_with_status("Create Invoice Error", content, true).into_string(),
                     ))
                     .unwrap());
             }
@@ -193,7 +197,7 @@ pub async fn post_create_bolt11(
     Ok(Response::builder()
         .header("content-type", "text/html")
         .body(Body::from(
-            layout("BOLT11 Invoice Created", content).into_string(),
+            layout_with_status("BOLT11 Invoice Created", content, true).into_string(),
         ))
         .unwrap())
 }
@@ -287,7 +291,7 @@ pub async fn post_create_bolt12(
     Ok(Response::builder()
         .header("content-type", "text/html")
         .body(Body::from(
-            layout("BOLT12 Offer Created", content).into_string(),
+            layout_with_status("BOLT12 Offer Created", content, true).into_string(),
         ))
         .unwrap())
 }

+ 5 - 2
crates/cdk-ldk-node/src/web/handlers/lightning.rs

@@ -4,7 +4,7 @@ use axum::response::Html;
 use maud::html;
 
 use crate::web::handlers::utils::AppState;
-use crate::web::templates::{format_sats_as_btc, layout};
+use crate::web::templates::{format_sats_as_btc, is_node_running, layout_with_status};
 
 pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
     let balances = state.node.inner.list_balances();
@@ -216,5 +216,8 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
         }
     };
 
-    Ok(Html(layout("Lightning", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("Lightning", content, is_running).into_string(),
+    ))
 }

+ 86 - 118
crates/cdk-ldk-node/src/web/handlers/onchain.rs

@@ -13,7 +13,8 @@ use serde::{Deserialize, Serialize};
 use crate::web::handlers::utils::deserialize_optional_u64;
 use crate::web::handlers::AppState;
 use crate::web::templates::{
-    error_message, form_card, format_sats_as_btc, info_card, layout, success_message,
+    error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status,
+    success_message,
 };
 
 #[derive(Deserialize, Serialize)]
@@ -32,24 +33,54 @@ pub struct ConfirmOnchainForm {
     confirmed: Option<String>,
 }
 
+fn quick_actions_section() -> maud::Markup {
+    html! {
+        div class="card" style="margin-bottom: 2rem;" {
+            h2 { "Quick Actions" }
+            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" }
+                    }
+                }
+
+                // 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" }
+                    }
+                }
+            }
+        }
+    }
+}
+
 pub async fn get_new_address(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
     let address_result = state.node.inner.onchain_payment().new_address();
 
     let content = match address_result {
         Ok(address) => {
             html! {
-                (success_message(&format!("New address generated: {address}")))
                 div class="card" {
                     h2 { "Bitcoin Address" }
-                    div class="info-item" {
-                        span class="info-label" { "Address:" }
-                        span class="info-value" style="font-family: monospace; font-size: 0.9rem;" { (address.to_string()) }
+                    div class="address-display" {
+                        div class="address-container" {
+                            span class="address-text" { (address.to_string()) }
+                        }
                     }
                 }
                 div class="card" {
-                    a href="/onchain" { button { "← Back to On-chain" } }
-                    " "
-                    a href="/onchain/new-address" { button { "Generate Another Address" } }
+                    div style="display: flex; justify-content: space-between; gap: 1rem;" {
+                        a href="/onchain" { button class="button-secondary" { "Back" } }
+                        form method="post" action="/onchain/new-address" style="display: inline;" {
+                            button class="button-primary" type="submit" { "Generate Another Address" }
+                        }
+                    }
                 }
             }
         }
@@ -57,13 +88,16 @@ pub async fn get_new_address(State(state): State<AppState>) -> Result<Html<Strin
             html! {
                 (error_message(&format!("Failed to generate address: {e}")))
                 div class="card" {
-                    a href="/onchain" { button { "← Back to On-chain" } }
+                    a href="/onchain" { button class="button-primary" { "← Back to On-chain" } }
                 }
             }
         }
     };
 
-    Ok(Html(layout("New Address", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("New Address", content, is_running).into_string(),
+    ))
 }
 
 pub async fn onchain_page(
@@ -79,29 +113,8 @@ pub async fn onchain_page(
     let mut content = html! {
         h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
-        // Quick Actions section - individual cards
-        div class="card" style="margin-bottom: 2rem;" {
-            h2 { "Quick Actions" }
-            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" }
-                    }
-                }
-
-                // 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" }
-                    }
-                }
-            }
-        }
+        // Quick Actions section - only show on overview
+        (quick_actions_section())
 
         // On-chain Balance as metric cards
         div class="card" {
@@ -124,30 +137,6 @@ pub async fn onchain_page(
             content = html! {
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
-                // Quick Actions section - individual cards
-                div class="card" style="margin-bottom: 2rem;" {
-                    h2 { "Quick Actions" }
-                    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" }
-                            }
-                        }
-
-                        // 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" }
-                            }
-                        }
-                    }
-                }
-
                 // Send form above balance
                 (form_card(
                     "Send On-chain Payment",
@@ -193,30 +182,6 @@ pub async fn onchain_page(
             content = html! {
                 h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
 
-                // Quick Actions section - individual cards
-                div class="card" style="margin-bottom: 2rem;" {
-                    h2 { "Quick Actions" }
-                    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" }
-                            }
-                        }
-
-                        // 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" }
-                            }
-                        }
-                    }
-                }
-
                 // Generate address form above balance
                 (form_card(
                     "Generate New Address",
@@ -252,7 +217,10 @@ pub async fn onchain_page(
         }
     }
 
-    Ok(Html(layout("On-chain", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("On-chain", content, is_running).into_string(),
+    ))
 }
 
 pub async fn post_send_onchain(
@@ -294,7 +262,7 @@ pub async fn onchain_confirm_page(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Send On-chain Error", content).into_string(),
+                    layout_with_status("Send On-chain Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -320,7 +288,7 @@ pub async fn onchain_confirm_page(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Send On-chain Error", content).into_string(),
+                    layout_with_status("Send On-chain Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -345,46 +313,46 @@ pub async fn onchain_confirm_page(
     let content = html! {
         h2 style="text-align: center; margin-bottom: 3rem;" { "Confirm On-chain Transaction" }
 
-        div class="card" style="border: 2px solid hsl(var(--primary)); background-color: hsl(var(--primary) / 0.05);" {
-            h2 { "⚠️ Transaction Confirmation" }
-            p style="color: hsl(var(--muted-foreground)); margin-bottom: 1.5rem;" {
-                "Please review the transaction details carefully before proceeding. This action cannot be undone."
-            }
-        }
-
-        (info_card(
-            "Transaction Details",
-            vec![
-                ("Recipient Address", form.address.clone()),
-                ("Amount to Send", if is_send_all {
-                    format!("{} (All available funds)", format_sats_as_btc(amount_to_send))
-                } else {
-                    format_sats_as_btc(amount_to_send)
-                }),
-                ("Current Spendable Balance", format_sats_as_btc(spendable_balance)),
-            ]
-        ))
-
         @if is_send_all {
-            div class="card" style="border: 1px solid hsl(32.6 75.4% 55.1%); background-color: hsl(32.6 75.4% 55.1% / 0.1);" {
-                h3 style="color: hsl(32.6 75.4% 55.1%);" { "Send All Notice" }
-                p style="color: hsl(32.6 75.4% 55.1%);" {
-                    "This transaction will send all available funds to the recipient address. "
-                    "Network fees will be deducted from the total amount automatically."
+            div class="card send-all-notice" {
+                h3 { "Send All Notice" }
+                p {
+                    "This transaction will send all available funds to the recipient address. Network fees will be deducted from the total amount automatically."
                 }
             }
         }
 
+        // Transaction Details Card
         div class="card" {
-            div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
+            h2 { "Transaction Details" }
+            div class="transaction-details" {
+                div class="detail-row" {
+                    span class="detail-label" { "Recipient Address:" }
+                    span class="detail-value" { (form.address.clone()) }
+                }
+                div class="detail-row" {
+                    span class="detail-label" { "Amount to Send:" }
+                    span class="detail-value-amount" {
+                        (if is_send_all {
+                            format!("{} (All available funds)", format_sats_as_btc(amount_to_send))
+                        } else {
+                            format_sats_as_btc(amount_to_send)
+                        })
+                    }
+                }
+                div class="detail-row" {
+                    span class="detail-label" { "Current Spendable Balance:" }
+                    span class="detail-value-amount" { (format_sats_as_btc(spendable_balance)) }
+                }
+            }
+
+            div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid hsl(var(--border));" {
                 a href="/onchain?action=send" {
                     button type="button" class="button-secondary" { "Cancel" }
                 }
-                div style="display: flex; gap: 0.5rem;" {
-                    a href=(confirmation_url) {
-                        button class="button-primary" {
-                            "✓ Confirm & Send Transaction"
-                        }
+                a href=(confirmation_url) {
+                    button class="button-primary" {
+                        "Confirm"
                     }
                 }
             }
@@ -394,7 +362,7 @@ pub async fn onchain_confirm_page(
     Ok(Response::builder()
         .header("content-type", "text/html")
         .body(Body::from(
-            layout("Confirm Transaction", content).into_string(),
+            layout_with_status("Confirm Transaction", content, true).into_string(),
         ))
         .unwrap())
 }
@@ -427,7 +395,7 @@ async fn execute_onchain_transaction(
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
                 .body(Body::from(
-                    layout("Send On-chain Error", content).into_string(),
+                    layout_with_status("Send On-chain Error", content, true).into_string(),
                 ))
                 .unwrap());
         }
@@ -506,7 +474,7 @@ async fn execute_onchain_transaction(
     Ok(Response::builder()
         .header("content-type", "text/html")
         .body(Body::from(
-            layout("Send On-chain Result", content).into_string(),
+            layout_with_status("Send On-chain Result", content, true).into_string(),
         ))
         .unwrap())
 }

+ 32 - 18
crates/cdk-ldk-node/src/web/handlers/payments.rs

@@ -15,8 +15,8 @@ use serde::Deserialize;
 use crate::web::handlers::utils::{deserialize_optional_u64, get_paginated_payments_streaming};
 use crate::web::handlers::AppState;
 use crate::web::templates::{
-    error_message, form_card, format_msats_as_btc, format_sats_as_btc, info_card, layout,
-    payment_list_item, success_message,
+    error_message, form_card, format_msats_as_btc, format_sats_as_btc, info_card, is_node_running,
+    layout_with_status, payment_list_item, success_message,
 };
 
 #[derive(Deserialize)]
@@ -236,12 +236,13 @@ pub async fn payments_page(
         }
     };
 
-    Ok(Html(layout("Payment History", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("Payment History", content, is_running).into_string(),
+    ))
 }
 
-pub async fn send_payments_page(
-    State(_state): State<AppState>,
-) -> Result<Html<String>, StatusCode> {
+pub async fn send_payments_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
     let content = html! {
         h2 style="text-align: center; margin-bottom: 3rem;" { "Send Payment" }
         div class="grid" {
@@ -280,13 +281,12 @@ pub async fn send_payments_page(
             ))
         }
 
-        div class="card" {
-            h3 { "Payment History" }
-            a href="/payments" { button { "View All Payments" } }
-        }
     };
 
-    Ok(Html(layout("Send Payments", content).into_string()))
+    let is_running = is_node_running(&state.node.inner);
+    Ok(Html(
+        layout_with_status("Send Payments", content, is_running).into_string(),
+    ))
 }
 
 pub async fn post_pay_bolt11(
@@ -306,7 +306,9 @@ pub async fn post_pay_bolt11(
             return Ok(Response::builder()
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
-                .body(Body::from(layout("Payment Error", content).into_string()))
+                .body(Body::from(
+                    layout_with_status("Payment Error", content, true).into_string(),
+                ))
                 .unwrap());
         }
     };
@@ -348,7 +350,9 @@ pub async fn post_pay_bolt11(
             return Ok(Response::builder()
                 .status(StatusCode::INTERNAL_SERVER_ERROR)
                 .header("content-type", "text/html")
-                .body(Body::from(layout("Payment Error", content).into_string()))
+                .body(Body::from(
+                    layout_with_status("Payment Error", content, true).into_string(),
+                ))
                 .unwrap());
         }
     };
@@ -433,7 +437,9 @@ pub async fn post_pay_bolt11(
 
     Ok(Response::builder()
         .header("content-type", "text/html")
-        .body(Body::from(layout("Payment Result", content).into_string()))
+        .body(Body::from(
+            layout_with_status("Payment Result", content, true).into_string(),
+        ))
         .unwrap())
 }
 
@@ -454,7 +460,9 @@ pub async fn post_pay_bolt12(
             return Ok(Response::builder()
                 .status(StatusCode::BAD_REQUEST)
                 .header("content-type", "text/html")
-                .body(Body::from(layout("Payment Error", content).into_string()))
+                .body(Body::from(
+                    layout_with_status("Payment Error", content, true).into_string(),
+                ))
                 .unwrap());
         }
     };
@@ -486,7 +494,9 @@ pub async fn post_pay_bolt12(
                     return Ok(Response::builder()
                         .status(StatusCode::BAD_REQUEST)
                         .header("content-type", "text/html")
-                        .body(Body::from(layout("Payment Error", content).into_string()))
+                        .body(Body::from(
+                            layout_with_status("Payment Error", content, true).into_string(),
+                        ))
                         .unwrap());
                 }
             };
@@ -518,7 +528,9 @@ pub async fn post_pay_bolt12(
             return Ok(Response::builder()
                 .status(StatusCode::INTERNAL_SERVER_ERROR)
                 .header("content-type", "text/html")
-                .body(Body::from(layout("Payment Error", content).into_string()))
+                .body(Body::from(
+                    layout_with_status("Payment Error", content, true).into_string(),
+                ))
                 .unwrap());
         }
     };
@@ -610,6 +622,8 @@ pub async fn post_pay_bolt12(
 
     Ok(Response::builder()
         .header("content-type", "text/html")
-        .body(Body::from(layout("Payment Result", content).into_string()))
+        .body(Body::from(
+            layout_with_status("Payment Result", content, true).into_string(),
+        ))
         .unwrap())
 }

+ 365 - 23
crates/cdk-ldk-node/src/web/templates/layout.rs

@@ -1,6 +1,12 @@
+use ldk_node::Node;
 use maud::{html, Markup, DOCTYPE};
 
-pub fn layout(title: &str, content: Markup) -> Markup {
+/// Helper function to check if the node is running
+pub fn is_node_running(node: &Node) -> bool {
+    node.status().is_running
+}
+
+pub fn layout_with_status(title: &str, content: Markup, is_running: bool) -> Markup {
     html! {
         (DOCTYPE)
         html lang="en" {
@@ -252,12 +258,21 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                         box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
                     }
 
+                    .status-indicator.status-inactive {
+                        background-color: #ef4444;
+                        box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
+                    }
+
                     .status-text {
                         font-size: 0.875rem;
                         font-weight: 500;
                         color: #10b981;
                     }
 
+                    .status-text.status-inactive {
+                        color: #ef4444;
+                    }
+
                     .node-title {
                         font-size: 1.875rem;
                         font-weight: 600;
@@ -281,38 +296,104 @@ pub fn layout(title: &str, content: Markup) -> Markup {
 
                     /* Responsive header */
                     @media (max-width: 768px) {
+                        header {
+                            height: 180px; /* Slightly taller for better mobile layout */
+                            padding: 1rem 0;
+                        }
+
+                        header .container {
+                            padding: 0 1rem;
+                            height: 100%;
+                            display: flex;
+                            align-items: center;
+                            justify-content: center;
+                        }
+
                         .header-content {
                             flex-direction: column;
                             gap: 1rem;
                             text-align: center;
+                            width: 100%;
+                            justify-content: center;
                         }
 
                         .header-left {
                             flex-direction: column;
                             text-align: center;
+                            align-items: center;
+                            gap: 0.75rem;
+                        }
+
+                        .header-avatar {
+                            width: 64px;
+                            height: 64px;
+                            padding: 0.5rem;
+                        }
+
+                        .header-avatar-image {
+                            width: 40px;
+                            height: 40px;
                         }
 
                         .node-title {
                             font-size: 1.5rem;
                         }
+
+                        .node-subtitle {
+                            font-size: 0.8125rem;
+                            text-align: center;
+                        }
+
+                        .node-status {
+                            justify-content: center;
+                        }
                     }
 
+                    @media (max-width: 480px) {
+                        header {
+                            height: 160px;
+                        }
+
+                        .header-avatar {
+                            width: 56px;
+                            height: 56px;
+                            padding: 0.375rem;
+                        }
+
+                        .header-avatar-image {
+                            width: 36px;
+                            height: 36px;
+                        }
+
+                        .node-title {
+                            font-size: 1.25rem;
+                        }
+
+                        .node-subtitle {
+                            font-size: 0.75rem;
+                        }
+                    }
+
+                    /* Dark mode navigation styles */
+                    @media (prefers-color-scheme: dark) {
                         nav a {
                             color: var(--text-muted) !important;
                         }
 
                         nav a:hover {
                             color: var(--text-secondary) !important;
-                            background-color: rgba(255, 255, 255, 0.05) !important;
+                            background-color: rgba(255, 255, 255, 0.08) !important;
+                            transform: translateY(-1px) !important;
                         }
 
                         nav a.active {
                             color: var(--text-primary) !important;
-                            background-color: rgba(255, 255, 255, 0.08) !important;
+                            background-color: rgba(255, 255, 255, 0.1) !important;
                         }
 
                         nav a.active:hover {
-                            background-color: rgba(255, 255, 255, 0.1) !important;
+                            background-color: rgba(255, 255, 255, 0.12) !important;
+                            transform: translateY(-1px) !important;
                         }
                     }
 
@@ -409,23 +490,6 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                         line-height: 1.6;
                     }
 
-                    @media (max-width: 768px) {
-                        header {
-                            height: 150px; /* Smaller height on mobile */
-                        }
-
-                        header .container {
-                            padding: 0 1rem;
-                        }
-
-                        h1 {
-                            font-size: 2.25rem;
-                        }
-
-                        .subtitle {
-                            font-size: 1.1rem;
-                        }
-                    }
 
                     /* Card fade-in animation */
                     @keyframes fade-in {
@@ -491,6 +555,15 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                         background-color: hsl(var(--muted));
                     }
 
+                    /* Light mode navigation hover states */
+                    @media (prefers-color-scheme: light) {
+                        nav a:hover {
+                            color: hsl(var(--foreground));
+                            background-color: hsl(var(--muted) / 0.8);
+                            transform: translateY(-1px);
+                        }
+                    }
+
                     nav a.active {
                         color: hsl(var(--primary-foreground));
                         background-color: hsl(var(--primary));
@@ -587,6 +660,20 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                         width: 100%;
                     }
 
+                    /* Dark mode input field improvements */
+                    @media (prefers-color-scheme: dark) {
+                        input, textarea, select {
+                            background-color: hsl(0 0% 8%);
+                            border: 1px solid hsl(0 0% 20%);
+                            color: hsl(var(--foreground));
+                        }
+
+                        input:focus, textarea:focus, select:focus {
+                            background-color: hsl(0 0% 10%);
+                            border-color: hsl(var(--ring));
+                        }
+                    }
+
                     input:focus, textarea:focus, select:focus {
                         outline: 2px solid transparent;
                         outline-offset: 2px;
@@ -599,6 +686,74 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                         opacity: 0.5;
                     }
 
+                    /* Subtle pagination dropdown styling */
+                    .per-page-selector {
+                        display: flex;
+                        align-items: center;
+                        gap: 0.5rem;
+                        margin: 1rem 0 0 0;
+                        padding: 0;
+                        background-color: transparent;
+                        border: none;
+                        border-radius: 0;
+                        font-size: 0.875rem;
+                    }
+
+                    .per-page-selector label {
+                        color: hsl(var(--muted-foreground));
+                        font-weight: 500;
+                    }
+
+                    .per-page-selector select {
+                        background-color: transparent;
+                        border: 1px solid hsl(var(--muted));
+                        border-radius: calc(var(--radius) - 2px);
+                        padding: 0.25rem 0.5rem;
+                        font-size: 0.875rem;
+                        color: hsl(var(--muted-foreground));
+                        min-width: 50px;
+                        cursor: pointer;
+                        transition: all 0.2s ease;
+                        flex: none;
+                        width: auto;
+                    }
+
+                    .per-page-selector select:hover {
+                        border-color: hsl(var(--ring));
+                        background-color: hsl(var(--muted) / 0.5);
+                    }
+
+                    .per-page-selector select:focus {
+                        outline: 2px solid transparent;
+                        outline-offset: 2px;
+                        border-color: hsl(var(--ring));
+                        box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2);
+                    }
+
+                    .per-page-selector span {
+                        color: hsl(var(--muted-foreground));
+                        font-weight: 500;
+                    }
+
+                    /* Form actions layout */
+                    .form-actions {
+                        display: flex;
+                        justify-content: space-between;
+                        align-items: center;
+                        gap: 1rem;
+                        margin-top: 1.5rem;
+                        padding-top: 1rem;
+                        border-top: 1px solid hsl(var(--border));
+                    }
+
+                    .form-actions .button-secondary {
+                        order: 1;
+                    }
+
+                    .form-actions .button-primary {
+                        order: 2;
+                    }
+
                     button {
                         display: inline-flex;
                         align-items: center;
@@ -1082,6 +1237,14 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                         margin-bottom: 1.5rem;
                     }
 
+                    /* Dark mode payment card improvements - match other cards */
+                    @media (prefers-color-scheme: dark) {
+                        .payment-item {
+                            background-color: rgba(255, 255, 255, 0.03) !important;
+                            border: none !important;
+                        }
+                    }
+
                     .payment-header {
                         display: flex;
                         justify-content: space-between;
@@ -1266,6 +1429,39 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                         border: 1px solid hsl(32 95% 44% / 0.2);
                     }
 
+                    /* Dark mode payment type badge improvements */
+                    @media (prefers-color-scheme: dark) {
+                        .payment-type-onchain {
+                            background-color: hsl(32 95% 60% / 0.15);
+                            color: hsl(32 95% 70%);
+                            border: 1px solid hsl(32 95% 60% / 0.3);
+                        }
+
+                        .payment-type-bolt11 {
+                            background-color: hsl(217 91% 70% / 0.15);
+                            color: hsl(217 91% 80%);
+                            border: 1px solid hsl(217 91% 70% / 0.3);
+                        }
+
+                        .payment-type-bolt12 {
+                            background-color: hsl(262 83% 70% / 0.15);
+                            color: hsl(262 83% 80%);
+                            border: 1px solid hsl(262 83% 70% / 0.3);
+                        }
+
+                        .payment-type-spontaneous {
+                            background-color: hsl(142.1 70.6% 60% / 0.15);
+                            color: hsl(142.1 70.6% 75%);
+                            border: 1px solid hsl(142.1 70.6% 60% / 0.3);
+                        }
+
+                        .payment-type-bolt11-jit {
+                            background-color: hsl(199 89% 65% / 0.15);
+                            color: hsl(199 89% 80%);
+                            border: 1px solid hsl(199 89% 65% / 0.3);
+                        }
+                    }
+
                     .payment-type-spontaneous {
                         background-color: hsl(142.1 70.6% 45.3% / 0.1);
                         color: hsl(142.1 70.6% 45.3%);
@@ -1635,6 +1831,147 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                             fill: rgba(156, 163, 175, 0.2);
                         }
                     }
+
+                    /* Address display styling */
+                    .address-display {
+                        margin: 1.5rem 0;
+                    }
+
+                    .address-container {
+                        padding: 1rem 0;
+                    }
+
+                    .address-text {
+                        font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
+                        font-size: 1.25rem;
+                        font-weight: 500;
+                        color: hsl(var(--foreground));
+                        word-break: break-all;
+                        overflow-wrap: break-word;
+                        hyphens: auto;
+                        flex: 1;
+                        min-width: 0;
+                        line-height: 1.4;
+                        background-color: transparent;
+                        border: none;
+                        padding: 0;
+                    }
+
+
+                    /* Dark mode address styling */
+                    @media (prefers-color-scheme: dark) {
+                        .address-text {
+                            color: var(--text-primary) !important;
+                        }
+                    }
+
+                    /* Responsive address display */
+                    @media (max-width: 640px) {
+                        .address-text {
+                            font-size: 1.125rem;
+                            text-align: center;
+                        }
+                    }
+
+                    /* Transaction confirmation styling */
+                    .transaction-details {
+                        margin-top: 1rem;
+                    }
+
+                    .transaction-details .detail-row {
+                        display: flex;
+                        align-items: baseline;
+                        margin-bottom: 1rem;
+                        gap: 1rem;
+                        padding: 0.75rem 0;
+                        border-bottom: 1px solid hsl(var(--border));
+                    }
+
+                    .transaction-details .detail-row:last-child {
+                        border-bottom: none;
+                        margin-bottom: 0;
+                    }
+
+                    .transaction-details .detail-label {
+                        font-weight: 500;
+                        color: hsl(var(--muted-foreground));
+                        font-size: 0.875rem;
+                        min-width: 180px;
+                        flex-shrink: 0;
+                    }
+
+                    .transaction-details .detail-value {
+                        color: hsl(var(--foreground));
+                        font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
+                        font-size: 0.875rem;
+                        word-break: break-all;
+                        flex: 1;
+                        min-width: 0;
+                    }
+
+                    .transaction-details .detail-value-amount {
+                        color: hsl(var(--foreground));
+                        font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
+                        font-size: 1rem;
+                        font-weight: 600;
+                        flex: 1;
+                        min-width: 0;
+                    }
+
+                    .send-all-notice {
+                        border: 1px solid hsl(32.6 75.4% 55.1%);
+                        background-color: hsl(32.6 75.4% 55.1% / 0.1);
+                    }
+
+                    .send-all-notice h3 {
+                        color: hsl(32.6 75.4% 55.1%);
+                        font-size: 1rem;
+                        font-weight: 600;
+                        margin-bottom: 0.5rem;
+                    }
+
+                    .send-all-notice p {
+                        color: hsl(32.6 75.4% 55.1%);
+                        font-size: 0.875rem;
+                        line-height: 1.4;
+                        margin: 0;
+                    }
+
+                    /* Dark mode transaction styling */
+                    @media (prefers-color-scheme: dark) {
+                        .transaction-details .detail-label {
+                            color: var(--text-muted) !important;
+                        }
+
+                        .transaction-details .detail-value,
+                        .transaction-details .detail-value-amount {
+                            color: var(--text-primary) !important;
+                        }
+
+                        .send-all-notice {
+                            background-color: hsl(32.6 75.4% 55.1% / 0.15) !important;
+                            border-color: hsl(32.6 75.4% 55.1% / 0.3) !important;
+                        }
+                    }
+
+                    /* Responsive transaction details */
+                    @media (max-width: 640px) {
+                        .transaction-details .detail-row {
+                            flex-direction: column;
+                            align-items: flex-start;
+                            gap: 0.5rem;
+                        }
+
+                        .transaction-details .detail-label {
+                            min-width: auto;
+                            font-size: 0.8125rem;
+                        }
+
+                        .transaction-details .detail-value,
+                        .transaction-details .detail-value-amount {
+                            font-size: 0.875rem;
+                        }
+                    }
                     "
                 }
             }
@@ -1648,8 +1985,13 @@ pub fn layout(title: &str, content: Markup) -> Markup {
                                 }
                                 div class="node-info" {
                                     div class="node-status" {
-                                        span class="status-indicator status-running" {}
-                                        span class="status-text" { "Running" }
+                                        @if is_running {
+                                            span class="status-indicator" {}
+                                            span class="status-text" { "Running" }
+                                        } @else {
+                                            span class="status-indicator status-inactive" {}
+                                            span class="status-text status-inactive" { "Inactive" }
+                                        }
                                     }
                                     h1 class="node-title" { "CDK LDK Node" }
                                     span class="node-subtitle" { "Cashu Mint & Lightning Network Node Management" }

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


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