cdk-ldk web ui updates (#1027)

This commit is contained in:
Erik
2025-09-03 16:46:49 +02:00
committed by GitHub
parent 734e62b04a
commit 39f256a648
7 changed files with 840 additions and 335 deletions

View File

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

View File

@@ -213,7 +213,7 @@ pub async fn post_open_channel(
}
pub async fn close_channel_page(
State(_state): State<AppState>,
State(state): State<AppState>,
query: Query<HashMap<String, String>>,
) -> Result<Html<String>, StatusCode> {
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()));
}
// 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(
"Close Channel",
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) }
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)) }
}
}
}
div class="info-item" {
span class="info-label" { "Node ID:" }
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
}
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="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(
State(_state): State<AppState>,
State(state): State<AppState>,
query: Query<HashMap<String, String>>,
) -> Result<Html<String>, StatusCode> {
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(
"Force Close Channel",
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 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."
}
}
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) }
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)) }
}
}
}
div class="info-item" {
span class="info-label" { "Node ID:" }
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
}
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="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" }
}
},
);

View File

@@ -3,7 +3,7 @@ use axum::http::StatusCode;
use axum::response::Html;
use maud::html;
use crate::web::handlers::AppState;
use crate::web::handlers::utils::AppState;
use crate::web::templates::{format_sats_as_btc, layout};
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! {
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;" {
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! {
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;" {
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 channel in &channels {
div class="channel-item" {
div class="channel-header" {
span class="channel-id" { "Channel ID: " (channel.channel_id.to_string()) }
// Channels list
@for (index, channel) in channels.iter().enumerate() {
@let node_id = channel.counterparty_node_id.to_string();
@let channel_number = index + 1;
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 {
span class="status-badge status-active" { "Active" }
} @else {
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" }
}
}
}
}
}
}
};

View File

@@ -79,15 +79,26 @@ pub async fn onchain_page(
let mut content = html! {
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;" {
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! {
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;" {
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" {}
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;" {
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" }
@@ -171,15 +193,26 @@ pub async fn onchain_page(
content = html! {
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;" {
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" {
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;" {
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" }
}
}
@@ -345,7 +378,7 @@ pub async fn onchain_confirm_page(
div class="card" {
div style="display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem;" {
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;" {
a href=(confirmation_url) {

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 43 KiB