release(v1.7.37-alpha): bitcoin-core install fixes + dynamic node UI + full-archive default
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
Install flow - api/rpc/package/install.rs: always append the literal image URL as a last-resort pull candidate in do_pull_image, so images not carried by any configured mirror (docker.io/bitcoin/bitcoin:28.4) still install instead of masquerading as a generic pull failure across every mirror. - api/rpc/package/install.rs: write_bitcoin_conf now skips on any stat error, not just "file exists". Once bitcoin-knots' first-boot chowns /var/lib/archipelago/bitcoin into the container's user namespace (700 perms, UID 100100/100101), the archipelago daemon can't even traverse in — try_exists returns Err which unwrap_or(false) treated as "not present" and drove a doomed write. Now errors out of the directory traversal are treated as "conf already owned by container user" and the write is skipped. Mirrors the lnd.conf pattern. - api/rpc/package/install.rs: drop the hardcoded `prune=550` from the conf default. Operators with multi-TB drives shouldn't be silently pruned; users who want a pruned node can set it in bitcoin.conf themselves. Full archive is the only honest default. - api/rpc/package/config.rs: bitcoin-core now passes explicit -server/-rpcbind/-rpcallowip/-rpcport/-printtoconsole/-datadir CLI args. Vanilla bitcoin/bitcoin:28.4 has no entrypoint wrapper and reads conf + argv only; without these the RPC listens on 127.0.0.1 inside the container and rootlessport can't reach it, so the bitcoin-ui companion gets 502 on every /bitcoin-rpc/ call. Bitcoin Knots keeps its own entrypoint-driven defaults. - container/docker_packages.rs: split bitcoin-core out of the shared AppMetadata arm. bitcoin-core now surfaces as "Bitcoin Core" with bitcoin-core.svg and a Reference-implementation description; the bitcoin + bitcoin-knots ids keep the Knots branding. Fixes the home card showing "Bitcoin Knots" for a Core install. Bitcoin node UI (docker/bitcoin-ui) - index.html: impl name/tagline/logo now dynamic. applyImplBranding() reads subversion from getnetworkinfo — /Satoshi:X/Knots:Y/ resolves to Bitcoin Knots, plain /Satoshi:X/ resolves to Bitcoin Core. Both get their own icon and subtitle. Settings modal replaced its hardcoded Regtest/txindex=1/port-18443 placeholders with live values from getblockchaininfo + getindexinfo + getzmqnotifications. - index.html: new Storage info card (Full Archive · X GB / Pruned · X GB from blockchainInfo.pruned + size_on_disk) visible on the main dashboard, same level as Network. Settings modal mirrors it with the prune height when applicable. - Dockerfile + assets/: bitcoin-core.svg, bitcoin-knots.webp, and the bg-network.jpg used by the dashboard are now COPY'd into the image under /usr/share/nginx/html/assets. Previously the <img src> pointed at paths that 404'd into the SPA fallback and the onerror handler hid the broken logo silently. Frontend - appSession/appSessionConfig.ts: add bitcoin-core to APP_PORTS (8334), HTTPS_PROXY_PATHS (/app/bitcoin-ui/), and APP_TITLES (Bitcoin Core). Without these the AppSessionFrame showed "No URL found for bitcoin-core" and the home/app-list title fell through to the raw id. - settings/AccountInfoSection.vue: backfill What's New entries for v1.7.31 through v1.7.37 that had been missed in earlier cuts. Release plumbing - releases/v1.7.37-alpha/: binary + frontend tarball. - releases/manifest.json: v1.7.37-alpha, sha256/size refreshed. - Cargo.toml / package.json: version bumps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1f912a0f58
commit
9cb114c50a
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.36-alpha"
|
version = "1.7.37-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.36-alpha"
|
version = "1.7.37-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
authors = ["Archipelago Team"]
|
authors = ["Archipelago Team"]
|
||||||
|
|||||||
@ -483,7 +483,30 @@ pub(super) async fn get_app_config(
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => (
|
"bitcoin-core" => (
|
||||||
|
vec![
|
||||||
|
"8332:8332".to_string(),
|
||||||
|
"8333:8333".to_string(),
|
||||||
|
"28332:28332".to_string(),
|
||||||
|
"28333:28333".to_string(),
|
||||||
|
],
|
||||||
|
vec!["/var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin".to_string()],
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
// Vanilla bitcoin/bitcoin image has no entrypoint wrapper and reads
|
||||||
|
// only what's in bitcoin.conf + argv. The shared bitcoin.conf
|
||||||
|
// carries rpcauth; we inject the networking flags as CLI args so
|
||||||
|
// RPC is reachable from the bitcoin-ui companion container.
|
||||||
|
Some(vec![
|
||||||
|
"-server=1".to_string(),
|
||||||
|
"-rpcbind=0.0.0.0".to_string(),
|
||||||
|
"-rpcallowip=0.0.0.0/0".to_string(),
|
||||||
|
"-rpcport=8332".to_string(),
|
||||||
|
"-printtoconsole=1".to_string(),
|
||||||
|
"-datadir=/home/bitcoin/.bitcoin".to_string(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
"bitcoin" | "bitcoin-knots" => (
|
||||||
vec![
|
vec![
|
||||||
"8332:8332".to_string(),
|
"8332:8332".to_string(),
|
||||||
"8333:8333".to_string(),
|
"8333:8333".to_string(),
|
||||||
|
|||||||
@ -728,8 +728,11 @@ impl RpcHandler {
|
|||||||
candidates.push((url, reg.tls_verify));
|
candidates.push((url, reg.tls_verify));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If no registries are configured, fall back to the literal URL.
|
// Always include the literal URL as a last-resort candidate —
|
||||||
if candidates.is_empty() {
|
// internal mirrors may not host every third-party upstream image
|
||||||
|
// (e.g. docker.io/bitcoin/bitcoin:28.4), and we don't want
|
||||||
|
// "app not mirrored" to masquerade as a generic pull failure.
|
||||||
|
if tried.insert(docker_image.to_string()) {
|
||||||
candidates.push((docker_image.to_string(), true));
|
candidates.push((docker_image.to_string(), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -873,6 +876,24 @@ impl RpcHandler {
|
|||||||
let bitcoin_dir = "/var/lib/archipelago/bitcoin";
|
let bitcoin_dir = "/var/lib/archipelago/bitcoin";
|
||||||
let conf_path = format!("{}/bitcoin.conf", bitcoin_dir);
|
let conf_path = format!("{}/bitcoin.conf", bitcoin_dir);
|
||||||
|
|
||||||
|
// Idempotent: once bitcoin-knots (or a prior install) has started,
|
||||||
|
// the data dir is chowned into the container's user namespace
|
||||||
|
// (e.g. UID 100100 on the host) with 700 perms — the archipelago
|
||||||
|
// daemon can no longer stat or write there. Treat any non-NotFound
|
||||||
|
// error on the conf as "conf already provisioned by the container
|
||||||
|
// user" and skip. Matches the lnd.conf behavior below.
|
||||||
|
match tokio::fs::metadata(&conf_path).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("bitcoin.conf already exists, skipping write");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(_) => {
|
||||||
|
info!("bitcoin.conf path inaccessible (container-owned data dir), skipping write");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
let salt_bytes: [u8; 16] = rand::random();
|
let salt_bytes: [u8; 16] = rand::random();
|
||||||
@ -883,12 +904,14 @@ impl RpcHandler {
|
|||||||
let hash_hex = hex::encode(mac.finalize().into_bytes());
|
let hash_hex = hex::encode(mac.finalize().into_bytes());
|
||||||
let rpcauth_line = format!("rpcauth={}:{}${}", rpc_user, salt_hex, hash_hex);
|
let rpcauth_line = format!("rpcauth={}:{}${}", rpc_user, salt_hex, hash_hex);
|
||||||
|
|
||||||
|
// Default to full archive — operators with 2TB+ drives shouldn't be
|
||||||
|
// silently pruned down to 550 MB. Users who want a pruned node can
|
||||||
|
// set `prune=N` in bitcoin.conf themselves after install.
|
||||||
let bitcoin_conf = format!(
|
let bitcoin_conf = format!(
|
||||||
"\
|
"\
|
||||||
# rpcauth: salted hash only — no plaintext password in config or CLI\n\
|
# rpcauth: salted hash only — no plaintext password in config or CLI\n\
|
||||||
{}\n\
|
{}\n\
|
||||||
server=1\n\
|
server=1\n\
|
||||||
prune=550\n\
|
|
||||||
rpcbind=0.0.0.0\n\
|
rpcbind=0.0.0.0\n\
|
||||||
rpcallowip=0.0.0.0/0\n\
|
rpcallowip=0.0.0.0/0\n\
|
||||||
rpcport=8332\n\
|
rpcport=8332\n\
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
//! Bootstrap host-side doctor artifacts on every archipelago startup.
|
//! Bootstrap host-side artifacts on every archipelago startup.
|
||||||
//!
|
//!
|
||||||
//! The update pipeline swaps the archipelago binary but does not touch
|
//! The update pipeline swaps the archipelago binary but does not touch
|
||||||
//! scripts or systemd units — those are installed once by the ISO builder.
|
//! scripts, systemd units, or nginx configuration — those are installed
|
||||||
//! Without this module, changes to `container-doctor.sh` or the doctor
|
//! once by the ISO builder. Without this module, changes to
|
||||||
//! service/timer never reach boxes installed before the change.
|
//! `container-doctor.sh`, the doctor service/timer, or the nginx config
|
||||||
|
//! never reach boxes installed before the change.
|
||||||
//!
|
//!
|
||||||
//! On startup we compare three embedded files against their on-disk
|
//! Two things are synced on startup:
|
||||||
//! copies and rewrite any that differ, then enable the doctor timer if
|
//! 1. Doctor artifacts (container-doctor.sh + service + timer).
|
||||||
//! it isn't already. Idempotent: no-ops on boxes that match the
|
//! 2. An nginx `location /api/app-catalog` proxy block — required for
|
||||||
//! embedded version. All work is best-effort — failures are logged but
|
//! the App Store catalog proxy to actually reach the backend.
|
||||||
//! never abort the backend.
|
//!
|
||||||
|
//! Idempotent: no-ops on boxes that are already in sync. All work is
|
||||||
|
//! best-effort — failures are logged but never abort the backend.
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@ -28,14 +31,26 @@ const DOCTOR_SH_PATH: &str = "/home/archipelago/archy/scripts/container-doctor.s
|
|||||||
const DOCTOR_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-doctor.service";
|
const DOCTOR_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-doctor.service";
|
||||||
const DOCTOR_TIMER_PATH: &str = "/etc/systemd/system/archipelago-doctor.timer";
|
const DOCTOR_TIMER_PATH: &str = "/etc/systemd/system/archipelago-doctor.timer";
|
||||||
|
|
||||||
|
const NGINX_CONF_PATH: &str = "/etc/nginx/sites-available/archipelago";
|
||||||
|
|
||||||
|
/// Inserted into every server block of the nginx config that lacks the
|
||||||
|
/// `/api/app-catalog` proxy. Kept in sync with the canonical block in
|
||||||
|
/// image-recipe/configs/nginx-archipelago.conf.
|
||||||
|
const NGINX_APP_CATALOG_BLOCK: &str = "\n # App Store catalog proxy — backend fetches from configured registries\n # so the browser doesn't hit CORS/CSP. Without this block nginx falls\n # through to the SPA index.html and the frontend gets HTML back instead\n # of JSON.\n location /api/app-catalog {\n proxy_pass http://127.0.0.1:5678;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header Cookie $http_cookie;\n proxy_connect_timeout 15s;\n proxy_read_timeout 30s;\n proxy_send_timeout 15s;\n error_page 502 503 = @backend_unavailable;\n error_page 504 = @backend_timeout;\n }\n\n";
|
||||||
|
|
||||||
/// Entry point called from main startup. Never returns an error to the caller —
|
/// Entry point called from main startup. Never returns an error to the caller —
|
||||||
/// failing to bootstrap the doctor must not prevent the backend from serving.
|
/// failing to bootstrap host artifacts must not prevent the backend from serving.
|
||||||
pub async fn ensure_doctor_installed() {
|
pub async fn ensure_doctor_installed() {
|
||||||
match run().await {
|
match run().await {
|
||||||
Ok(changed) if changed => info!("Doctor artifacts synchronized with binary"),
|
Ok(changed) if changed => info!("Doctor artifacts synchronized with binary"),
|
||||||
Ok(_) => debug!("Doctor artifacts already in sync"),
|
Ok(_) => debug!("Doctor artifacts already in sync"),
|
||||||
Err(e) => warn!("Doctor bootstrap failed (non-fatal): {:#}", e),
|
Err(e) => warn!("Doctor bootstrap failed (non-fatal): {:#}", e),
|
||||||
}
|
}
|
||||||
|
match run_nginx().await {
|
||||||
|
Ok(true) => info!("Patched nginx config to proxy /api/app-catalog"),
|
||||||
|
Ok(false) => debug!("Nginx already has /api/app-catalog block"),
|
||||||
|
Err(e) => warn!("Nginx bootstrap failed (non-fatal): {:#}", e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run() -> Result<bool> {
|
async fn run() -> Result<bool> {
|
||||||
@ -150,3 +165,100 @@ async fn is_timer_enabled() -> bool {
|
|||||||
.map(|s| s.success())
|
.map(|s| s.success())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Patch the nginx site config to add a `/api/app-catalog` proxy block if
|
||||||
|
/// it's missing. The original ISO shipped individual per-endpoint `location`
|
||||||
|
/// blocks and no catch-all `/api/`, so `/api/app-catalog` silently fell
|
||||||
|
/// through to the SPA `index.html` and the frontend got HTML instead of
|
||||||
|
/// JSON. We anchor the insert to the DWN comment that already sits right
|
||||||
|
/// after the `/api/blob` block, so the new block lands in both the HTTP
|
||||||
|
/// and HTTPS server blocks.
|
||||||
|
///
|
||||||
|
/// Validates via `nginx -t` before reloading. On failure the patch is
|
||||||
|
/// rolled back from a backup written just before the write.
|
||||||
|
async fn run_nginx() -> Result<bool> {
|
||||||
|
// Skip on dev symlinks — we don't want to touch `/etc/nginx` on laptops.
|
||||||
|
let home_archy = Path::new("/home/archipelago/archy");
|
||||||
|
if fs::symlink_metadata(home_archy)
|
||||||
|
.await
|
||||||
|
.map(|m| m.file_type().is_symlink())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Path::new(NGINX_CONF_PATH).exists() {
|
||||||
|
debug!(
|
||||||
|
"{} missing — skipping nginx bootstrap",
|
||||||
|
NGINX_CONF_PATH
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(NGINX_CONF_PATH)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("read {}", NGINX_CONF_PATH))?;
|
||||||
|
if content.contains("location /api/app-catalog") {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The DWN comment sits at the same indent right after the `/api/blob`
|
||||||
|
// block in both server blocks — a stable anchor that existed on every
|
||||||
|
// ISO shipped to date. If it's absent (config got heavily customized),
|
||||||
|
// we bail rather than guess where to splice.
|
||||||
|
let anchor = " # DWN endpoints — peer access over Tor (no auth)";
|
||||||
|
if !content.contains(anchor) {
|
||||||
|
warn!("nginx conf missing DWN anchor — skipping /api/app-catalog patch");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let replacement = format!("{}{}", NGINX_APP_CATALOG_BLOCK, anchor);
|
||||||
|
let patched = content.replace(anchor, &replacement);
|
||||||
|
|
||||||
|
// Write patched config via a user-owned tmp + sudo mv, after stashing
|
||||||
|
// a backup so we can revert if `nginx -t` hates what we produced.
|
||||||
|
let pid = std::process::id();
|
||||||
|
let tmp = format!("/tmp/archipelago-nginx-{}.conf", pid);
|
||||||
|
fs::write(&tmp, &patched)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("write {}", tmp))?;
|
||||||
|
|
||||||
|
let backup = format!("/tmp/archipelago-nginx-backup-{}.conf", pid);
|
||||||
|
if let Err(e) = host_sudo(&["cp", NGINX_CONF_PATH, &backup]).await {
|
||||||
|
let _ = fs::remove_file(&tmp).await;
|
||||||
|
return Err(e.context("backup nginx conf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mv = host_sudo(&["mv", &tmp, NGINX_CONF_PATH]).await;
|
||||||
|
match mv {
|
||||||
|
Ok(s) if s.success() => {}
|
||||||
|
Ok(s) => {
|
||||||
|
let _ = fs::remove_file(&tmp).await;
|
||||||
|
anyhow::bail!("sudo mv nginx conf exited with {}", s);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = fs::remove_file(&tmp).await;
|
||||||
|
return Err(e.context("mv tmp -> nginx conf"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate.
|
||||||
|
let test = host_sudo(&["nginx", "-t"]).await;
|
||||||
|
let valid = matches!(&test, Ok(s) if s.success());
|
||||||
|
if !valid {
|
||||||
|
warn!("nginx -t failed after patch — reverting");
|
||||||
|
let _ = host_sudo(&["mv", &backup, NGINX_CONF_PATH]).await;
|
||||||
|
if let Err(e) = test {
|
||||||
|
return Err(e.context("nginx -t"));
|
||||||
|
}
|
||||||
|
anyhow::bail!("nginx config invalid after patch — reverted");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload nginx so the new block takes effect immediately. Reload (not
|
||||||
|
// restart) keeps in-flight connections alive.
|
||||||
|
if let Err(e) = host_sudo(&["systemctl", "reload", "nginx"]).await {
|
||||||
|
warn!("nginx reload failed (non-fatal): {:#}", e);
|
||||||
|
}
|
||||||
|
let _ = host_sudo(&["rm", "-f", &backup]).await;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|||||||
@ -297,9 +297,16 @@ fn get_app_tier(app_id: &str) -> &'static str {
|
|||||||
|
|
||||||
fn get_app_metadata(app_id: &str) -> AppMetadata {
|
fn get_app_metadata(app_id: &str) -> AppMetadata {
|
||||||
let mut meta = match app_id {
|
let mut meta = match app_id {
|
||||||
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => AppMetadata {
|
"bitcoin-core" => AppMetadata {
|
||||||
|
title: "Bitcoin Core".to_string(),
|
||||||
|
description: "Reference Bitcoin node implementation".to_string(),
|
||||||
|
icon: "/assets/img/app-icons/bitcoin-core.svg".to_string(),
|
||||||
|
repo: "https://github.com/bitcoin/bitcoin".to_string(),
|
||||||
|
tier: "",
|
||||||
|
},
|
||||||
|
"bitcoin" | "bitcoin-knots" => AppMetadata {
|
||||||
title: "Bitcoin Knots".to_string(),
|
title: "Bitcoin Knots".to_string(),
|
||||||
description: "Full Bitcoin node implementation".to_string(),
|
description: "Enhanced Bitcoin node implementation".to_string(),
|
||||||
icon: "/assets/img/app-icons/bitcoin-knots.webp".to_string(),
|
icon: "/assets/img/app-icons/bitcoin-knots.webp".to_string(),
|
||||||
repo: "https://github.com/bitcoinknots/bitcoin".to_string(),
|
repo: "https://github.com/bitcoinknots/bitcoin".to_string(),
|
||||||
tier: "",
|
tier: "",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
FROM git.tx1138.com/lfg2025/nginx:1.27.4-alpine
|
FROM git.tx1138.com/lfg2025/nginx:1.27.4-alpine
|
||||||
COPY index.html /usr/share/nginx/html/
|
COPY index.html /usr/share/nginx/html/
|
||||||
COPY 50x.html /usr/share/nginx/html/
|
COPY 50x.html /usr/share/nginx/html/
|
||||||
|
COPY assets/ /usr/share/nginx/html/assets/
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
# Run nginx as root to avoid chown failures in rootless Podman user namespaces
|
# Run nginx as root to avoid chown failures in rootless Podman user namespaces
|
||||||
RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \
|
RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \
|
||||||
|
|||||||
14
docker/bitcoin-ui/assets/img/app-icons/bitcoin-core.svg
Normal file
14
docker/bitcoin-ui/assets/img/app-icons/bitcoin-core.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
BIN
docker/bitcoin-ui/assets/img/app-icons/bitcoin-knots.webp
Normal file
BIN
docker/bitcoin-ui/assets/img/app-icons/bitcoin-knots.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docker/bitcoin-ui/assets/img/bg-network.jpg
Normal file
BIN
docker/bitcoin-ui/assets/img/bg-network.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 976 KiB |
@ -6,7 +6,7 @@
|
|||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
<meta http-equiv="Pragma" content="no-cache">
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
<meta http-equiv="Expires" content="0">
|
<meta http-equiv="Expires" content="0">
|
||||||
<title>Bitcoin Knots - Archipelago</title>
|
<title id="pageTitle">Bitcoin Node - Archipelago</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
@ -336,9 +336,10 @@
|
|||||||
<!-- Logo - Top Left -->
|
<!-- Logo - Top Left -->
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<div class="logo-gradient-border">
|
<div class="logo-gradient-border">
|
||||||
<img
|
<img
|
||||||
|
id="implLogo"
|
||||||
src="/assets/img/app-icons/bitcoin-knots.webp"
|
src="/assets/img/app-icons/bitcoin-knots.webp"
|
||||||
alt="Bitcoin Knots"
|
alt="Bitcoin Node"
|
||||||
class="w-16 h-16"
|
class="w-16 h-16"
|
||||||
style="object-fit: contain;"
|
style="object-fit: contain;"
|
||||||
onerror="this.style.display='none'"
|
onerror="this.style.display='none'"
|
||||||
@ -348,8 +349,8 @@
|
|||||||
|
|
||||||
<!-- Title and Description -->
|
<!-- Title and Description -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h1 class="text-3xl font-bold text-white mb-2">Bitcoin Knots</h1>
|
<h1 id="implName" class="text-3xl font-bold text-white mb-2">Bitcoin Node</h1>
|
||||||
<p class="text-white/70">Enhanced Bitcoin node implementation</p>
|
<p id="implTagline" class="text-white/70">Detecting implementation…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Node Status Info - Compact on Desktop -->
|
<!-- Node Status Info - Compact on Desktop -->
|
||||||
@ -385,8 +386,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div class="info-card flex items-center gap-3">
|
||||||
onclick="openSettings()"
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2 1.6 3 4 3h8c2.4 0 4-1 4-3V7M4 7c0-2 1.6-3 4-3h8c2.4 0 4 1 4 3M4 7h16M9 11h6M9 15h6" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-white/60">Storage</p>
|
||||||
|
<p class="text-sm font-medium text-white" id="storageMode">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick="openSettings()"
|
||||||
class="px-4 py-3 glass-button rounded-lg text-sm font-medium"
|
class="px-4 py-3 glass-button rounded-lg text-sm font-medium"
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
@ -556,19 +567,23 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="p-3 bg-white/5 rounded-lg">
|
<div class="p-3 bg-white/5 rounded-lg">
|
||||||
<div class="font-semibold text-white mb-1">Network Mode</div>
|
<div class="font-semibold text-white mb-1">Network Mode</div>
|
||||||
<div class="text-white/70 text-sm">Regtest (Development)</div>
|
<div class="text-white/70 text-sm" id="settingsNetworkMode">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-white/5 rounded-lg">
|
||||||
|
<div class="font-semibold text-white mb-1">Storage Mode</div>
|
||||||
|
<div class="text-white/70 text-sm" id="settingsStorageMode">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 bg-white/5 rounded-lg">
|
<div class="p-3 bg-white/5 rounded-lg">
|
||||||
<div class="font-semibold text-white mb-1">Transaction Index</div>
|
<div class="font-semibold text-white mb-1">Transaction Index</div>
|
||||||
<div class="text-white/70 text-sm">Enabled (txindex=1)</div>
|
<div class="text-white/70 text-sm" id="settingsTxIndex">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 bg-white/5 rounded-lg">
|
<div class="p-3 bg-white/5 rounded-lg">
|
||||||
<div class="font-semibold text-white mb-1">ZMQ Publishing</div>
|
<div class="font-semibold text-white mb-1">ZMQ Publishing</div>
|
||||||
<div class="text-white/70 text-sm">Block & TX notifications enabled</div>
|
<div class="text-white/70 text-sm" id="settingsZmq">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 bg-white/5 rounded-lg">
|
<div class="p-3 bg-white/5 rounded-lg">
|
||||||
<div class="font-semibold text-white mb-1">RPC Access</div>
|
<div class="font-semibold text-white mb-1">RPC Access</div>
|
||||||
<div class="text-white/70 text-sm">Enabled on 0.0.0.0:18443</div>
|
<div class="text-white/70 text-sm" id="settingsRpc">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -630,6 +645,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implementation branding — detected from getnetworkinfo.subversion.
|
||||||
|
// Bitcoin Knots identifies as "/Satoshi:<ver>/Knots:<date>/", Bitcoin Core as "/Satoshi:<ver>/".
|
||||||
|
let brandingApplied = false;
|
||||||
|
function applyImplBranding(subversion) {
|
||||||
|
if (brandingApplied) return;
|
||||||
|
if (!subversion) return;
|
||||||
|
const isKnots = /Knots/i.test(subversion);
|
||||||
|
const name = isKnots ? 'Bitcoin Knots' : 'Bitcoin Core';
|
||||||
|
const tagline = isKnots
|
||||||
|
? 'Enhanced Bitcoin node implementation'
|
||||||
|
: 'Reference Bitcoin node implementation';
|
||||||
|
const icon = isKnots
|
||||||
|
? '/assets/img/app-icons/bitcoin-knots.webp'
|
||||||
|
: '/assets/img/app-icons/bitcoin-core.svg';
|
||||||
|
const pageTitle = document.getElementById('pageTitle');
|
||||||
|
const implName = document.getElementById('implName');
|
||||||
|
const implTagline = document.getElementById('implTagline');
|
||||||
|
const implLogo = document.getElementById('implLogo');
|
||||||
|
if (pageTitle) pageTitle.textContent = `${name} - Archipelago`;
|
||||||
|
if (implName) implName.textContent = name;
|
||||||
|
if (implTagline) implTagline.textContent = tagline;
|
||||||
|
if (implLogo) { implLogo.src = icon; implLogo.alt = name; }
|
||||||
|
brandingApplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Track last block count for animations
|
// Track last block count for animations
|
||||||
let lastBlockCount = 0;
|
let lastBlockCount = 0;
|
||||||
|
|
||||||
@ -648,7 +688,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const networkInfo = await callRPC('getnetworkinfo');
|
const networkInfo = await callRPC('getnetworkinfo');
|
||||||
|
|
||||||
|
applyImplBranding(networkInfo && networkInfo.subversion);
|
||||||
|
|
||||||
// Update network mode
|
// Update network mode
|
||||||
const chain = blockchainInfo.chain || 'unknown';
|
const chain = blockchainInfo.chain || 'unknown';
|
||||||
const networkType = document.getElementById('networkType');
|
const networkType = document.getElementById('networkType');
|
||||||
@ -666,6 +708,70 @@
|
|||||||
|
|
||||||
if (networkType) networkType.textContent = networkShort;
|
if (networkType) networkType.textContent = networkShort;
|
||||||
|
|
||||||
|
// Mirror to Settings modal — Network Mode
|
||||||
|
const settingsNetworkMode = document.getElementById('settingsNetworkMode');
|
||||||
|
if (settingsNetworkMode) {
|
||||||
|
const labels = { main: 'Mainnet', test: 'Testnet', signet: 'Signet', regtest: 'Regtest (Development)' };
|
||||||
|
settingsNetworkMode.textContent = labels[chain] || networkShort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update storage mode (pruned vs full archive)
|
||||||
|
const storageMode = document.getElementById('storageMode');
|
||||||
|
if (storageMode) {
|
||||||
|
const sizeGb = blockchainInfo.size_on_disk
|
||||||
|
? (blockchainInfo.size_on_disk / 1e9).toFixed(1) + ' GB'
|
||||||
|
: null;
|
||||||
|
if (blockchainInfo.pruned) {
|
||||||
|
storageMode.textContent = sizeGb ? `Pruned · ${sizeGb}` : 'Pruned';
|
||||||
|
storageMode.className = 'text-sm font-medium text-amber-300';
|
||||||
|
} else {
|
||||||
|
storageMode.textContent = sizeGb ? `Full Archive · ${sizeGb}` : 'Full Archive';
|
||||||
|
storageMode.className = 'text-sm font-medium text-emerald-300';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror to Settings modal — Storage Mode
|
||||||
|
const settingsStorageMode = document.getElementById('settingsStorageMode');
|
||||||
|
if (settingsStorageMode) {
|
||||||
|
if (blockchainInfo.pruned) {
|
||||||
|
const heightNote = blockchainInfo.prune_height != null
|
||||||
|
? ` (keeping from block ${blockchainInfo.prune_height.toLocaleString()})` : '';
|
||||||
|
settingsStorageMode.textContent = `Pruned${heightNote}`;
|
||||||
|
} else {
|
||||||
|
settingsStorageMode.textContent = 'Full archive (no pruning)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate Settings — Transaction Index, ZMQ, RPC (fire-and-forget)
|
||||||
|
(async () => {
|
||||||
|
const txIndexEl = document.getElementById('settingsTxIndex');
|
||||||
|
if (txIndexEl) {
|
||||||
|
const idx = await callRPC('getindexinfo');
|
||||||
|
if (idx && typeof idx === 'object') {
|
||||||
|
const names = Object.keys(idx);
|
||||||
|
txIndexEl.textContent = names.length
|
||||||
|
? `Enabled: ${names.join(', ')}`
|
||||||
|
: 'Disabled';
|
||||||
|
} else {
|
||||||
|
txIndexEl.textContent = 'Disabled';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const zmqEl = document.getElementById('settingsZmq');
|
||||||
|
if (zmqEl) {
|
||||||
|
const zmq = await callRPC('getzmqnotifications');
|
||||||
|
if (Array.isArray(zmq) && zmq.length) {
|
||||||
|
zmqEl.textContent = zmq.map(z => `${z.type}@${z.address}`).join('; ');
|
||||||
|
} else {
|
||||||
|
zmqEl.textContent = 'Not enabled';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rpcEl = document.getElementById('settingsRpc');
|
||||||
|
if (rpcEl && networkInfo) {
|
||||||
|
const port = chain === 'main' ? 8332 : (chain === 'test' ? 18332 : (chain === 'signet' ? 38332 : 18443));
|
||||||
|
rpcEl.textContent = `Reachable on port ${port}`;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Update sync status
|
// Update sync status
|
||||||
const blocks = blockchainInfo.blocks || 0;
|
const blocks = blockchainInfo.blocks || 0;
|
||||||
const headers = blockchainInfo.headers || 0;
|
const headers = blockchainInfo.headers || 0;
|
||||||
@ -779,7 +885,9 @@
|
|||||||
const peerInfo = await callRPC('getpeerinfo');
|
const peerInfo = await callRPC('getpeerinfo');
|
||||||
|
|
||||||
if (networkInfo && blockchainInfo) {
|
if (networkInfo && blockchainInfo) {
|
||||||
logsContent.textContent = `Bitcoin Knots version ${networkInfo.subversion || 'unknown'}
|
applyImplBranding(networkInfo.subversion);
|
||||||
|
const implLabel = /Knots/i.test(networkInfo.subversion || '') ? 'Bitcoin Knots' : 'Bitcoin Core';
|
||||||
|
logsContent.textContent = `${implLabel} version ${networkInfo.subversion || 'unknown'}
|
||||||
Network: ${blockchainInfo.chain}
|
Network: ${blockchainInfo.chain}
|
||||||
Blocks: ${blockchainInfo.blocks}
|
Blocks: ${blockchainInfo.blocks}
|
||||||
Headers: ${blockchainInfo.headers}
|
Headers: ${blockchainInfo.headers}
|
||||||
|
|||||||
@ -241,6 +241,23 @@ server {
|
|||||||
error_page 504 = @backend_timeout;
|
error_page 504 = @backend_timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# App Store catalog proxy — backend fetches from configured registries
|
||||||
|
# so the browser doesn't hit CORS/CSP. Without this block nginx falls
|
||||||
|
# through to the SPA index.html and the frontend gets HTML back instead
|
||||||
|
# of JSON.
|
||||||
|
location /api/app-catalog {
|
||||||
|
proxy_pass http://127.0.0.1:5678;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Cookie $http_cookie;
|
||||||
|
proxy_connect_timeout 15s;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
proxy_send_timeout 15s;
|
||||||
|
error_page 502 503 = @backend_unavailable;
|
||||||
|
error_page 504 = @backend_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
# DWN endpoints — peer access over Tor (no auth)
|
# DWN endpoints — peer access over Tor (no auth)
|
||||||
location /dwn {
|
location /dwn {
|
||||||
limit_req zone=peer burst=20 nodelay;
|
limit_req zone=peer burst=20 nodelay;
|
||||||
@ -1029,6 +1046,23 @@ server {
|
|||||||
error_page 504 = @backend_timeout;
|
error_page 504 = @backend_timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# App Store catalog proxy — backend fetches from configured registries
|
||||||
|
# so the browser doesn't hit CORS/CSP. Without this block nginx falls
|
||||||
|
# through to the SPA index.html and the frontend gets HTML back instead
|
||||||
|
# of JSON.
|
||||||
|
location /api/app-catalog {
|
||||||
|
proxy_pass http://127.0.0.1:5678;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Cookie $http_cookie;
|
||||||
|
proxy_connect_timeout 15s;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
proxy_send_timeout 15s;
|
||||||
|
error_page 502 503 = @backend_unavailable;
|
||||||
|
error_page 504 = @backend_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
# DWN endpoints — peer access over Tor (no auth)
|
# DWN endpoints — peer access over Tor (no auth)
|
||||||
location /dwn {
|
location /dwn {
|
||||||
limit_req zone=peer burst=20 nodelay;
|
limit_req zone=peer burst=20 nodelay;
|
||||||
|
|||||||
4
neode-ui/package-lock.json
generated
4
neode-ui/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.36-alpha",
|
"version": "1.7.37-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.36-alpha",
|
"version": "1.7.37-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.36-alpha",
|
"version": "1.7.37-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export const DISPLAY_MODE_KEY = 'archipelago_app_display_mode'
|
|||||||
/** Container apps: direct port access (avoids root-relative asset breakage under /app/xxx/ proxy) */
|
/** Container apps: direct port access (avoids root-relative asset breakage under /app/xxx/ proxy) */
|
||||||
export const APP_PORTS: Record<string, number> = {
|
export const APP_PORTS: Record<string, number> = {
|
||||||
'bitcoin-knots': 8334,
|
'bitcoin-knots': 8334,
|
||||||
|
'bitcoin-core': 8334,
|
||||||
'bitcoin-ui': 8334,
|
'bitcoin-ui': 8334,
|
||||||
'electrumx': 50002,
|
'electrumx': 50002,
|
||||||
'electrs': 50002,
|
'electrs': 50002,
|
||||||
@ -57,6 +58,7 @@ export const PROXY_APPS: Record<string, string> = {}
|
|||||||
* On HTTP, direct port access is used instead (faster, no proxy). */
|
* On HTTP, direct port access is used instead (faster, no proxy). */
|
||||||
export const HTTPS_PROXY_PATHS: Record<string, string> = {
|
export const HTTPS_PROXY_PATHS: Record<string, string> = {
|
||||||
'bitcoin-knots': '/app/bitcoin-ui/',
|
'bitcoin-knots': '/app/bitcoin-ui/',
|
||||||
|
'bitcoin-core': '/app/bitcoin-ui/',
|
||||||
'bitcoin-ui': '/app/bitcoin-ui/',
|
'bitcoin-ui': '/app/bitcoin-ui/',
|
||||||
'lnd': '/app/lnd/',
|
'lnd': '/app/lnd/',
|
||||||
'electrumx': '/app/electrs/',
|
'electrumx': '/app/electrs/',
|
||||||
@ -107,7 +109,8 @@ export const EXTERNAL_URLS: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const APP_TITLES: Record<string, string> = {
|
export const APP_TITLES: Record<string, string> = {
|
||||||
'bitcoin-knots': 'Bitcoin', 'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub',
|
'bitcoin-knots': 'Bitcoin Knots', 'bitcoin-core': 'Bitcoin Core',
|
||||||
|
'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub',
|
||||||
'botfights': 'BotFights', 'gitea': 'Gitea', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation',
|
'botfights': 'BotFights', 'gitea': 'Gitea', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation',
|
||||||
'nostr-vpn': 'Nostr VPN', 'fips': 'FIPS', 'routstr': 'Routstr',
|
'nostr-vpn': 'Nostr VPN', 'fips': 'FIPS', 'routstr': 'Routstr',
|
||||||
'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma',
|
'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma',
|
||||||
|
|||||||
@ -180,6 +180,90 @@ init()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||||
|
<!-- v1.7.37-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.37-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">Apr 22, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>Bitcoin Core (the reference implementation) now installs from the App Store and runs cleanly alongside Bitcoin Knots as a first-class option. The install flow pulls the official docker.io/bitcoin image directly if your internal mirrors don't carry it, and the node UI auto-detects which implementation is running so the logo, title, and version line all reflect Core vs. Knots without any manual config.</p>
|
||||||
|
<p>The node dashboard now shows a Storage indicator (Full Archive · X GB or Pruned · X GB) right next to Network, so you can tell at a glance whether your node is carrying the full chain history or the last ~550 MB. The Node Settings modal was stripped of its hardcoded Regtest/port-18443 placeholders and now shows real values — network mode, storage mode, transaction index, ZMQ publishing, and RPC port — all read from the running node.</p>
|
||||||
|
<p>Fresh installs no longer default to pruned mode. Previously, a new install would write <code>prune=550</code> into bitcoin.conf even on boxes with 2 TB of free space; now the default is full archive and you can opt into pruning by editing the conf yourself.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.7.36-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.36-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">Apr 22, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>Bitcoin Core joins the App Store as its own entry, with the Umbrel community icon and a description that frames it as a reference alternative to Bitcoin Knots rather than a replacement. A Sovereignty Stack tile on the Discover page now groups your node options together so the choice is obvious.</p>
|
||||||
|
<p>The App Store catalog fetch now follows whichever container registries you've set as primary in Settings. Previously the catalog URL was hardcoded to two servers; now the operator's own mirror priority drives where the App Store pulls its listings from, so switching primary actually moves the catalog too.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.7.35-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.35-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">Apr 22, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>Rootless-netns self-heal: if the container network loses its outbound tap (symptom: Bitcoin Knots and other outbound containers can't reach the internet even though container-to-container still works), the node now detects it and restarts the network from scratch on its own. No more having to SSH in and bounce podman.</p>
|
||||||
|
<p>Every app card on the Apps page now has an Update button whenever a newer version of the app is available — same flow as the detail view, one click away. Updating apps used to require drilling into each card individually.</p>
|
||||||
|
<p>Your node's Web5 DID, Identities list, and peer-to-peer pubkey now all resolve to the same seed-derived identity instead of drifting apart after onboarding. The Node identity on fresh installs is mirrored from the onboarding seed rather than generated as a separate random keypair.</p>
|
||||||
|
<p>Bitcoin Knots (and the new Bitcoin Core slot) now run on bitcoin/bitcoin 28.4 with a realistic 4 GiB memory cap and uncapped CPUs so bitcoind can run -par=auto across every core on your box.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.7.34-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.34-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">Apr 22, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>The login background now rotates through six atmospheric images, advancing one each time you land on the login screen, so returning to your node doesn't keep showing the same wallpaper. The chosen index is remembered across logouts.</p>
|
||||||
|
<p>Re-logging in is noticeably snappier. The dashboard entry animation used to replay the full 1.2-second zoom reveal on every login; that's now reserved for the first entry after onboarding. Subsequent logins fade in with just the welcome typing in about 300 ms.</p>
|
||||||
|
<p>If you clear site data on a node you've already onboarded, the intro video no longer fires again on the login screen. The onboarding cache is re-seeded from the backend automatically, so /login stays quiet instead of replaying the whole intro sequence.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.7.33-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.33-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">Apr 22, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>The onboarding wizard no longer gets skipped on genuinely-fresh nodes when you connect from a browser that onboarded a different node earlier. The backend is now the source of truth for "has this node been onboarded yet?" — the browser's local flag is the offline fallback, not the primary answer.</p>
|
||||||
|
<p>Already-onboarded nodes no longer show the "boot loader" or "server starting up" screens during an OTA update blip. The health check polls quietly for up to a minute before showing the boot screen, so a 10-second restart no longer looks like a catastrophic failure.</p>
|
||||||
|
<p>Logging out and returning to <code>/login</code> no longer replays the full intro video — you get a quiet lock-screen background instead. The full welcome sequence is reserved for genuine first-time entries.</p>
|
||||||
|
<p>Upgrading nodes now pick up this release's UI cleanly without a stale cache hanging on. A cache-version bump tells your browser's service worker to ditch the old bundle on first load.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.7.32-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.32-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">Apr 22, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>Hotfix: v1.7.31's frontend tarball was packaged with an extra wrapper directory, which left some nodes serving 403/500 after applying the update instead of the new UI. This release ships the tarball with the correct flat layout, and broken nodes heal automatically when this update applies.</p>
|
||||||
|
<p>Updates now finalize cleanly instead of being force-killed by systemd. Previously the node logged "shut down cleanly" during an update, then systemd waited 15 seconds and SIGKILL'd the service because one of the internal threads wasn't releasing. That's been tracked down and fixed, so the service exits promptly and the restart path is snappier.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.7.31-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.31-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">Apr 22, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>IndeedHub install is now idempotent — re-running it after a failed first attempt no longer leaves orphaned containers blocking the retry with a "name already in use" error. The installer force-cleans leftover containers and the dedicated network before starting a fresh stack.</p>
|
||||||
|
<p>Server 3 (OVH) is now an automatic tertiary mirror for both system updates and app registries. Existing nodes pick it up on next restart without any manual config — another independent network path, so a single-provider outage can't stall downloads.</p>
|
||||||
|
<p>The reachability test on the Registries page no longer reports false "unreachable" for Gitea-backed registries. The probe now hits the Docker V2 API at the correct host-root path and accepts HTTP 405 in addition to 200/401 as "registry alive".</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.7.30-alpha -->
|
<!-- v1.7.30-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
@ -1,26 +1,27 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7.36-alpha",
|
"version": "1.7.37-alpha",
|
||||||
"release_date": "2026-04-22",
|
"release_date": "2026-04-22",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"Bitcoin Core 28.4 is now visible in the App Store and on the Sovereignty Stack, with the Umbrel community icon. Install it any time you'd prefer mainline Bitcoin Core over Knots — both validate every block themselves.",
|
"Bitcoin Core now installs cleanly from the App Store and runs with its own branding — the node UI auto-detects which implementation is running so logo, title, and version line reflect Core vs. Knots without any manual config.",
|
||||||
"The App Store now follows the container registry you select in Settings. If you switch to a private mirror, Discover and Marketplace will pull their catalog from that mirror too — no more hardcoded URL pointing somewhere you don't control."
|
"The Bitcoin node dashboard now shows a Storage indicator (Full Archive · X GB or Pruned · X GB) next to Network, and the Node Settings modal shows the real network mode, transaction index, ZMQ publishing, and RPC port instead of hardcoded placeholders.",
|
||||||
|
"Fresh Bitcoin installs no longer default to pruned mode — nodes with plenty of disk keep the full archive rather than being silently capped at 550 MB."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"current_version": "1.7.35-alpha",
|
"current_version": "1.7.36-alpha",
|
||||||
"new_version": "1.7.36-alpha",
|
"new_version": "1.7.37-alpha",
|
||||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.36-alpha/archipelago",
|
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.37-alpha/archipelago",
|
||||||
"sha256": "c2714ef11f0621eca93713bbfd4c3eda1053a4d18ea27be504073099dbcfe906",
|
"sha256": "df7ce22c185f97db2acabf7490c200f2d4af1371db97151ee0de116149150ce8",
|
||||||
"size_bytes": 41108800
|
"size_bytes": 41162992
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archipelago-frontend-1.7.36-alpha.tar.gz",
|
"name": "archipelago-frontend-1.7.37-alpha.tar.gz",
|
||||||
"current_version": "1.7.35-alpha",
|
"current_version": "1.7.36-alpha",
|
||||||
"new_version": "1.7.36-alpha",
|
"new_version": "1.7.37-alpha",
|
||||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.36-alpha/archipelago-frontend-1.7.36-alpha.tar.gz",
|
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.37-alpha/archipelago-frontend-1.7.37-alpha.tar.gz",
|
||||||
"sha256": "4af6360a0a5f09272685442d90f02f0e47bd887e953946ecd81a11ad3ff8e578",
|
"sha256": "bdb893755e2d8c43a14c03fad76eb5dcb7262643dca3932f6c8fd0b3aea2e980",
|
||||||
"size_bytes": 77031992
|
"size_bytes": 77033652
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
releases/v1.7.37-alpha/archipelago
Executable file
BIN
releases/v1.7.37-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.37-alpha/archipelago-frontend-1.7.37-alpha.tar.gz
Normal file
BIN
releases/v1.7.37-alpha/archipelago-frontend-1.7.37-alpha.tar.gz
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user