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

@@ -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"

View File

@@ -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?" } p style="margin-bottom: 1.5rem;" { "Are you sure you want to close this channel?" }
div class="info-item" {
span class="info-label" { "User Channel ID:" } // Channel details in consistent format
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel_id) } 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:" } form method="post" action="/channels/close" style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;" {
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (node_id) }
}
form method="post" action="/channels/close" style="margin-top: 1rem;" {
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" class="button-secondary" { "Cancel" } }
" " button type="submit" class="button-destructive" { "Close Channel" }
a href="/balance" { button type="button" { "Cancel" } }
} }
}, },
); );
@@ -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;" { 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: #d63384; margin: 0 0 0.5rem 0;" { "⚠️ Warning: Force Close" } h4 style="color: #f97316; margin: 0 0 0.5rem 0;" { "⚠️ Warning: Force Close" }
p style="color: #d63384; margin: 0; font-size: 0.9rem;" { 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?" } p style="margin-bottom: 1.5rem;" { "Are you sure you want to force close this channel?" }
div class="info-item" {
span class="info-label" { "User Channel ID:" } // Channel details in consistent format
span class="info-value" style="font-family: monospace; font-size: 0.85rem;" { (channel_id) } 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:" } form method="post" action="/channels/force-close" style="margin-top: 1rem; display: flex; justify-content: space-between; align-items: center;" {
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;" {
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" class="button-secondary" { "Cancel" } }
" " button type="submit" class="button-destructive" { "Force Close Channel" }
a href="/balance" { button type="button" { "Cancel" } }
} }
}, },
); );

View File

