Compare commits
2 Commits
v1.7.34-al
...
v1.7.36-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7106a81c6a | ||
|
|
987158ef5f |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"updated": "2026-04-12T00:00:00Z",
|
||||
"updated": "2026-04-22T00:00:00Z",
|
||||
"registry": "git.tx1138.com/lfg2025",
|
||||
"featured": {
|
||||
"id": "indeedhub",
|
||||
@@ -18,6 +18,14 @@
|
||||
"dockerImage": "git.tx1138.com/lfg2025/bitcoin-knots:latest",
|
||||
"repoUrl": "https://github.com/bitcoinknots/bitcoin"
|
||||
},
|
||||
{
|
||||
"id": "bitcoin-core", "title": "Bitcoin Core", "version": "28.4",
|
||||
"description": "Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks.",
|
||||
"icon": "/assets/img/app-icons/bitcoin-core.svg",
|
||||
"author": "Bitcoin Core contributors", "category": "money", "tier": "optional",
|
||||
"dockerImage": "bitcoin/bitcoin:28.4",
|
||||
"repoUrl": "https://github.com/bitcoin/bitcoin"
|
||||
},
|
||||
{
|
||||
"id": "lnd", "title": "LND", "version": "0.18.4",
|
||||
"description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
app:
|
||||
id: bitcoin-core
|
||||
name: Bitcoin Core
|
||||
version: 24.0.0
|
||||
version: 28.4.0
|
||||
description: Full Bitcoin node implementation. The reference implementation of the Bitcoin protocol.
|
||||
|
||||
|
||||
container:
|
||||
image: bitcoin/bitcoin:24.0
|
||||
image: bitcoin/bitcoin:28.4
|
||||
image_signature: cosign://...
|
||||
pull_policy: verify-signature
|
||||
|
||||
@@ -13,8 +13,8 @@ app:
|
||||
- storage: 500Gi # Minimum disk space for mainnet
|
||||
|
||||
resources:
|
||||
cpu_limit: 2
|
||||
memory_limit: 2Gi
|
||||
cpu_limit: 0 # 0 = unlimited; bitcoind uses -par=auto across all cores
|
||||
memory_limit: 4Gi # matches container-specs.sh bitcoin-knots large-disk dbcache=4096
|
||||
disk_limit: 500Gi
|
||||
|
||||
security:
|
||||
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.34-alpha"
|
||||
version = "1.7.36-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.34-alpha"
|
||||
version = "1.7.36-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -115,14 +115,41 @@ impl ApiHandler {
|
||||
|
||||
/// Server-side fetch of the upstream app catalog so the browser can
|
||||
/// load it without fighting CORS (git.tx1138.com emits no ACAO) or
|
||||
/// CSP (the fallback IP-port URL isn't in `connect-src`). Tries the
|
||||
/// upstream URLs in the same order the frontend used, returns the
|
||||
/// first 2xx response. 15s total timeout.
|
||||
async fn handle_app_catalog_proxy() -> Result<Response<hyper::Body>> {
|
||||
const UPSTREAMS: &[&str] = &[
|
||||
"http://23.182.128.160:3000/lfg2025/app-catalog/raw/branch/main/catalog.json",
|
||||
"https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json",
|
||||
];
|
||||
/// CSP (the fallback IP-port URL isn't in `connect-src`). The upstream
|
||||
/// list is derived from the operator's configured container registries
|
||||
/// so switching mirrors in Settings changes the App Store source too —
|
||||
/// each active registry contributes one Gitea `raw/branch/main/catalog.json`
|
||||
/// URL (http or https per `tls_verify`), tried in priority order.
|
||||
/// If registry config can't be loaded, falls back to the legacy
|
||||
/// hardcoded pair so the App Store still renders on nodes that haven't
|
||||
/// persisted a registry config yet. 15s total timeout.
|
||||
async fn handle_app_catalog_proxy(&self) -> Result<Response<hyper::Body>> {
|
||||
let mut upstreams: Vec<String> = Vec::new();
|
||||
if let Ok(config) =
|
||||
crate::container::registry::load_registries(&self.config.data_dir).await
|
||||
{
|
||||
for reg in config.active_registries() {
|
||||
let scheme = if reg.tls_verify { "https" } else { "http" };
|
||||
// Gitea raw URL: <scheme>://<host>/<namespace>/app-catalog/raw/branch/main/catalog.json.
|
||||
// reg.url already includes the namespace (e.g. "host/lfg2025"),
|
||||
// so we just tack on the repo + raw path.
|
||||
upstreams.push(format!(
|
||||
"{}://{}/app-catalog/raw/branch/main/catalog.json",
|
||||
scheme, reg.url
|
||||
));
|
||||
}
|
||||
}
|
||||
if upstreams.is_empty() {
|
||||
upstreams.push(
|
||||
"http://23.182.128.160:3000/lfg2025/app-catalog/raw/branch/main/catalog.json"
|
||||
.to_string(),
|
||||
);
|
||||
upstreams.push(
|
||||
"https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let client = match reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()
|
||||
@@ -136,8 +163,8 @@ impl ApiHandler {
|
||||
));
|
||||
}
|
||||
};
|
||||
for url in UPSTREAMS {
|
||||
match client.get(*url).send().await {
|
||||
for url in &upstreams {
|
||||
match client.get(url).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
if let Ok(bytes) = resp.bytes().await {
|
||||
return Ok(Response::builder()
|
||||
@@ -408,7 +435,7 @@ impl ApiHandler {
|
||||
if !self.is_authenticated(&headers).await {
|
||||
return Ok(Self::unauthorized());
|
||||
}
|
||||
Self::handle_app_catalog_proxy().await
|
||||
self.handle_app_catalog_proxy().await
|
||||
}
|
||||
|
||||
// LND connect info — nginx validates session cookie (presence check),
|
||||
|
||||
152
core/archipelago/src/bootstrap.rs
Normal file
152
core/archipelago/src/bootstrap.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
//! Bootstrap host-side doctor artifacts on every archipelago startup.
|
||||
//!
|
||||
//! The update pipeline swaps the archipelago binary but does not touch
|
||||
//! scripts or systemd units — those are installed once by the ISO builder.
|
||||
//! Without this module, changes to `container-doctor.sh` or the doctor
|
||||
//! service/timer never reach boxes installed before the change.
|
||||
//!
|
||||
//! On startup we compare three embedded files against their on-disk
|
||||
//! copies and rewrite any that differ, then enable the doctor timer if
|
||||
//! it isn't already. Idempotent: no-ops on boxes that match the
|
||||
//! embedded version. All work is best-effort — failures are logged but
|
||||
//! never abort the backend.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use tokio::fs;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::update::host_sudo;
|
||||
|
||||
const DOCTOR_SH: &str = include_str!("../../../scripts/container-doctor.sh");
|
||||
const DOCTOR_SERVICE: &str =
|
||||
include_str!("../../../image-recipe/configs/archipelago-doctor.service");
|
||||
const DOCTOR_TIMER: &str =
|
||||
include_str!("../../../image-recipe/configs/archipelago-doctor.timer");
|
||||
|
||||
const DOCTOR_SH_PATH: &str = "/home/archipelago/archy/scripts/container-doctor.sh";
|
||||
const DOCTOR_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-doctor.service";
|
||||
const DOCTOR_TIMER_PATH: &str = "/etc/systemd/system/archipelago-doctor.timer";
|
||||
|
||||
/// 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.
|
||||
pub async fn ensure_doctor_installed() {
|
||||
match run().await {
|
||||
Ok(changed) if changed => info!("Doctor artifacts synchronized with binary"),
|
||||
Ok(_) => debug!("Doctor artifacts already in sync"),
|
||||
Err(e) => warn!("Doctor bootstrap failed (non-fatal): {:#}", e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> Result<bool> {
|
||||
// Dev-box guard: on contributors' laptops `/home/archipelago/archy` is
|
||||
// typically a symlink into the git checkout, and writing through it
|
||||
// would clobber the working tree with whatever the binary happens to
|
||||
// have been compiled from. Production ISO installs materialize a real
|
||||
// directory.
|
||||
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)
|
||||
{
|
||||
debug!("/home/archipelago/archy is a symlink — skipping doctor bootstrap (dev box)");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Skip entirely on machines without the canonical scripts directory —
|
||||
// writing orphan files there just causes confusion.
|
||||
let scripts_dir = Path::new(DOCTOR_SH_PATH)
|
||||
.parent()
|
||||
.context("doctor script path has no parent")?;
|
||||
if !scripts_dir.exists() {
|
||||
debug!(
|
||||
"Scripts dir {} missing — skipping doctor bootstrap",
|
||||
scripts_dir.display()
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
// 1. Script — lives in archipelago's home dir, user-writable.
|
||||
if needs_write(DOCTOR_SH_PATH, DOCTOR_SH).await {
|
||||
fs::write(DOCTOR_SH_PATH, DOCTOR_SH)
|
||||
.await
|
||||
.with_context(|| format!("write {}", DOCTOR_SH_PATH))?;
|
||||
let _ = tokio::process::Command::new("chmod")
|
||||
.args(["+x", DOCTOR_SH_PATH])
|
||||
.status()
|
||||
.await;
|
||||
info!("Updated {}", DOCTOR_SH_PATH);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 2. Systemd unit files — /etc is restricted; route through host_sudo.
|
||||
let service_changed = write_root_if_needed(DOCTOR_SERVICE_PATH, DOCTOR_SERVICE).await?;
|
||||
let timer_changed = write_root_if_needed(DOCTOR_TIMER_PATH, DOCTOR_TIMER).await?;
|
||||
changed = changed || service_changed || timer_changed;
|
||||
|
||||
// 3. Reload + enable. Only when we actually touched units, or when the
|
||||
// timer isn't enabled yet (catches fresh upgrades of boxes that predate
|
||||
// the doctor entirely).
|
||||
let timer_enabled = is_timer_enabled().await;
|
||||
if service_changed || timer_changed || !timer_enabled {
|
||||
if let Err(e) = host_sudo(&["systemctl", "daemon-reload"]).await {
|
||||
warn!("daemon-reload failed: {:#}", e);
|
||||
}
|
||||
if let Err(e) = host_sudo(&["systemctl", "enable", "--now", "archipelago-doctor.timer"])
|
||||
.await
|
||||
{
|
||||
warn!("enable archipelago-doctor.timer failed: {:#}", e);
|
||||
} else if !timer_enabled {
|
||||
info!("Enabled archipelago-doctor.timer");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
async fn needs_write(path: &str, expected: &str) -> bool {
|
||||
match fs::read_to_string(path).await {
|
||||
Ok(current) => current != expected,
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write content to a root-owned path via `sudo mv` of a user-owned tmp file.
|
||||
/// Returns true if a write happened.
|
||||
async fn write_root_if_needed(path: &str, content: &str) -> Result<bool> {
|
||||
if !needs_write(path, content).await {
|
||||
return Ok(false);
|
||||
}
|
||||
let tmp = format!(
|
||||
"/tmp/archipelago-bootstrap-{}-{}.tmp",
|
||||
std::process::id(),
|
||||
Path::new(path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("unit")
|
||||
);
|
||||
fs::write(&tmp, content)
|
||||
.await
|
||||
.with_context(|| format!("write tmp {}", tmp))?;
|
||||
let status = host_sudo(&["mv", &tmp, path])
|
||||
.await
|
||||
.with_context(|| format!("sudo mv {} -> {}", tmp, path))?;
|
||||
if !status.success() {
|
||||
let _ = fs::remove_file(&tmp).await;
|
||||
anyhow::bail!("sudo mv to {} exited with {}", path, status);
|
||||
}
|
||||
info!("Updated {}", path);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn is_timer_enabled() -> bool {
|
||||
tokio::process::Command::new("systemctl")
|
||||
.args(["is-enabled", "--quiet", "archipelago-doctor.timer"])
|
||||
.status()
|
||||
.await
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -216,6 +216,80 @@ impl IdentityManager {
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// Mirror an existing Ed25519 signing key as a manager-level identity.
|
||||
///
|
||||
/// Used at boot to expose the node's own seed-derived key (the one that
|
||||
/// backs `server_info.pubkey` and peer-to-peer connections) as an
|
||||
/// entry in the Identities page, so all three surfaces — DID Status,
|
||||
/// "Node" entry on Identities, and peer-connect DID — resolve to the
|
||||
/// same DID. The id is deterministic (`node-<pubkey16>`), so repeated
|
||||
/// calls on the same key are idempotent: if the file already exists
|
||||
/// we return the existing record untouched.
|
||||
pub async fn create_from_signing_key(
|
||||
&self,
|
||||
name: String,
|
||||
purpose: IdentityPurpose,
|
||||
signing_key: SigningKey,
|
||||
) -> Result<IdentityRecord> {
|
||||
let pubkey_hex = hex::encode(signing_key.verifying_key().as_bytes());
|
||||
let did = did_key_from_pubkey_hex(&pubkey_hex)?;
|
||||
let id = format!("node-{}", &pubkey_hex[..16]);
|
||||
|
||||
// Idempotent: if we already mirrored this key, just return it.
|
||||
let file_path = self.identities_dir.join(format!("{}.json", id));
|
||||
if file_path.exists() {
|
||||
return self.get(&id).await;
|
||||
}
|
||||
|
||||
let created_at = chrono::Utc::now().to_rfc3339();
|
||||
// Mark as the node (master) identity so it gets the hex SVG.
|
||||
let default_profile = IdentityProfile {
|
||||
picture: Some(crate::avatar::default_picture(&pubkey_hex, true)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let identity_file = IdentityFile {
|
||||
id: id.clone(),
|
||||
name: name.clone(),
|
||||
purpose: purpose.clone(),
|
||||
secret_key: signing_key.to_bytes().to_vec(),
|
||||
pubkey_hex: pubkey_hex.clone(),
|
||||
did: did.clone(),
|
||||
created_at,
|
||||
nostr_secret_hex: None,
|
||||
nostr_pubkey_hex: None,
|
||||
profile: Some(default_profile),
|
||||
derivation_index: Some(0),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&identity_file)
|
||||
.context("Failed to serialize identity")?;
|
||||
fs::write(&file_path, json.as_bytes())
|
||||
.await
|
||||
.context("Failed to write identity file")?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&file_path, std::fs::Permissions::from_mode(0o600))
|
||||
.await
|
||||
.context("Failed to set identity file permissions")?;
|
||||
}
|
||||
|
||||
// First identity becomes the default.
|
||||
let (existing, _) = self.list().await?;
|
||||
if existing.len() <= 1 {
|
||||
self.set_default(&id).await?;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Mirrored node signing key as Node identity '{}' ({})",
|
||||
name,
|
||||
purpose
|
||||
);
|
||||
|
||||
self.get(&id).await
|
||||
}
|
||||
|
||||
/// Create a new identity with keys derived from a BIP-39 master seed.
|
||||
/// The derivation index is auto-incremented and persisted.
|
||||
pub async fn create_from_seed(
|
||||
|
||||
@@ -28,6 +28,7 @@ mod avatar;
|
||||
mod backup;
|
||||
mod bitcoin_rpc;
|
||||
mod blobs;
|
||||
mod bootstrap;
|
||||
mod config;
|
||||
mod constants;
|
||||
mod container;
|
||||
@@ -171,6 +172,11 @@ async fn main() -> Result<()> {
|
||||
update::run_update_scheduler(update_data_dir).await;
|
||||
});
|
||||
|
||||
// Synchronize host-side doctor artifacts (script + systemd units) with
|
||||
// what's embedded in this binary. Runs in the background so it never
|
||||
// delays server readiness; best-effort, warnings only.
|
||||
tokio::spawn(bootstrap::ensure_doctor_installed());
|
||||
|
||||
// Spawn periodic container snapshot (for crash recovery)
|
||||
crash_recovery::spawn_snapshot_task(config.data_dir.clone());
|
||||
|
||||
|
||||
@@ -89,22 +89,32 @@ impl Server {
|
||||
// Load persisted messages (Archipelago channel)
|
||||
node_message::init(&config.data_dir).await;
|
||||
|
||||
// Auto-create default identity if none exist (fresh boot or factory reset)
|
||||
// Auto-create the Node identity on fresh boot, mirroring the node's
|
||||
// own signing key (seed-derived when onboarded, random otherwise).
|
||||
// This keeps the DID shown on the Identities page, the DID Status
|
||||
// card, and the DID used for peer-to-peer connects all aligned on
|
||||
// one value — the seed-derived node DID. Idempotent: if the entry
|
||||
// already exists from a prior boot, create_from_signing_key returns
|
||||
// the existing record unchanged.
|
||||
{
|
||||
let im = crate::identity_manager::IdentityManager::new(&config.data_dir).await;
|
||||
if let Ok(mgr) = im {
|
||||
if let Ok((list, _)) = mgr.list().await {
|
||||
if list.is_empty() {
|
||||
let signing_key = ed25519_dalek::SigningKey::from_bytes(
|
||||
&identity.signing_key().to_bytes(),
|
||||
);
|
||||
match mgr
|
||||
.create(
|
||||
"Default".to_string(),
|
||||
.create_from_signing_key(
|
||||
"Node".to_string(),
|
||||
crate::identity_manager::IdentityPurpose::Personal,
|
||||
signing_key,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(record) => {
|
||||
let _ = mgr.create_nostr_key(&record.id).await;
|
||||
tracing::info!(did = %record.did, "Auto-created default identity with Nostr key");
|
||||
tracing::info!(did = %record.did, "Auto-created Node identity mirroring node key");
|
||||
}
|
||||
Err(e) => tracing::debug!("Auto-identity creation (non-fatal): {}", e),
|
||||
}
|
||||
|
||||
@@ -792,7 +792,7 @@ pub async fn cancel_download(data_dir: &Path) -> Result<()> {
|
||||
/// though sudo itself is root. `systemd-run --wait` spawns a transient
|
||||
/// service unit that inherits systemd's default protections (i.e. none
|
||||
/// of ours), escaping the namespace.
|
||||
async fn host_sudo(args: &[&str]) -> Result<std::process::ExitStatus> {
|
||||
pub(crate) async fn host_sudo(args: &[&str]) -> Result<std::process::ExitStatus> {
|
||||
let mut full: Vec<&str> = vec![
|
||||
"systemd-run",
|
||||
"--wait",
|
||||
|
||||
4
neode-ui/package-lock.json
generated
4
neode-ui/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.6.0-alpha",
|
||||
"version": "1.7.36-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.6.0-alpha",
|
||||
"version": "1.7.36-alpha",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.6.0-alpha",
|
||||
"version": "1.7.36-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
14
neode-ui/public/assets/img/app-icons/bitcoin-core.svg
Normal file
14
neode-ui/public/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 |
@@ -22,6 +22,18 @@
|
||||
"category": "money",
|
||||
"tier": "core"
|
||||
},
|
||||
{
|
||||
"id": "bitcoin-core",
|
||||
"title": "Bitcoin Core",
|
||||
"version": "28.4",
|
||||
"description": "Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks on the Bitcoin network.",
|
||||
"icon": "/assets/img/app-icons/bitcoin-core.svg",
|
||||
"author": "Bitcoin Core contributors",
|
||||
"dockerImage": "bitcoin/bitcoin:28.4",
|
||||
"repoUrl": "https://github.com/bitcoin/bitcoin",
|
||||
"category": "money",
|
||||
"tier": "optional"
|
||||
},
|
||||
{
|
||||
"id": "lnd",
|
||||
"title": "LND",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"launch": "Launch",
|
||||
"starting": "Starting...",
|
||||
"stopping": "Stopping...",
|
||||
"update": "Update",
|
||||
"updating": "Updating...",
|
||||
"send": "Send",
|
||||
"sending": "Sending...",
|
||||
"back": "Back",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"launch": "Abrir",
|
||||
"starting": "Iniciando...",
|
||||
"stopping": "Deteniendo...",
|
||||
"update": "Actualizar",
|
||||
"updating": "Actualizando...",
|
||||
"send": "Enviar",
|
||||
"sending": "Enviando...",
|
||||
"back": "Volver",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
@start="actions.startApp"
|
||||
@stop="actions.stopApp"
|
||||
@restart="actions.restartApp"
|
||||
@update="updateApp"
|
||||
@show-uninstall="showUninstallModal"
|
||||
/>
|
||||
</div>
|
||||
@@ -296,4 +297,12 @@ function goToApp(id: string) {
|
||||
function launchApp(id: string) {
|
||||
useAppLauncherStore().openSession(id)
|
||||
}
|
||||
|
||||
async function updateApp(id: string) {
|
||||
try {
|
||||
await serverStore.updatePackage(id)
|
||||
} catch (err) {
|
||||
actions.actionError.value = `Failed to update ${id}: ${err instanceof Error ? err.message : 'Unknown error'}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -114,6 +114,29 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-4 flex gap-2">
|
||||
<!-- Update available -->
|
||||
<button
|
||||
v-if="pkg['available-update'] && pkg.state !== 'updating'"
|
||||
@click.stop="$emit('update', id)"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium flex items-center justify-center gap-1.5 bg-orange-500/20 border border-orange-500/40 text-orange-200 hover:bg-orange-500/30 transition-colors"
|
||||
:title="`Update to v${pkg['available-update']}`"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{{ t('common.update') }}
|
||||
</button>
|
||||
<!-- Updating in progress -->
|
||||
<span
|
||||
v-if="pkg.state === 'updating'"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium flex items-center justify-center gap-1.5 bg-orange-500/20 border border-orange-500/40 text-orange-200"
|
||||
>
|
||||
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ t('common.updating') }}
|
||||
</span>
|
||||
<!-- Launch -->
|
||||
<button
|
||||
v-if="canLaunch(pkg)"
|
||||
@@ -211,6 +234,7 @@ const emit = defineEmits<{
|
||||
start: [id: string]
|
||||
stop: [id: string]
|
||||
restart: [id: string]
|
||||
update: [id: string]
|
||||
showUninstall: [id: string, pkg: PackageDataEntry]
|
||||
}>()
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ export async function fetchAppCatalog(): Promise<AppCatalog | null> {
|
||||
export function getCuratedAppList(): MarketplaceApp[] {
|
||||
return [
|
||||
{ id: 'bitcoin-knots', title: 'Bitcoin Knots', version: '28.1.0', description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-knots.webp', author: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest`, repoUrl: 'https://github.com/bitcoinknots/bitcoin' },
|
||||
{ id: 'bitcoin-core', title: 'Bitcoin Core', version: '28.4', description: 'Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-core.svg', author: 'Bitcoin Core contributors', dockerImage: 'bitcoin/bitcoin:28.4', repoUrl: 'https://github.com/bitcoin/bitcoin' },
|
||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '1.13.7', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: `${R}/btcpayserver:1.13.7`, repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||
{ id: 'lnd', title: 'LND', version: '0.18.4', description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', icon: '/assets/img/app-icons/lnd.svg', author: 'Lightning Labs', dockerImage: `${R}/lnd:v0.18.4-beta`, repoUrl: 'https://github.com/lightningnetwork/lnd' },
|
||||
{ id: 'mempool', title: 'Mempool Explorer', version: '3.0.0', description: 'Self-hosted Bitcoin blockchain and mempool visualizer. Monitor transactions without revealing your addresses to third parties.', icon: '/assets/img/app-icons/mempool.webp', author: 'Mempool', dockerImage: `${R}/mempool-frontend:v3.0.0`, repoUrl: 'https://github.com/mempool/mempool' },
|
||||
@@ -164,6 +165,11 @@ export const FEATURED_DEFINITIONS: {
|
||||
desc: 'The foundation of sovereignty. Run a full Bitcoin node to validate every transaction yourself. No trusted third parties. No asking permission. Your node enforces the consensus rules that protect your wealth. Don\'t trust — verify.',
|
||||
tag: 'FULL VALIDATION // ZERO TRUST',
|
||||
},
|
||||
{
|
||||
id: 'bitcoin-core',
|
||||
desc: 'The reference Bitcoin implementation. Same full-node guarantees as Knots, tracking upstream releases from the Bitcoin Core maintainers. Pick this if you\'d rather run mainline Bitcoin Core than Knots — both validate every block themselves.',
|
||||
tag: 'REFERENCE CLIENT // ZERO TRUST',
|
||||
},
|
||||
{
|
||||
id: 'lnd',
|
||||
desc: 'Lightning-fast payments over the Lightning Network. Open channels, route transactions, and earn routing fees — all from your sovereign node. Instant settlement. Near-zero fees. The future of money, running on your hardware.',
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
{
|
||||
"version": "1.7.34-alpha",
|
||||
"version": "1.7.36-alpha",
|
||||
"release_date": "2026-04-22",
|
||||
"changelog": [
|
||||
"Clearing browser data on an already-onboarded node no longer sends you back through the onboarding wizard. The page now asks the node on every visit and re-caches the answer locally, so clearing site data is harmless.",
|
||||
"Login screen background now rotates each time you land on it — cycles through the six intro images so the lock screen doesn't look identical on every logout.",
|
||||
"Removed the zoom-in effect from subsequent logins. Only the first-ever login (right after onboarding) plays the full zoom + glitch reveal; every re-login now just lands on the dashboard with the welcome typing, no animation."
|
||||
"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.",
|
||||
"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."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.33-alpha",
|
||||
"new_version": "1.7.34-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.34-alpha/archipelago",
|
||||
"sha256": "ba0c6cb531f48a8d350775ab61972291c5c92a298551a7c0543d42e1fe3b18e1",
|
||||
"size_bytes": 40791976
|
||||
"current_version": "1.7.35-alpha",
|
||||
"new_version": "1.7.36-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.36-alpha/archipelago",
|
||||
"sha256": "c2714ef11f0621eca93713bbfd4c3eda1053a4d18ea27be504073099dbcfe906",
|
||||
"size_bytes": 41108800
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.34-alpha.tar.gz",
|
||||
"current_version": "1.7.33-alpha",
|
||||
"new_version": "1.7.34-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.34-alpha/archipelago-frontend-1.7.34-alpha.tar.gz",
|
||||
"sha256": "8700c1ce6534567f008670a810682333966481e0c2e5d36164013219fb2ed3d9",
|
||||
"size_bytes": 77007484
|
||||
"name": "archipelago-frontend-1.7.36-alpha.tar.gz",
|
||||
"current_version": "1.7.35-alpha",
|
||||
"new_version": "1.7.36-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",
|
||||
"sha256": "4af6360a0a5f09272685442d90f02f0e47bd887e953946ecd81a11ad3ff8e578",
|
||||
"size_bytes": 77031992
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
releases/v1.7.35-alpha/archipelago
Executable file
BIN
releases/v1.7.35-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.35-alpha/archipelago-frontend-1.7.35-alpha.tar.gz
Normal file
BIN
releases/v1.7.35-alpha/archipelago-frontend-1.7.35-alpha.tar.gz
Normal file
Binary file not shown.
BIN
releases/v1.7.36-alpha/archipelago
Executable file
BIN
releases/v1.7.36-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.36-alpha/archipelago-frontend-1.7.36-alpha.tar.gz
Normal file
BIN
releases/v1.7.36-alpha/archipelago-frontend-1.7.36-alpha.tar.gz
Normal file
Binary file not shown.
@@ -367,7 +367,72 @@ print(' '.join(['\"' + a + '\"' if ' ' in a else a for a in args[2:]]))
|
||||
[ ${#fixed_names[@]} -gt 0 ] && return 0 || return 1
|
||||
}
|
||||
|
||||
# ── Fix 8: Restart stopped core containers ──────────────────
|
||||
# ── Fix 8: Rootless netns egress lost ────────────────────────
|
||||
# Rootless podman uses pasta to give containers internet egress. If pasta's
|
||||
# tap vanishes (host link flap, mount churn), the rootless-netns keeps inter-
|
||||
# container traffic working but silently loses outbound. Bitcoin IBD stalls
|
||||
# at 0 peers; package pulls fail. The only reliable repair is a stop-all/
|
||||
# start-all cycle so pasta + aardvark-dns rebuild the netns from scratch.
|
||||
fix_rootless_netns_egress() {
|
||||
local archi_uid
|
||||
archi_uid=$(id -u archipelago 2>/dev/null) || return 1
|
||||
|
||||
# Locate the rootless-netns via aardvark-dns (it lives inside it).
|
||||
local aardvark_pid
|
||||
aardvark_pid=$(pgrep -U "$archi_uid" -f '^/usr/lib/podman/aardvark-dns' 2>/dev/null | head -1)
|
||||
[ -z "$aardvark_pid" ] && return 1 # no rootless network active
|
||||
|
||||
# Host precheck: if the host itself can't reach the internet, no point
|
||||
# cycling containers — this is an upstream problem.
|
||||
if ! timeout 3 bash -c '</dev/tcp/1.1.1.1/443' 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Probe egress from inside the rootless-netns. One probe is noisy;
|
||||
# require two consecutive failures 10s apart to rule out transients.
|
||||
if timeout 3 nsenter -t "$aardvark_pid" -n bash -c '</dev/tcp/1.1.1.1/443' 2>/dev/null; then
|
||||
return 1 # first probe succeeded
|
||||
fi
|
||||
sleep 10
|
||||
aardvark_pid=$(pgrep -U "$archi_uid" -f '^/usr/lib/podman/aardvark-dns' 2>/dev/null | head -1)
|
||||
[ -z "$aardvark_pid" ] && return 1
|
||||
if timeout 3 nsenter -t "$aardvark_pid" -n bash -c '</dev/tcp/1.1.1.1/443' 2>/dev/null; then
|
||||
return 1 # recovered on its own
|
||||
fi
|
||||
|
||||
log "Rootless-netns egress is broken (host online, container netns unreachable) — cycling"
|
||||
|
||||
local PODMANCMD="sudo -u archipelago XDG_RUNTIME_DIR=/run/user/$archi_uid podman"
|
||||
local running
|
||||
running=$($PODMANCMD ps --format '{{.Names}}' 2>/dev/null)
|
||||
if [ -z "$running" ]; then
|
||||
log " No running containers to cycle — skipping"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local count
|
||||
count=$(echo "$running" | wc -l)
|
||||
log " Stopping $count running containers (graceful, 30s)..."
|
||||
$PODMANCMD stop --all --time 30 >/dev/null 2>&1
|
||||
sleep 5
|
||||
|
||||
log " Starting containers back up..."
|
||||
for c in $running; do
|
||||
$PODMANCMD start "$c" >/dev/null 2>&1 &
|
||||
done
|
||||
wait
|
||||
sleep 5
|
||||
|
||||
aardvark_pid=$(pgrep -U "$archi_uid" -f '^/usr/lib/podman/aardvark-dns' 2>/dev/null | head -1)
|
||||
if [ -n "$aardvark_pid" ] && timeout 3 nsenter -t "$aardvark_pid" -n bash -c '</dev/tcp/1.1.1.1/443' 2>/dev/null; then
|
||||
log " Rootless-netns egress restored ($count containers cycled)"
|
||||
else
|
||||
log " WARN: egress still broken after cycle — may need manual intervention"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Fix 9: Restart stopped core containers ──────────────────
|
||||
# Rootless Podman 4.x restart policies don't auto-restart on crash.
|
||||
# This check restarts any exited core containers (tiers 0-2).
|
||||
fix_stopped_core_containers() {
|
||||
@@ -414,6 +479,7 @@ run_fix "tor-permissions" fix_tor_permissions
|
||||
run_fix "searxng" fix_searxng
|
||||
run_fix "bitcoin-txindex" fix_bitcoin_txindex
|
||||
run_fix "exit-127" fix_exit_127
|
||||
run_fix "netns-egress" fix_rootless_netns_egress
|
||||
run_fix "stopped-core" fix_stopped_core_containers
|
||||
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user