feat: bitcoin-ui CSS fix, HTTPS proxy support, deploy script improvements

Bitcoin UI:
- Replace cdn.tailwindcss.com with locally bundled tailwind.css (CSP blocks external scripts)
- Make all asset paths relative for nginx proxy compatibility
- Add bitcoin-ui build/deploy to deploy-to-target.sh (was missing entirely)
- Use --network host (bitcoin-ui proxies Bitcoin RPC at 127.0.0.1:8332)

HTTPS mixed content fix:
- Add HTTPS_PROXY_PATHS in AppSession.vue — when parent page is HTTPS,
  iframe loads through nginx proxy instead of direct HTTP port
- Prevents browser blocking HTTP iframes inside HTTPS pages
- All Tailscale servers use HTTPS, this was breaking all app iframes

Deploy & first-boot improvements:
- first-boot-containers.sh auto-detects disk size for pruning vs txindex
- first-boot-containers.sh checks fallback source path for UI containers
- Added mempool-electrs to APP_PORTS mapping
- ElectrumX container creation in first-boot
- Podman doctor/fix/uptime skills added

Also includes: session persistence, identity management, LND transactions,
ElectrumX status UI, nostr-provider improvements, Web5 enhancements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-16 12:58:35 +00:00
co-authored by Claude Opus 4.6
parent 4e54b8bd4d
commit 367b483a72
49 changed files with 6180 additions and 495 deletions
+98
View File
@@ -48,6 +48,28 @@ pub struct IdentityRecord {
pub nostr_pubkey: Option<String>,
/// Nostr public key in bech32 npub format (NIP-19)
pub nostr_npub: Option<String>,
/// Nostr profile metadata (NIP-01 kind 0)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<IdentityProfile>,
}
/// Nostr profile metadata fields (NIP-01 kind 0 + NIP-24 extra fields).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IdentityProfile {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub about: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub picture: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub banner: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub website: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nip05: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lud16: Option<String>,
}
/// On-disk format for identity storage (includes secret key bytes).
@@ -64,6 +86,9 @@ struct IdentityFile {
nostr_secret_hex: Option<String>,
#[serde(default)]
nostr_pubkey_hex: Option<String>,
/// Nostr profile metadata
#[serde(default)]
profile: Option<IdentityProfile>,
}
pub struct IdentityManager {
@@ -123,6 +148,7 @@ impl IdentityManager {
created_at: created_at.clone(),
nostr_secret_hex: None,
nostr_pubkey_hex: None,
profile: None,
};
let file_path = self.identities_dir.join(format!("{}.json", id));
@@ -345,6 +371,77 @@ impl IdentityManager {
.context("NIP-44 decryption failed")
}
/// Update the profile metadata for an identity.
pub async fn update_profile(&self, id: &str, profile: IdentityProfile) -> Result<()> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
let mut file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
file.profile = Some(profile);
let json = serde_json::to_string_pretty(&file).context("Failed to serialize identity")?;
fs::write(&file_path, json.as_bytes()).await.context("Failed to write identity file")?;
Ok(())
}
/// Publish kind 0 (metadata) event to a Nostr relay.
pub async fn publish_profile(&self, id: &str, relay_url: &str) -> Result<String> {
let record = self.get(id).await?;
let keys = self.load_nostr_keys(id).await?;
let profile = record.profile.unwrap_or_default();
// Build kind 0 content JSON (NIP-01 + NIP-24)
let mut content = serde_json::Map::new();
content.insert("name".to_string(), serde_json::json!(record.name));
if let Some(v) = &profile.display_name { content.insert("display_name".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.about { content.insert("about".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.picture { content.insert("picture".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.banner { content.insert("banner".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.website { content.insert("website".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.nip05 { content.insert("nip05".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.lud16 { content.insert("lud16".to_string(), serde_json::json!(v)); }
let content_str = serde_json::to_string(&content).context("Failed to serialize profile content")?;
let client = nostr_sdk::Client::new(keys);
client.add_relay(relay_url).await.context("Failed to add relay")?;
client.connect().await;
let builder = nostr_sdk::prelude::EventBuilder::new(
nostr_sdk::prelude::Kind::Metadata,
&content_str,
);
let output = client.send_event_builder(builder).await.context("Failed to publish profile")?;
client.disconnect().await;
Ok(output.id().to_hex())
}
/// Export all keys for an identity (SENSITIVE — only call after password verification).
pub async fn export_keys(&self, id: &str) -> Result<serde_json::Value> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
let file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
let ed25519_secret_hex = hex::encode(&file.secret_key);
let nostr_nsec = file.nostr_secret_hex.as_ref().and_then(|h| {
nostr_sdk::SecretKey::from_hex(h)
.ok()
.and_then(|sk| sk.to_bech32().ok())
});
Ok(serde_json::json!({
"ed25519_secret_hex": ed25519_secret_hex,
"nostr_secret_hex": file.nostr_secret_hex,
"nostr_nsec": nostr_nsec,
}))
}
// --- internal helpers ---
}
@@ -395,6 +492,7 @@ impl IdentityManager {
created_at: file.created_at,
nostr_pubkey: file.nostr_pubkey_hex,
nostr_npub,
profile: file.profile,
})
}