@@ -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;" { div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" { // Open Channel Card
button class="button-primary" style="width: 100%;" { "Open Channel" } 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;" { div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
a href="/channels/open" style="text-decoration: none; flex: 1; min-width: 200px;" { // Open Channel Card
button class="button-primary" style="width: 100%;" { "Open Channel" } 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" { // Channel Details header (outside card)
h2 { "Channel Details" } h2 class="section-header" { "Channel Details" }
// Channels list // Channels list
@for channel in &channels { @for (index, channel) in channels.iter().enumerate() {
div class="channel-item" { @let node_id = channel.counterparty_node_id.to_string();
div class="channel-header" { @let channel_number = index + 1;
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="balance-item" {
div class="info-item" { div class="balance-amount" { (format_sats_as_btc(channel.inbound_capacity_msat / 1000)) }
span class="info-label" { "Short Channel ID" } div class="balance-label" { "Inbound" }
span class="info-value" { (short_channel_id.to_string()) }
}
} }
div class="balance-info" { div class="balance-item" {
div class="balance-item" { div class="balance-amount" { (format_sats_as_btc(channel.channel_value_sats)) }
div class="balance-amount" { (format_sats_as_btc(channel.outbound_capacity_msat / 1000)) } div class="balance-label" { "Total" }
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" }
}
} }
@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)) { // Action buttons
button style="background: #dc3545;" { "Close Channel" } @if channel.is_usable {
} div class="channel-actions" {
a href=(format!("/channels/force-close?channel_id={}&node_id={}", channel.user_channel_id.0, channel.counterparty_node_id)) { a href=(format!("/channels/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" } 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! { 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;" { div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" { // Receive Bitcoin Card
button class="button-primary" style="width: 100%;" { "Receive Bitcoin" } 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;" { div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" { // Receive Bitcoin Card
button class="button-primary" style="width: 100%;" { "Receive Bitcoin" } 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;" { div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
a href="/onchain?action=receive" style="text-decoration: none; flex: 1; min-width: 200px;" { // Receive Bitcoin Card
button class="button-primary" style="width: 100%;" { "Receive Bitcoin" } 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) {

View File

@@ -58,36 +58,261 @@ pub fn layout(title: &str, content: Markup) -> Markup {
--header-subtitle: #333333; --header-subtitle: #333333;
} }
/* Dark mode using system preference */ /* Dark mode using system preference */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { body {
--background: 222.2 84% 4.9%; background: linear-gradient(rgb(23, 25, 29), rgb(18, 19, 21));
--foreground: 210 40% 98%; }
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
/* Dark mode colors */ :root {
--fg-primary: #f8fafc; --background: 0 0% 0%;
--fg-muted: #94a3b8; --foreground: 0 0% 100%;
--card: 0 0% 0%;
--card-foreground: 0 0% 100%;
--popover: 0 0% 0%;
--popover-foreground: 0 0% 100%;
--primary: 0 0% 100%;
--primary-foreground: 0 0% 0%;
--secondary: 0 0% 20%;
--secondary-foreground: 0 0% 100%;
--muted: 0 0% 20%;
--muted-foreground: 0 0% 70%;
--accent: 0 0% 20%;
--accent-foreground: 0 0% 100%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 20%;
--input: 0 0% 20%;
--ring: 0 0% 83.9%;
/* Dark mode text hierarchy colors */
--text-primary: #ffffff;
--text-secondary: #e6e6e6;
--text-tertiary: #cccccc;
--text-quaternary: #b3b3b3;
--text-muted: #999999;
--text-muted-2: #888888;
--text-muted-3: #666666;
--text-muted-4: #333333;
--text-subtle: #1a1a1a;
/* Header text colors for dark mode */ /* Header text colors for dark mode */
--header-title: #ffffff; --header-title: #ffffff;
--header-subtitle: #e2e8f0; --header-subtitle: #e6e6e6;
}
/* Dark mode box styling - no borders, subtle background */
.card {
background-color: rgba(255, 255, 255, 0.03) !important;
border: none !important;
}
.channel-box {
background-color: rgba(255, 255, 255, 0.03) !important;
border: none !important;
}
.metric-card {
background-color: rgba(255, 255, 255, 0.03) !important;
border: none !important;
}
.balance-item {
background-color: rgba(255, 255, 255, 0.03) !important;
border: none !important;
}
.node-info-main-container {
background-color: rgba(255, 255, 255, 0.03) !important;
border: none !important;
}
.node-avatar {
background-color: rgba(255, 255, 255, 0.03) !important;
border: none !important;
}
/* Text hierarchy colors */
.section-header {
color: var(--text-primary) !important;
}
.channel-alias {
color: var(--text-primary) !important;
}
.detail-label {
color: var(--text-muted) !important;
}
.detail-value, .detail-value-amount {
color: var(--text-secondary) !important;
}
.metric-label, .balance-label {
color: var(--text-muted) !important;
}
.metric-value, .balance-amount {
color: var(--text-primary) !important;
}
/* Page headers and section titles */
h1, h2, h3, h4, h5, h6 {
color: var(--text-primary) !important;
}
/* Form card titles */
.form-card h2, .form-card h3 {
color: var(--text-primary) !important;
}
/* Quick action cards styling */
.quick-action-card {
background-color: rgba(255, 255, 255, 0.03) !important;
border: none !important;
border-radius: 0.75rem !important;
padding: 1.5rem !important;
}
/* Dark mode outline button styling */
.button-outline {
background-color: transparent !important;
color: var(--text-primary) !important;
border: 1px solid var(--text-muted) !important;
}
.button-outline:hover {
background-color: rgba(255, 255, 255, 0.2) !important;
}
/* Navigation dark mode styling */
nav {
background-color: transparent !important;
border-top: none !important;
border-bottom: none !important;
}
}
/* New Header Layout Styles */
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.header-avatar {
flex-shrink: 0;
background-color: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
padding: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
}
.header-avatar-image {
width: 48px;
height: 48px;
border-radius: calc(var(--radius) - 2px);
object-fit: cover;
display: block;
}
.node-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding-top: 0;
margin-top: 0;
}
.node-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-indicator {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
background-color: #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.status-text {
font-size: 0.875rem;
font-weight: 500;
color: #10b981;
}
.node-title {
font-size: 1.875rem;
font-weight: 600;
color: var(--header-title);
margin: 0;
line-height: 1.1;
}
.node-subtitle {
font-size: 0.875rem;
color: var(--header-subtitle);
font-weight: 500;
}
.header-right {
display: flex;
align-items: center;
}
/* Responsive header */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.header-left {
flex-direction: column;
text-align: center;
}
.node-title {
font-size: 1.5rem;
}
}
nav a {
color: var(--text-muted) !important;
}
nav a:hover {
color: var(--text-secondary) !important;
background-color: rgba(255, 255, 255, 0.05) !important;
}
nav a.active {
color: var(--text-primary) !important;
background-color: rgba(255, 255, 255, 0.08) !important;
}
nav a.active:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
} }
} }
@@ -135,13 +360,13 @@ pub fn layout(title: &str, content: Markup) -> Markup {
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
border-bottom: 1px solid hsl(var(--border)); border-bottom: 1px solid hsl(var(--border));
margin-bottom: 3rem; margin-bottom: 2rem;
text-align: center; text-align: left;
width: 100%; width: 100%;
height: 400px; /* Fixed height for better proportion */ height: 200px; /* Reduced height for more compact header */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
} }
/* Dark mode header background - using different image */ /* Dark mode header background - using different image */
@@ -153,14 +378,17 @@ pub fn layout(title: &str, content: Markup) -> Markup {
/* Ensure text is positioned properly */ /* Ensure text is positioned properly */
header .container { header .container {
position: absolute; position: relative;
top: 50%; top: auto;
left: 50%; left: auto;
transform: translate(-50%, -50%); transform: none;
z-index: 2; z-index: 2;
width: 100%; width: 100%;
max-width: 1200px; max-width: 1200px;
padding: 0 2rem; padding: 0 2rem;
display: flex;
align-items: center;
justify-content: flex-start;
} }
h1 { h1 {
@@ -183,7 +411,7 @@ pub fn layout(title: &str, content: Markup) -> Markup {
@media (max-width: 768px) { @media (max-width: 768px) {
header { header {
height: 300px; /* Smaller height on mobile */ height: 150px; /* Smaller height on mobile */
} }
header .container { header .container {
@@ -424,14 +652,17 @@ pub fn layout(title: &str, content: Markup) -> Markup {
} }
.button-destructive { .button-destructive {
background-color: hsl(var(--destructive)); background-color: transparent !important;
color: hsl(var(--destructive-foreground)); color: #DC2626 !important;
border: 1px solid #DC2626 !important;
} }
.button-destructive:hover { .button-destructive:hover {
background-color: hsl(var(--destructive) / 0.9); background-color: rgba(220, 38, 38, 0.2) !important;
} }
.button-sm { .button-sm {
height: 2rem; height: 2rem;
border-radius: calc(var(--radius) - 4px); border-radius: calc(var(--radius) - 4px);
@@ -694,7 +925,7 @@ pub fn layout(title: &str, content: Markup) -> Markup {
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
} }
/* Legacy status classes */ /* Status badge classes - consistent with payment type badges */
.status-badge { .status-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -706,16 +937,24 @@ pub fn layout(title: &str, content: Markup) -> Markup {
} }
.status-active { .status-active {
background-color: hsl(142.1 70.6% 45.3%); background-color: hsl(142.1 70.6% 45.3% / 0.1);
color: hsl(355.7 78% 98.4%); color: hsl(142.1 70.6% 45.3%);
border: 1px solid hsl(142.1 70.6% 45.3% / 0.2);
} }
.status-inactive { .status-inactive {
background-color: hsl(var(--destructive)); background-color: hsl(0 84.2% 60.2% / 0.1);
color: hsl(var(--destructive-foreground)); color: hsl(0 84.2% 60.2%);
border: 1px solid hsl(0 84.2% 60.2% / 0.2);
} }
.channel-item { .status-pending {
background-color: hsl(215.4 16.3% 46.9% / 0.1);
color: hsl(215.4 16.3% 46.9%);
border: 1px solid hsl(215.4 16.3% 46.9% / 0.2);
}
.channel-box {
background-color: hsl(var(--card)); background-color: hsl(var(--card));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: var(--radius); border-radius: var(--radius);
@@ -723,23 +962,87 @@ pub fn layout(title: &str, content: Markup) -> Markup {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.channel-header { .section-header {
display: flex; font-size: 1.25rem;
justify-content: space-between; font-weight: 700;
align-items: center; color: hsl(var(--foreground));
margin-bottom: 1.5rem;
line-height: 1.2;
}
.channel-alias {
font-size: 1.25rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 1rem; margin-bottom: 1rem;
line-height: 1.2;
}
.channel-details {
margin-bottom: 1.5rem;
}
.detail-row {
display: flex;
align-items: baseline;
margin-bottom: 0.75rem;
gap: 1rem; gap: 1rem;
} }
.channel-id { .detail-row:last-child {
margin-bottom: 0;
}
.detail-label {
font-weight: 500;
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
min-width: 120px;
flex-shrink: 0;
}
.detail-value {
color: hsl(var(--foreground));
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
font-size: 0.875rem; font-size: 0.875rem;
color: hsl(var(--muted-foreground));
word-break: break-all; word-break: break-all;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.detail-value-amount {
color: hsl(var(--foreground));
font-size: 0.875rem;
word-break: break-all;
flex: 1;
min-width: 0;
}
.channel-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
gap: 1rem;
}
@media (max-width: 640px) {
.channel-actions {
flex-direction: column;
align-items: stretch;
}
.detail-row {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.detail-label {
min-width: auto;
}
}
.balance-info { .balance-info {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
@@ -908,6 +1211,30 @@ pub fn layout(title: &str, content: Markup) -> Markup {
border-color: hsl(var(--primary)); border-color: hsl(var(--primary));
} }
/* Dark mode specific styling for payment filter tabs */
@media (prefers-color-scheme: dark) {
.payment-filter-tab {
background-color: rgba(255, 255, 255, 0.03) !important;
border-color: var(--text-muted) !important;
color: var(--text-muted) !important;
}
.payment-filter-tab:hover {
background-color: rgba(255, 255, 255, 0.08) !important;
color: var(--text-secondary) !important;
}
.payment-filter-tab.active {
background-color: rgba(255, 255, 255, 0.12) !important;
color: var(--text-primary) !important;
border-color: var(--text-secondary) !important;
}
.payment-filter-tab.active:hover {
background-color: rgba(255, 255, 255, 0.15) !important;
}
}
.payment-type-badge { .payment-type-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -1086,7 +1413,7 @@ pub fn layout(title: &str, content: Markup) -> Markup {
display: flex; display: flex;
gap: 1.5rem; gap: 1.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
align-items: flex-start; align-items: stretch;
} }
.node-info-main-container { .node-info-main-container {
@@ -1099,6 +1426,7 @@ pub fn layout(title: &str, content: Markup) -> Markup {
border-radius: var(--radius); border-radius: var(--radius);
padding: 1.5rem; padding: 1.5rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
height: 100%;
} }
.node-info-left { .node-info-left {
@@ -1170,6 +1498,7 @@ pub fn layout(title: &str, content: Markup) -> Markup {
width: 280px; width: 280px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-self: stretch;
} }
.node-metrics .card { .node-metrics .card {
@@ -1177,16 +1506,28 @@ pub fn layout(title: &str, content: Markup) -> Markup {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-self: stretch;
} }
.node-metrics .metrics-container { .node-metrics .metrics-container {
flex-direction: column; flex-direction: column;
margin: 1rem 0 0 0; margin: 1rem 0 0 0;
flex: 1; flex: 1;
display: flex;
justify-content: flex-start;
gap: 1rem;
align-items: stretch;
} }
.node-metrics .metric-card { .node-metrics .metric-card {
min-width: auto; min-width: auto;
padding: 1rem;
height: fit-content;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
} }
/* Mobile responsive design for node info */ /* Mobile responsive design for node info */
@@ -1300,19 +1641,70 @@ pub fn layout(title: &str, content: Markup) -> Markup {
body { body {
header { header {
div class="container" { div class="container" {
h1 { "CDK LDK Node" } div class="header-content" {
p class="subtitle" { "Lightning Network Node Management" } div class="header-left" {
div class="header-avatar" {
img src="/static/images/nut.png" alt="CDK LDK Node Icon" class="header-avatar-image";
}
div class="node-info" {
div class="node-status" {
span class="status-indicator status-running" {}
span class="status-text" { "Running" }
}
h1 class="node-title" { "CDK LDK Node" }
span class="node-subtitle" { "Cashu Mint & Lightning Network Node Management" }
}
}
div class="header-right" {
// Right side content can be added here later if needed
}
}
} }
} }
nav { nav {
div class="container" { div class="container" {
ul { ul {
li { a href="/" { "Dashboard" } } li {
li { a href="/balance" { "Lightning" } } a href="/" {
li { a href="/onchain" { "On-chain" } } svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 0.5rem;" {
li { a href="/invoices" { "Invoices" } } path d="M15.6 2.7a10 10 0 1 0 5.7 5.7" {}
li { a href="/payments" { "All Payments" } } circle cx="12" cy="12" r="2" {}
path d="M13.4 10.6 19 5" {}
}
"Dashboard"
}
}
li {
a href="/balance" {
svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 0.5rem;" {
path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" {}
}
"Lightning"
}
}
li {
a href="/onchain" {
svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 0.5rem;" {
path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" {}
path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" {}
}
"On-chain"
}
}
li {
a href="/payments" {
svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 0.5rem;" {
path d="M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5" {}
path d="m16 19 3 3 3-3" {}
path d="M18 12h.01" {}
path d="M19 16v6" {}
path d="M6 12h.01" {}
circle cx="12" cy="12" r="2" {}
}
"All Payments"
}
}
} }
} }
} }
@@ -1320,6 +1712,8 @@ pub fn layout(title: &str, content: Markup) -> Markup {
main class="container" { main class="container" {
(content) (content)
} }
} }
} }
} }

View File

@@ -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",
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 43 KiB