Compare commits
2 Commits
v1.7.15-al
...
v1.7.17-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4706dd16e7 | ||
|
|
3cbfcabedf |
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.15-alpha"
|
||||
version = "1.7.17-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.15-alpha"
|
||||
version = "1.7.17-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -420,6 +420,7 @@ impl RpcHandler {
|
||||
"update.status" => self.handle_update_status().await,
|
||||
"update.dismiss" => self.handle_update_dismiss().await,
|
||||
"update.download" => self.handle_update_download().await,
|
||||
"update.cancel-download" => self.handle_update_cancel_download().await,
|
||||
"update.apply" => self.handle_update_apply().await,
|
||||
"update.git-apply" => self.handle_update_git_apply().await,
|
||||
"update.rollback" => self.handle_update_rollback().await,
|
||||
|
||||
@@ -79,6 +79,7 @@ impl RpcHandler {
|
||||
let local_did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
|
||||
let local_onion = data.server_info.tor_address.clone().unwrap_or_default();
|
||||
let local_pubkey = data.server_info.pubkey.clone();
|
||||
let local_name = data.server_info.name.clone();
|
||||
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let node_identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
|
||||
@@ -90,6 +91,7 @@ impl RpcHandler {
|
||||
&local_onion,
|
||||
&local_pubkey,
|
||||
local_fips_npub.as_deref(),
|
||||
local_name.as_deref(),
|
||||
|data| node_identity.sign(data),
|
||||
)
|
||||
.await?;
|
||||
@@ -447,6 +449,38 @@ impl RpcHandler {
|
||||
.get("fips_npub")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
// Optional, unsigned: peer's display name. Display-only — identity
|
||||
// claims are anchored on the signed did/pubkey below.
|
||||
let incoming_name = params
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Reject self-peering. If somehow our own did / onion / pubkey
|
||||
// comes back at us (misconfigured invite, gossip loop), adding
|
||||
// the entry causes sync loops where the node syncs with itself
|
||||
// forever. Drop it quietly — no useful recovery path.
|
||||
let (own_data, _) = self.state_manager.get_snapshot().await;
|
||||
let own_did_result =
|
||||
identity::did_key_from_pubkey_hex(&own_data.server_info.pubkey).ok();
|
||||
let own_onion_trim = own_data
|
||||
.server_info
|
||||
.tor_address
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.trim_end_matches(".onion")
|
||||
.to_string();
|
||||
let incoming_onion_trim = onion.trim_end_matches(".onion");
|
||||
if own_did_result.as_deref() == Some(did)
|
||||
|| pubkey == own_data.server_info.pubkey
|
||||
|| (!own_onion_trim.is_empty() && own_onion_trim == incoming_onion_trim)
|
||||
{
|
||||
tracing::warn!(
|
||||
peer_did = %did,
|
||||
"Rejected peer-joined: inbound identity matches this node"
|
||||
);
|
||||
anyhow::bail!("Refusing to peer with self");
|
||||
}
|
||||
|
||||
// Verify ed25519 signature to prevent federation spoofing (H2 security fix)
|
||||
let signature = params.get("signature").and_then(|v| v.as_str());
|
||||
@@ -471,11 +505,12 @@ impl RpcHandler {
|
||||
|
||||
let nodes = federation::load_nodes(&self.config.data_dir).await?;
|
||||
if let Some(existing) = nodes.iter().find(|n| n.did == did) {
|
||||
// If already known but missing onion/pubkey/fips_npub, update them
|
||||
// If already known but missing onion/pubkey/fips_npub/name, update them
|
||||
let needs_onion = existing.onion.is_empty();
|
||||
let needs_pubkey = existing.pubkey.is_empty();
|
||||
let needs_fips = existing.fips_npub.is_none() && fips_npub.is_some();
|
||||
if needs_onion || needs_pubkey || needs_fips {
|
||||
let needs_name = existing.name.is_none() && incoming_name.is_some();
|
||||
if needs_onion || needs_pubkey || needs_fips || needs_name {
|
||||
let mut updated = existing.clone();
|
||||
if needs_onion && !onion.is_empty() {
|
||||
updated.onion = onion.to_string();
|
||||
@@ -486,6 +521,9 @@ impl RpcHandler {
|
||||
if needs_fips {
|
||||
updated.fips_npub = fips_npub.clone();
|
||||
}
|
||||
if needs_name {
|
||||
updated.name = incoming_name.clone();
|
||||
}
|
||||
updated.last_seen = Some(chrono::Utc::now().to_rfc3339());
|
||||
federation::update_node(&self.config.data_dir, &updated).await?;
|
||||
info!(peer_did = %did, peer_onion = %onion, "Updated existing peer with fresh identity fields");
|
||||
@@ -497,7 +535,7 @@ impl RpcHandler {
|
||||
did: did.to_string(),
|
||||
pubkey: pubkey.to_string(),
|
||||
onion: onion.to_string(),
|
||||
name: None,
|
||||
name: incoming_name.clone(),
|
||||
trust_level: TrustLevel::Trusted,
|
||||
added_at: chrono::Utc::now().to_rfc3339(),
|
||||
last_seen: None,
|
||||
@@ -512,9 +550,38 @@ impl RpcHandler {
|
||||
|
||||
// Mirror into mesh state so the inbound peer is addressable from
|
||||
// the chat UI without waiting for the next mesh restart.
|
||||
self.register_federation_peer_in_mesh(pubkey, did, None)
|
||||
self.register_federation_peer_in_mesh(pubkey, did, incoming_name.as_deref())
|
||||
.await;
|
||||
|
||||
// Bump the data-model revision so any Federation view with an
|
||||
// open WebSocket reloads its node list without waiting for the
|
||||
// user to click Sync.
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
self.state_manager.update_data(data).await;
|
||||
|
||||
// Transitive discovery: spawn a task that pulls the new peer's
|
||||
// state (its own federated peers end up as Observer entries on
|
||||
// our side) so after a join every existing peer in our list is
|
||||
// aware of the newcomer via the next pair of syncs, without the
|
||||
// user clicking anything. Best-effort; errors are logged only.
|
||||
let data_dir = self.config.data_dir.clone();
|
||||
let new_peer_did = did.to_string();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
if let Err(e) = crate::federation::sync_with_peer_by_did(
|
||||
&data_dir,
|
||||
&new_peer_did,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
peer_did = %new_peer_did,
|
||||
error = %e,
|
||||
"Transitive sync on peer-joined failed (non-fatal)"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(serde_json::json!({ "accepted": true }))
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +282,7 @@ impl RpcHandler {
|
||||
let local_fips_npub = crate::identity::fips_npub(&identity_dir2)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
let local_name = data.server_info.name.clone();
|
||||
match crate::federation::accept_invite(
|
||||
&self.config.data_dir,
|
||||
invite_code,
|
||||
@@ -289,6 +290,7 @@ impl RpcHandler {
|
||||
&local_onion,
|
||||
&local_pubkey,
|
||||
local_fips_npub.as_deref(),
|
||||
local_name.as_deref(),
|
||||
|bytes| node_identity.sign(bytes),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -168,6 +168,27 @@ impl RpcHandler {
|
||||
let active = total > 0 && downloaded < total;
|
||||
let completed = total > 0 && downloaded >= total;
|
||||
|
||||
// Stall detection: if the progress-at timestamp hasn't advanced
|
||||
// for 30+ seconds while active, the download is wedged (usually
|
||||
// HTTP stream silently dropped and reqwest is waiting out its
|
||||
// read timeout). The UI uses this to surface a Cancel button
|
||||
// with explanatory copy.
|
||||
let stalled = if active {
|
||||
let last_at = update::DOWNLOAD_PROGRESS_AT
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if last_at > 0 {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
now.saturating_sub(last_at) > 30_000
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"current_version": state.current_version,
|
||||
"last_check": state.last_check,
|
||||
@@ -179,6 +200,7 @@ impl RpcHandler {
|
||||
"bytes_downloaded": downloaded,
|
||||
"total_bytes": total,
|
||||
"active": active,
|
||||
"stalled": stalled,
|
||||
}))
|
||||
} else { None },
|
||||
}))
|
||||
@@ -200,6 +222,13 @@ impl RpcHandler {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Cancel an in-flight or stuck download. Clears the live counters
|
||||
/// and staging dir so the UI returns to the "Download Update" state.
|
||||
pub(super) async fn handle_update_cancel_download(&self) -> Result<serde_json::Value> {
|
||||
update::cancel_download(&self.config.data_dir).await?;
|
||||
Ok(serde_json::json!({ "canceled": true }))
|
||||
}
|
||||
|
||||
/// Apply the staged update.
|
||||
pub(super) async fn handle_update_apply(&self) -> Result<serde_json::Value> {
|
||||
update::apply_update(&self.config.data_dir).await?;
|
||||
|
||||
@@ -121,6 +121,7 @@ pub async fn accept_invite(
|
||||
local_onion: &str,
|
||||
local_pubkey: &str,
|
||||
local_fips_npub: Option<&str>,
|
||||
local_name: Option<&str>,
|
||||
sign_fn: impl FnOnce(&[u8]) -> String,
|
||||
) -> Result<FederatedNode> {
|
||||
let ParsedInvite {
|
||||
@@ -131,6 +132,20 @@ pub async fn accept_invite(
|
||||
fips_npub,
|
||||
} = parse_invite(code)?;
|
||||
|
||||
// Refuse self-peering. If the invite's did / onion / pubkey matches
|
||||
// our own, adding it pollutes the federation list with a node that
|
||||
// sees itself as its own peer and causes sync loops. The user
|
||||
// almost certainly pasted the wrong invite.
|
||||
if did == local_did || pubkey == local_pubkey || {
|
||||
let a = onion.trim_end_matches(".onion");
|
||||
let b = local_onion.trim_end_matches(".onion");
|
||||
!a.is_empty() && a == b
|
||||
} {
|
||||
anyhow::bail!(
|
||||
"Refusing to federate with self — invite points at this node's own did / onion / pubkey"
|
||||
);
|
||||
}
|
||||
|
||||
// Make accept idempotent: drop any existing entry that conflicts with
|
||||
// this invite — same DID (same node, refreshing the link), same onion
|
||||
// (node rotated identity but kept its hidden service), or same pubkey
|
||||
@@ -190,6 +205,7 @@ pub async fn accept_invite(
|
||||
local_onion,
|
||||
local_pubkey,
|
||||
local_fips_npub,
|
||||
local_name,
|
||||
sign_fn,
|
||||
)
|
||||
.await;
|
||||
@@ -201,20 +217,22 @@ pub async fn accept_invite(
|
||||
/// Prefers FIPS (if the remote advertised an npub in their invite) and
|
||||
/// falls back to Tor. Signs the message with our ed25519 key so the
|
||||
/// remote peer can verify authenticity regardless of transport.
|
||||
async fn notify_join(
|
||||
pub(crate) async fn notify_join(
|
||||
remote_onion: &str,
|
||||
remote_fips_npub: Option<&str>,
|
||||
local_did: &str,
|
||||
local_onion: &str,
|
||||
local_pubkey: &str,
|
||||
local_fips_npub: Option<&str>,
|
||||
local_name: Option<&str>,
|
||||
sign_fn: impl FnOnce(&[u8]) -> String,
|
||||
) -> Result<()> {
|
||||
// Sign the canonical message: "peer-joined:{did}:{onion}:{pubkey}"
|
||||
// Signature domain intentionally unchanged — fips_npub is carried
|
||||
// as an unsigned informational field. The FIPS daemon's own Noise
|
||||
// handshake authenticates the actual transport session, so a
|
||||
// stripped/substituted npub here merely downgrades the path to Tor.
|
||||
// Signature domain intentionally unchanged — fips_npub + name are
|
||||
// carried as unsigned informational fields. Name is display-only
|
||||
// (any identity claim is anchored on the signed did/pubkey); the
|
||||
// FIPS daemon's own Noise handshake authenticates the transport
|
||||
// session regardless of the advertised npub.
|
||||
let sign_data = format!("peer-joined:{}:{}:{}", local_did, local_onion, local_pubkey);
|
||||
let signature = sign_fn(sign_data.as_bytes());
|
||||
|
||||
@@ -227,6 +245,9 @@ async fn notify_join(
|
||||
if let Some(npub) = local_fips_npub {
|
||||
params["fips_npub"] = serde_json::Value::String(npub.to_string());
|
||||
}
|
||||
if let Some(name) = local_name {
|
||||
params["name"] = serde_json::Value::String(name.to_string());
|
||||
}
|
||||
|
||||
let body = serde_json::json!({
|
||||
"method": "federation.peer-joined",
|
||||
|
||||
@@ -17,5 +17,5 @@ pub use storage::{
|
||||
add_node, fips_npub_for_onion, load_nodes, record_peer_transport, remove_node, save_nodes,
|
||||
set_trust_level, update_node,
|
||||
};
|
||||
pub use sync::{build_local_state, deploy_to_peer, sync_with_peer};
|
||||
pub use sync::{build_local_state, deploy_to_peer, sync_with_peer, sync_with_peer_by_did};
|
||||
pub use types::{AppStatus, FederatedNode, NodeStateSnapshot, TrustLevel};
|
||||
|
||||
@@ -82,6 +82,30 @@ pub async fn sync_with_peer(
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Convenience wrapper: look up a federated peer by DID, derive our
|
||||
/// own local_did / signing context from the node identity on disk, and
|
||||
/// call sync_with_peer. Used by transitive-discovery code paths where
|
||||
/// the caller only knows the peer's DID (e.g. the peer-joined RPC's
|
||||
/// follow-up task).
|
||||
pub async fn sync_with_peer_by_did(
|
||||
data_dir: &Path,
|
||||
peer_did: &str,
|
||||
) -> Result<NodeStateSnapshot> {
|
||||
let nodes = super::storage::load_nodes(data_dir).await?;
|
||||
let peer = nodes
|
||||
.into_iter()
|
||||
.find(|n| n.did == peer_did)
|
||||
.ok_or_else(|| anyhow::anyhow!("Unknown federation peer: {}", peer_did))?;
|
||||
|
||||
let identity_dir = data_dir.join("identity");
|
||||
let node_identity =
|
||||
crate::identity::NodeIdentity::load_or_create(&identity_dir).await?;
|
||||
let local_pubkey_hex = node_identity.pubkey_hex();
|
||||
let local_did = crate::identity::did_key_from_pubkey_hex(&local_pubkey_hex)?;
|
||||
|
||||
sync_with_peer(data_dir, &peer, &local_did, |data| node_identity.sign(data)).await
|
||||
}
|
||||
|
||||
/// Merge peers advertised by a Trusted federated node into our own
|
||||
/// federation list. New peers are added at `Observer` trust (not
|
||||
/// Trusted — that requires a direct invite). Existing peers get their
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
|
||||
use chrono::Timelike;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use tokio::fs;
|
||||
use tracing::{debug, info};
|
||||
|
||||
@@ -14,6 +14,27 @@ use tracing::{debug, info};
|
||||
/// download runs in one place at a time; no need for per-handler state.
|
||||
pub static DOWNLOAD_BYTES: AtomicU64 = AtomicU64::new(0);
|
||||
pub static DOWNLOAD_TOTAL: AtomicU64 = AtomicU64::new(0);
|
||||
/// Set true to ask the in-flight download loop to bail out at the next
|
||||
/// chunk boundary. Read via `is_canceled`; reset at the start of every
|
||||
/// `download_update` run. Also flipped by the `cancel_download` RPC.
|
||||
pub static DOWNLOAD_CANCEL: AtomicBool = AtomicBool::new(false);
|
||||
/// Monotonic ms timestamp of the last time DOWNLOAD_BYTES advanced.
|
||||
/// Lets `update.status` flag a download as "stalled" when no bytes have
|
||||
/// arrived for a while, so the UI can offer a Cancel button with more
|
||||
/// confidence than "looks stuck at 0%".
|
||||
pub static DOWNLOAD_PROGRESS_AT: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn now_ms() -> u64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn is_canceled() -> bool {
|
||||
DOWNLOAD_CANCEL.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
const DEFAULT_UPDATE_MANIFEST_URL: &str =
|
||||
"https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json";
|
||||
@@ -223,12 +244,20 @@ pub async fn download_update(data_dir: &Path) -> Result<DownloadProgress> {
|
||||
let mut downloaded = 0u64;
|
||||
let total_bytes: u64 = manifest.components.iter().map(|c| c.size_bytes).sum();
|
||||
|
||||
// Seed the live counters so polls during the handshake show the
|
||||
// right denominator immediately instead of 0/0 → NaN%.
|
||||
// Clear any stale cancel flag from a prior aborted run, then seed
|
||||
// the live counters so polls during the handshake show the right
|
||||
// denominator immediately instead of 0/0 → NaN%.
|
||||
DOWNLOAD_CANCEL.store(false, Ordering::Relaxed);
|
||||
DOWNLOAD_TOTAL.store(total_bytes, Ordering::Relaxed);
|
||||
DOWNLOAD_BYTES.store(0, Ordering::Relaxed);
|
||||
DOWNLOAD_PROGRESS_AT.store(now_ms(), Ordering::Relaxed);
|
||||
|
||||
for component in &manifest.components {
|
||||
if is_canceled() {
|
||||
DOWNLOAD_TOTAL.store(0, Ordering::Relaxed);
|
||||
DOWNLOAD_BYTES.store(0, Ordering::Relaxed);
|
||||
anyhow::bail!("Download canceled");
|
||||
}
|
||||
info!(name = %component.name, url = %component.download_url, "Downloading component");
|
||||
let dest = staging_dir.join(&component.name);
|
||||
download_component_resumable(&client, component, &dest, downloaded).await?;
|
||||
@@ -289,7 +318,18 @@ async fn download_component_resumable(
|
||||
delay,
|
||||
last_err.as_ref().map(|e| e.to_string()).unwrap_or_default()
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
|
||||
// Sleep in 500ms slices so a Cancel during backoff wakes
|
||||
// promptly instead of waiting out the full exponential window.
|
||||
let slices = delay * 2;
|
||||
for _ in 0..slices {
|
||||
if is_canceled() {
|
||||
anyhow::bail!("Download canceled");
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
if is_canceled() {
|
||||
anyhow::bail!("Download canceled");
|
||||
}
|
||||
|
||||
let mut req = client.get(&component.download_url);
|
||||
@@ -348,7 +388,12 @@ async fn download_component_resumable(
|
||||
let mut resp = resp;
|
||||
let mut stream_err = false;
|
||||
let mut on_disk = existing_len;
|
||||
let mut canceled = false;
|
||||
loop {
|
||||
if is_canceled() {
|
||||
canceled = true;
|
||||
break;
|
||||
}
|
||||
match resp.chunk().await {
|
||||
Ok(Some(bytes)) => {
|
||||
if let Err(e) = file.write_all(&bytes).await {
|
||||
@@ -361,6 +406,7 @@ async fn download_component_resumable(
|
||||
prior_total + on_disk.min(component.size_bytes),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
DOWNLOAD_PROGRESS_AT.store(now_ms(), Ordering::Relaxed);
|
||||
}
|
||||
Ok(None) => break, // stream ended cleanly
|
||||
Err(e) => {
|
||||
@@ -370,6 +416,13 @@ async fn download_component_resumable(
|
||||
}
|
||||
}
|
||||
}
|
||||
if canceled {
|
||||
let _ = file.flush().await;
|
||||
drop(file);
|
||||
DOWNLOAD_TOTAL.store(0, Ordering::Relaxed);
|
||||
DOWNLOAD_BYTES.store(0, Ordering::Relaxed);
|
||||
anyhow::bail!("Download canceled");
|
||||
}
|
||||
let _ = file.flush().await;
|
||||
let _ = file.sync_all().await;
|
||||
drop(file);
|
||||
@@ -414,6 +467,30 @@ async fn download_component_resumable(
|
||||
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("download failed without a captured error")))
|
||||
}
|
||||
|
||||
/// Cancel an in-flight download. Sets the cancellation flag so the
|
||||
/// download loop bails out at the next chunk or backoff boundary, then
|
||||
/// zeros the live counters and wipes the staging directory so the UI
|
||||
/// sees "no active download" immediately and the next attempt starts
|
||||
/// clean. Safe to call even when no download is running.
|
||||
pub async fn cancel_download(data_dir: &Path) -> Result<()> {
|
||||
DOWNLOAD_CANCEL.store(true, Ordering::Relaxed);
|
||||
DOWNLOAD_BYTES.store(0, Ordering::Relaxed);
|
||||
DOWNLOAD_TOTAL.store(0, Ordering::Relaxed);
|
||||
let staging = data_dir.join("update-staging");
|
||||
if staging.exists() {
|
||||
let _ = tokio::fs::remove_dir_all(&staging).await;
|
||||
}
|
||||
// Clear the "downloaded, ready to apply" marker too — a canceled
|
||||
// download is not a staged update.
|
||||
if let Ok(mut state) = load_state(data_dir).await {
|
||||
if state.update_in_progress {
|
||||
state.update_in_progress = false;
|
||||
let _ = save_state(data_dir, &state).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a command as root, but *outside* the archipelago service's
|
||||
/// restricted mount namespace.
|
||||
///
|
||||
|
||||
@@ -696,7 +696,15 @@
|
||||
"gitMethodHint": "This node builds from source. Update will git-pull, rebuild the backend and UI, then restart — takes a few minutes.",
|
||||
"gitApplyTitle": "Pull & Rebuild?",
|
||||
"gitApplyMessage": "Archipelago will pull the latest code, rebuild, and restart. This can take several minutes and the UI will be briefly unavailable.",
|
||||
"gitApplyStarted": "Update started. The backend will rebuild and restart — this can take a few minutes."
|
||||
"gitApplyStarted": "Update started. The backend will rebuild and restart — this can take a few minutes.",
|
||||
"cancelDownload": "Cancel Download",
|
||||
"cancelingDownload": "Canceling…",
|
||||
"cancelDownloadTitle": "Cancel Download?",
|
||||
"cancelDownloadConfirm": "This will stop the current download and discard the partial file. You can start again from scratch afterwards.",
|
||||
"cancelDownloadButton": "Cancel Download",
|
||||
"cancelDownloadSuccess": "Download canceled. You can try again.",
|
||||
"cancelDownloadFailed": "Failed to cancel download.",
|
||||
"downloadStalled": "Download appears stuck — try Cancel and start again."
|
||||
},
|
||||
"kiosk": {
|
||||
"pressEsc": "Press ESC to exit",
|
||||
|
||||
@@ -684,18 +684,26 @@
|
||||
"rollbackSuccess": "Se revirti\u00f3 a la versi\u00f3n anterior. El servicio se reiniciar\u00e1.",
|
||||
"rollbackFailed": "Error al revertir.",
|
||||
"pullAndRebuild": "Pull y Recompilar",
|
||||
"finishingDownload": "Terminando descarga — verificando checksum…",
|
||||
"overlayApplying": "Instalando actualizaci\u00f3n…",
|
||||
"overlayRestarting": "Reiniciando servidor…",
|
||||
"overlayReconnecting": "Reconectando a la nueva versi\u00f3n…",
|
||||
"overlayReady": "Actualizaci\u00f3n instalada — recargando…",
|
||||
"finishingDownload": "Terminando descarga \u2014 verificando checksum\u2026",
|
||||
"overlayApplying": "Instalando actualizaci\u00f3n\u2026",
|
||||
"overlayRestarting": "Reiniciando servidor\u2026",
|
||||
"overlayReconnecting": "Reconectando a la nueva versi\u00f3n\u2026",
|
||||
"overlayReady": "Actualizaci\u00f3n instalada \u2014 recargando\u2026",
|
||||
"overlayStalled": "Tardando m\u00e1s de lo esperado",
|
||||
"overlayTarget": "Instalando v{version}",
|
||||
"overlayReloadNow": "Recargar ahora",
|
||||
"gitMethodHint": "Este nodo compila desde el c\u00f3digo fuente. La actualizaci\u00f3n har\u00e1 git-pull, recompilar\u00e1 y reiniciar\u00e1 — tarda unos minutos.",
|
||||
"gitMethodHint": "Este nodo compila desde el c\u00f3digo fuente. La actualizaci\u00f3n har\u00e1 git-pull, recompilar\u00e1 y reiniciar\u00e1 \u2014 tarda unos minutos.",
|
||||
"gitApplyTitle": "\u00bfPull y Recompilar?",
|
||||
"gitApplyMessage": "Archipelago descargar\u00e1 el c\u00f3digo m\u00e1s reciente, lo compilar\u00e1 y reiniciar\u00e1. Puede tardar varios minutos y la UI estar\u00e1 brevemente no disponible.",
|
||||
"gitApplyStarted": "Actualizaci\u00f3n iniciada. El backend se recompilar\u00e1 y reiniciar\u00e1 — puede tardar unos minutos."
|
||||
"gitApplyStarted": "Actualizaci\u00f3n iniciada. El backend se recompilar\u00e1 y reiniciar\u00e1 \u2014 puede tardar unos minutos.",
|
||||
"cancelDownload": "Cancelar descarga",
|
||||
"cancelingDownload": "Cancelando\u2026",
|
||||
"cancelDownloadTitle": "\u00bfCancelar descarga?",
|
||||
"cancelDownloadConfirm": "Esto detendr\u00e1 la descarga actual y descartar\u00e1 el archivo parcial. Podr\u00e1s volver a empezar desde cero.",
|
||||
"cancelDownloadButton": "Cancelar descarga",
|
||||
"cancelDownloadSuccess": "Descarga cancelada. Puedes intentarlo de nuevo.",
|
||||
"cancelDownloadFailed": "No se pudo cancelar la descarga.",
|
||||
"downloadStalled": "La descarga parece atascada \u2014 prueba a cancelar y volver a empezar."
|
||||
},
|
||||
"kiosk": {
|
||||
"pressEsc": "Presione ESC para salir",
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useTransportStore } from '@/stores/transport'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
@@ -537,6 +537,8 @@ async function rotateDid(password: string) {
|
||||
}
|
||||
}
|
||||
|
||||
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
loadNodes()
|
||||
loadDwnStatus()
|
||||
@@ -549,5 +551,16 @@ onMounted(async () => {
|
||||
} catch {
|
||||
// Self DID not available
|
||||
}
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
loadNodes()
|
||||
loadPendingRequests()
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer)
|
||||
autoRefreshTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -109,17 +109,30 @@
|
||||
<h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.downloading') }}</h2>
|
||||
<div class="w-full h-3 bg-white/10 rounded-full overflow-hidden mb-2">
|
||||
<div
|
||||
class="h-full bg-orange-400 rounded-full transition-all duration-500"
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:class="downloadStalled ? 'bg-amber-400' : 'bg-orange-400'"
|
||||
:style="{ width: downloadPercentFormatted + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="downloadFinishing" class="w-3 h-3 border-2 border-orange-400 border-t-transparent rounded-full animate-spin shrink-0"></div>
|
||||
<p class="text-xs text-white/60">
|
||||
{{ downloadFinishing
|
||||
? t('systemUpdate.finishingDownload')
|
||||
: t('systemUpdate.percentComplete', { percent: downloadPercentFormatted }) }}
|
||||
</p>
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div v-if="downloadFinishing && !downloadStalled" class="w-3 h-3 border-2 border-orange-400 border-t-transparent rounded-full animate-spin shrink-0"></div>
|
||||
<p class="text-xs" :class="downloadStalled ? 'text-amber-300' : 'text-white/60'">
|
||||
{{ downloadStalled
|
||||
? t('systemUpdate.downloadStalled')
|
||||
: downloadFinishing
|
||||
? t('systemUpdate.finishingDownload')
|
||||
: t('systemUpdate.percentComplete', { percent: downloadPercentFormatted }) }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="requestCancelDownload"
|
||||
:disabled="cancelingDownload"
|
||||
class="glass-button rounded-lg px-4 py-1.5 text-xs font-medium disabled:opacity-40 shrink-0"
|
||||
:class="downloadStalled ? 'bg-amber-500/20 border-amber-400/40 text-amber-200' : ''"
|
||||
>
|
||||
{{ cancelingDownload ? t('systemUpdate.cancelingDownload') : t('systemUpdate.cancelDownload') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,14 +266,18 @@
|
||||
? t('systemUpdate.rollbackTitle')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.gitApplyTitle')
|
||||
: t('systemUpdate.applyTitle') }}
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadTitle')
|
||||
: t('systemUpdate.applyTitle') }}
|
||||
</h3>
|
||||
<p class="text-sm text-white/70 mb-6">
|
||||
{{ confirmAction === 'rollback'
|
||||
? t('systemUpdate.rollbackMessage')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.gitApplyMessage')
|
||||
: t('systemUpdate.applyMessage') }}
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadConfirm')
|
||||
: t('systemUpdate.applyMessage') }}
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button @click="cancelConfirm" class="glass-button rounded-lg px-4 py-2 text-sm font-medium">
|
||||
@@ -269,13 +286,15 @@
|
||||
<button
|
||||
@click="executeConfirm"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium"
|
||||
:class="confirmAction === 'rollback' ? 'bg-red-500/20 border-red-400/30' : 'bg-orange-500/20 border-orange-400/30'"
|
||||
:class="(confirmAction === 'rollback' || confirmAction === 'cancel-download') ? 'bg-red-500/20 border-red-400/30' : 'bg-orange-500/20 border-orange-400/30'"
|
||||
>
|
||||
{{ confirmAction === 'rollback'
|
||||
? t('systemUpdate.rollbackButton')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.pullAndRebuild')
|
||||
: t('systemUpdate.applyNow') }}
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadButton')
|
||||
: t('systemUpdate.applyNow') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,7 +332,9 @@ const loading = ref(false)
|
||||
const downloading = ref(false)
|
||||
const downloaded = ref(false)
|
||||
const applying = ref(false)
|
||||
const confirmAction = ref<'apply' | 'git-apply' | 'rollback' | null>(null)
|
||||
const cancelingDownload = ref(false)
|
||||
const downloadStalled = ref(false)
|
||||
const confirmAction = ref<'apply' | 'git-apply' | 'rollback' | 'cancel-download' | null>(null)
|
||||
const currentVersion = ref('0.0.0')
|
||||
const lastCheck = ref<string | null>(null)
|
||||
const updateInfo = ref<UpdateDetail | null>(null)
|
||||
@@ -335,13 +356,16 @@ async function pollDownloadProgress(): Promise<boolean> {
|
||||
bytes_downloaded: number
|
||||
total_bytes: number
|
||||
active: boolean
|
||||
stalled?: boolean
|
||||
} | null
|
||||
}>({ method: 'update.status' })
|
||||
const p = res.download_progress
|
||||
if (p && p.total_bytes > 0) {
|
||||
downloadPercent.value = Math.min(100, (p.bytes_downloaded / p.total_bytes) * 100)
|
||||
downloadStalled.value = !!p.stalled
|
||||
return p.active
|
||||
}
|
||||
downloadStalled.value = false
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
@@ -547,6 +571,10 @@ function requestRollback() {
|
||||
confirmAction.value = 'rollback'
|
||||
}
|
||||
|
||||
function requestCancelDownload() {
|
||||
confirmAction.value = 'cancel-download'
|
||||
}
|
||||
|
||||
function cancelConfirm() {
|
||||
confirmAction.value = null
|
||||
}
|
||||
@@ -560,6 +588,25 @@ async function executeConfirm() {
|
||||
await applyUpdateGitWithOverlay()
|
||||
} else if (action === 'rollback') {
|
||||
await rollbackUpdate()
|
||||
} else if (action === 'cancel-download') {
|
||||
await cancelDownload()
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelDownload() {
|
||||
cancelingDownload.value = true
|
||||
try {
|
||||
await rpcClient.call({ method: 'update.cancel-download' })
|
||||
downloading.value = false
|
||||
downloaded.value = false
|
||||
downloadPercent.value = 0
|
||||
downloadStalled.value = false
|
||||
showStatus(t('systemUpdate.cancelDownloadSuccess'))
|
||||
} catch (e) {
|
||||
showStatus(t('systemUpdate.cancelDownloadFailed'), true)
|
||||
if (import.meta.env.DEV) console.warn('Cancel download failed', e)
|
||||
} finally {
|
||||
cancelingDownload.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,219 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.7.17-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.17-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>When a download gets stuck, you can now cancel it. A new Cancel Download button sits next to the progress bar — it stops the transfer, clears the partial file, and returns you to a clean state so you can retry. No more staring at a frozen bar with no way to recover.</p>
|
||||
<p>Downloads that stall for 30 seconds or more now say so. The progress bar turns amber and shows 'Download appears stuck — try Cancel and start again' instead of just sitting silently at whatever percent it reached.</p>
|
||||
<p>Canceling is fast. It no longer has to wait out the retry timer — the download bails within half a second, so you're not stuck watching a stuck screen while you wait to unstick it.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.16-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.16-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Federation is now bidirectional and instant. When someone joins using your invite code, their node appears on your Federation page automatically — no need for the inviter to click Sync or wait for the next poll. Names and node details populate within seconds of the handshake finishing.</p>
|
||||
<p>New nodes can no longer federate with themselves. Accepting an invite that points back at the local node (by DID, public key, or onion address) is rejected up front, so self-peering no longer clutters the node list with a duplicate card.</p>
|
||||
<p>Transitive discovery: if nodes A and B are already federated and node C joins A, all three nodes now learn about each other. The new peer is pulled in as an Observer entry on existing federation members, so you can promote to Trusted with one click instead of trading a second invite code.</p>
|
||||
<p>The Federation page auto-refreshes every five seconds while it's open. Status changes, new peers, and incoming join requests surface on their own — clicking Sync remains available for an on-demand pull.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.15-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.15-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Updates survive network hiccups. Downloads now resume from exactly where a dropped connection left off, and retry up to 6 times with increasing gaps between attempts, instead of restarting from byte zero or giving up.</p>
|
||||
<p>The download progress bar now shows real progress. Instead of a fake number that creeps to 95% and freezes, you see the actual bytes arriving, and it continues to update correctly even if you navigate away and come back.</p>
|
||||
<p>Update check itself retries on slow responses. If git.tx1138.com is momentarily overloaded, the node tries three times with a five-second wait between attempts before concluding you're up to date.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.14-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.14-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Installing an update now shows a full-screen progress overlay with the Archipelago logo, a status message, and an animated bar. The page reloads itself automatically once the new version is up — no manual refresh. If something stalls, a 'Reload now' button appears after a few minutes.</p>
|
||||
<p>Download progress no longer looks frozen near the end. The bar pauses at 95% with a 'Finishing download — verifying checksum…' message and spinner while the last bytes arrive and are hashed.</p>
|
||||
<p>FIPS Reconnect now genuinely tries to fix the anchor. It runs a proper recovery sequence (stop → start → wait for the bootstrap window → check peers) and tells you the likely reason it's still unreachable — corrupt identity key, seed not unlocked, network blocking UDP, or the anchor server being down — instead of a generic 'try again'.</p>
|
||||
<p>Healed a latent FIPS identity bug: the public-key file was being written in text form (an 'npub1…' string) on some nodes, which the daemon couldn't parse and silently authenticated with a garbage key. The Reconnect button now rewrites the file in the correct binary format and re-installs the config before restarting — nodes stuck with no peers for 'no reason' should come back online.</p>
|
||||
<p>AIUI (Claude sidebar) is back. The installer now ships AIUI in the frontend bundle and preserves it across future updates — it was being wiped on every OTA because it lived outside the Vue build.</p>
|
||||
<p>Installing a big app (IndeedHub, Bitcoin, Penpot) no longer gives up early and shows 'didn't work' while the download is still running in the background. The client waits up to 45 minutes for the install pipeline to finish.</p>
|
||||
<p>'Rollback to Previous' is now labelled 'Rollback Available' — clearer that it's a choice you have, not a status you're stuck with.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.13-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.13-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>App catalog now loads reliably. Before, the Marketplace / Discover page couldn't fetch the catalog of apps because the upstream host wasn't sending the right CORS headers and the node's security policy didn't allow the fallback URL either. The node now fetches the catalog server-side and serves it same-origin to the browser — no more blank app lists.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.12-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.12-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Nothing new — version bump so freshly-installed nodes (from the 1.7.11 ISO) have something to OTA down, confirming the end-to-end update pipeline out of the box.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.11-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.11-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>OTA proof release — first version where Install Update should run clean from the UI with no manual steps. Click it and watch the sidebar flip to 1.7.11-alpha on its own.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.10-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.10-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Install Update actually applies now. The installer had to write into system folders that the backend service was sandboxed out of — every earlier 'Failed to apply update' was a layer of that onion. Fixed by running the file swaps in a separate system context.</p>
|
||||
<p>FIPS status on the Home and Server pages now reflects whether the public anchor is reachable. You'll see 'Active · N peers' (green) when healthy or 'No anchor' (orange) when the network is blocking the bootstrap — same signal as the full FIPS card.</p>
|
||||
<p>Pasting an https://… URL into the profile picture or banner now previews correctly. Before, if the URL failed to load, the UI would silently blank out instead of showing your initial as a placeholder.</p>
|
||||
<p>Uploaded profile pictures under 64 KB are now embedded directly in your Nostr profile (as a data URL), so any Nostr client can see them — not just ones routing over Tor. Larger uploads keep the onion URL for now, with a hint to paste a public URL for wider visibility.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.9-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.9-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>OTA verification release — nothing new to see. Click Install Update, grab a coffee, and watch the sidebar flip to 1.7.9-alpha on its own. If this one works end to end, the pipeline is solid and future updates will flow the same way.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.8-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.8-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Install Update finally works end-to-end over the air. The installer was trying to overwrite the running backend binary with a tool that fails on in-use files (ETXTBSY) — swapped it for an atomic rename, which the kernel allows on a live executable. Every previous 'Failed to apply update' attempt was this one root cause.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.7-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.7-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Over-the-air update test — no feature changes, just a version bump so your node can walk through the whole update flow end-to-end using the new robust installer. Safe to apply; nothing to do afterwards.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.6-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.6-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Install Update is now more robust. Each install gets its own uniquely-named staging folder and then moves files into place — the previous version had a small cleanup step that could hit a transient filesystem hiccup and bail out halfway. You'll also still see a rollback folder after a successful install.</p>
|
||||
<p>Dev-box OTA: nodes that build archipelago from source can now opt into the standard Download → Install flow instead of Pull & Rebuild, by setting ARCHIPELAGO_UPDATE_URL in the service environment. Useful when the dev machine has a checked-out repo but you want to test the regular update path.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.5-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.5-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Over-the-air update test — no feature changes, just a fresh version number so your node can walk through the whole update flow end-to-end: check, download, install, auto-restart. Safe to apply; nothing to do afterwards.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.4-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.4-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Install Update actually installs now. Before, the final step extracted the new UI into the wrong folder and bailed with 'Failed to apply update' — your node ended up backing up cleanly but never swapping in the new files. Fixed.</p>
|
||||
<p>Download progress no longer overshoots 100%. You'll see the bar climb smoothly to 95% and then jump to 100% when the download actually finishes.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.3-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.3-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>The version number in the sidebar now always matches the actual running version — no more lying to you about being on an older release after an update.</p>
|
||||
<p>FIPS Mesh card on the server page: cleaner layout on desktop (no more awkward gaps), and a one-click Reconnect button when the public anchor is unreachable — it restarts the FIPS daemon so it can re-bootstrap from the anchor.</p>
|
||||
<p>Profile pictures now show correctly in the identity list and editor. Before, uploaded images silently failed to render because the URL was only reachable over Tor; the UI now rewrites them to a local path while keeping the external URL for other Nostr clients.</p>
|
||||
<p>Identity rows now show your Display Name first (from your Nostr profile) with the internal identity name beside it in parentheses, so you see the name other people will see — not just the one you picked when creating it.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.2-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.2-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Install Update now actually installs. Before, the button would back up your current version then fail with 'Failed to apply update' because the installer couldn't write into system folders.</p>
|
||||
<p>The button's also been renamed to 'Install Update' (previously 'Apply Update') and the node restarts itself a moment after you click it — no more manual restart step.</p>
|
||||
<p>Your existing identities now show the generated avatar instead of just their initials — same look as freshly created ones.</p>
|
||||
<p>Everything from 1.7.0-alpha and 1.7.1-alpha carries over (default avatars on creation, one-click Save publishes to Nostr relays, public blob URLs for profile pictures, 30-minute download window, VPN peer restore on reboot, reconciler-only-repairs, filebrowser fix).</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.1-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.1-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Over-the-air update test — same features as 1.7.0, just a fresh version number so your node can try the new download-and-apply flow end-to-end. Safe to apply; nothing to do afterwards.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.0-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.0-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Every identity now gets a personal avatar the moment it's created. Your main node identity gets a distinctive hexagonal-network icon; other identities get a colourful generated pattern unique to each one.</p>
|
||||
<p>Profile editor: upload a profile picture and a banner, then tap Save — your Nostr profile now goes out to the relays in one step. No more 'Save' vs 'Save & Publish' confusion.</p>
|
||||
<p>Profile pictures and banners you upload are now reachable by other Nostr clients across the network — not just your own browser. Anyone who sees your profile on a relay can load the image.</p>
|
||||
<p>Update downloads on slow connections no longer cut out right at the end. The client waits up to 30 minutes for each component instead of giving up after 15 seconds.</p>
|
||||
<p>When you move a node to a new version without going through Check for Updates (for example via a reinstall or manual copy), it now reports the new version correctly instead of endlessly saying 'update available'.</p>
|
||||
<p>Your VPN peers come back automatically after a reboot. No more rescanning QR codes on your phone or laptop.</p>
|
||||
<p>Fresh installs stay lean — only File Browser is included out of the box. Other apps wait in the Marketplace until you pick them.</p>
|
||||
<p>File Browser stops rebooting itself every few hours — the housekeeper now leaves it alone once it's healthy.</p>
|
||||
<p>One-click 'Pull & Rebuild' button works for nodes that update from source (the development path), not just the standard download path.</p>
|
||||
<p>The download progress number is now clean (like 45.23%) instead of 45.270894%.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.3.5 -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"version": "1.7.15-alpha",
|
||||
"version": "1.7.17-alpha",
|
||||
"release_date": "2026-04-20",
|
||||
"changelog": [
|
||||
"Updates survive network hiccups. Downloads now resume from exactly where a dropped connection left off, and retry up to 6 times with increasing gaps between attempts, instead of restarting from byte zero or giving up.",
|
||||
"The download progress bar now shows real progress. Instead of a fake number that creeps to 95% and freezes, you see the actual bytes arriving, and it continues to update correctly even if you navigate away and come back.",
|
||||
"Update check itself retries on slow responses. If git.tx1138.com is momentarily overloaded, the node tries three times with a five-second wait between attempts before concluding you're up to date."
|
||||
"When a download gets stuck, you can now cancel it. A new Cancel Download button sits next to the progress bar — it stops the transfer, clears the partial file, and returns you to a clean state so you can retry. No more staring at a frozen bar with no way to recover.",
|
||||
"Downloads that stall for 30 seconds or more now say so. The progress bar turns amber and shows 'Download appears stuck — try Cancel and start again' instead of just sitting silently at whatever percent it reached.",
|
||||
"Canceling is fast. It no longer has to wait out the retry timer — the download bails within half a second, so you're not stuck watching a stuck screen while you wait to unstick it."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.14-alpha",
|
||||
"new_version": "1.7.15-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.15-alpha/archipelago",
|
||||
"sha256": "1070c87fd24fc56b2edcb6ea37f42fa47dfbdc9a4840151f723bbc9c081c162b",
|
||||
"size_bytes": 40584792
|
||||
"current_version": "1.7.16-alpha",
|
||||
"new_version": "1.7.17-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.17-alpha/archipelago",
|
||||
"sha256": "57020053d8c587feb9e4761ca66dd3fac43edafe0e8198c399e7ca4246e7752d",
|
||||
"size_bytes": 40649896
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.15-alpha.tar.gz",
|
||||
"current_version": "1.7.14-alpha",
|
||||
"new_version": "1.7.15-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.15-alpha/archipelago-frontend-1.7.15-alpha.tar.gz",
|
||||
"sha256": "8e630ebaddf88ac0e0500eeb80cfea24e6cd87c41c0d6b934e66d7b7f63fd43f",
|
||||
"size_bytes": 162078068
|
||||
"name": "archipelago-frontend-1.7.17-alpha.tar.gz",
|
||||
"current_version": "1.7.16-alpha",
|
||||
"new_version": "1.7.17-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.17-alpha/archipelago-frontend-1.7.17-alpha.tar.gz",
|
||||
"sha256": "59679f6d45c11f44ffb5dbd060ffca00022789aa830e731640bcb41be07d7a93",
|
||||
"size_bytes": 162083786
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
releases/v1.7.16-alpha/archipelago
Executable file
BIN
releases/v1.7.16-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.16-alpha/archipelago-frontend-1.7.16-alpha.tar.gz
Normal file
BIN
releases/v1.7.16-alpha/archipelago-frontend-1.7.16-alpha.tar.gz
Normal file
Binary file not shown.
BIN
releases/v1.7.17-alpha/archipelago
Executable file
BIN
releases/v1.7.17-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.17-alpha/archipelago-frontend-1.7.17-alpha.tar.gz
Normal file
BIN
releases/v1.7.17-alpha/archipelago-frontend-1.7.17-alpha.tar.gz
Normal file
Binary file not shown.
Reference in New Issue
Block a user