release(v1.7.2-alpha): fix Install Update + identity avatar backfill + label

Three user-visible fixes shipped together.

1. update.apply permission-denied
   apply_update() was doing fs::copy into /usr/local/bin/archipelago and
   tar xzf into /opt/archipelago as the archipelago user — both root-owned.
   The backup step succeeded (it wrote to data_dir) but the swap failed
   with a silent permission denied, wrapped as "Failed to apply archipelago".
   Now uses `sudo install -m 0755` for the binary and `sudo tar -xzf` for
   the frontend, plus a post-apply `sudo systemctl --no-block restart
   archipelago` scheduled 2s after the RPC reply so the UI sees success.

2. Apply → Install label
   en/es locale strings: applyUpdate / applyTitle / applyNow changed from
   "Apply" to "Install". Matches the user's mental model and distinguishes
   the user-facing verb from the internal apply_update() function.

3. Identity avatar backfill
   Identities created before df83163f had profile=None on disk and so
   rendered as initials. load_record() now synthesizes an IdentityProfile
   with a default picture (identicon for regular identities, the hex node
   SVG for derivation_index=0) when profile is missing. The synthetic
   profile lives only in the returned record; the file stays untouched so
   a later explicit Save persists whatever the user actually chose.

Artefacts:
  archipelago                                        70e5444e…67c589  40381960
  archipelago-frontend-1.7.2-alpha.tar.gz            806b027b…358a824 76983699

Changelog rewritten layman-style per saved feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-20 11:25:10 -04:00
parent 0ea9ad9adb
commit 66b4e2b313
6 changed files with 73 additions and 23 deletions

2
core/Cargo.lock generated
View File

@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "archipelago" name = "archipelago"
version = "1.7.1-alpha" version = "1.7.2-alpha"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"archipelago-container", "archipelago-container",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "archipelago" name = "archipelago"
version = "1.7.1-alpha" version = "1.7.2-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"]

View File

@ -739,6 +739,20 @@ impl IdentityManager {
.and_then(|pk| pk.to_bech32().ok()) .and_then(|pk| pk.to_bech32().ok())
}); });
// Backfill a default avatar for identities created before the
// default-avatar feature shipped. The synthetic profile lives only
// in the returned record — we don't rewrite the file on disk,
// since a later explicit save will persist whatever the user
// actually chose. Master identities (seed index 0) get the hex
// node SVG; all other pre-existing identities get the identicon.
let profile = file.profile.or_else(|| {
let is_master = file.derivation_index == Some(0);
Some(IdentityProfile {
picture: Some(crate::avatar::default_picture(&file.pubkey_hex, is_master)),
..Default::default()
})
});
Ok(IdentityRecord { Ok(IdentityRecord {
id: file.id, id: file.id,
name: file.name, name: file.name,
@ -749,7 +763,7 @@ impl IdentityManager {
created_at: file.created_at, created_at: file.created_at,
nostr_pubkey: file.nostr_pubkey_hex, nostr_pubkey: file.nostr_pubkey_hex,
nostr_npub, nostr_npub,
profile: file.profile, profile,
}) })
} }

View File

@ -277,25 +277,42 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
match name.as_str() { match name.as_str() {
"archipelago" => { "archipelago" => {
let dest = Path::new("/usr/local/bin/archipelago"); // /usr/local/bin is root-owned; archipelago user can't
fs::copy(&src, dest) // fs::copy into it directly. Use sudo install which handles
// the copy, mode, and ownership atomically.
let status = tokio::process::Command::new("sudo")
.args([
"install",
"-m",
"0755",
"-o",
"root",
"-g",
"root",
&src.to_string_lossy(),
"/usr/local/bin/archipelago",
])
.status()
.await .await
.with_context(|| format!("Failed to apply {}", name))?; .with_context(|| format!("Failed to spawn install for {}", name))?;
#[cfg(unix)] if !status.success() {
{ anyhow::bail!(
use std::os::unix::fs::PermissionsExt; "sudo install failed for {} (exit {:?})",
std::fs::set_permissions(dest, std::fs::Permissions::from_mode(0o755)) name,
.context("Failed to set binary permissions")?; status.code()
);
} }
info!(name = %name, "Backend binary applied"); info!(name = %name, "Backend binary applied");
} }
_ if name.contains("frontend") && name.ends_with(".tar.gz") => { _ if name.contains("frontend") && name.ends_with(".tar.gz") => {
let web_ui_dir = Path::new("/opt/archipelago/web-ui"); let web_ui_dir = Path::new("/opt/archipelago/web-ui");
// Back up current frontend // Back up current frontend. /opt/archipelago is root-owned;
// the backup goes under our data_dir where we can write.
let frontend_backup = backup_dir.join("web-ui-backup.tar.gz"); let frontend_backup = backup_dir.join("web-ui-backup.tar.gz");
if web_ui_dir.exists() { if web_ui_dir.exists() {
let status = tokio::process::Command::new("tar") let status = tokio::process::Command::new("sudo")
.args([ .args([
"tar",
"-czf", "-czf",
&frontend_backup.to_string_lossy(), &frontend_backup.to_string_lossy(),
"-C", "-C",
@ -309,15 +326,21 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
info!("Frontend backed up"); info!("Frontend backed up");
} }
} }
// Extract new frontend // Extract new frontend into /opt/archipelago (root-owned dir).
let status = tokio::process::Command::new("tar") let status = tokio::process::Command::new("sudo")
.args(["-xzf", &src.to_string_lossy(), "-C", "/opt/archipelago"]) .args(["tar", "-xzf", &src.to_string_lossy(), "-C", "/opt/archipelago"])
.status() .status()
.await .await
.with_context(|| format!("Failed to extract {}", name))?; .with_context(|| format!("Failed to extract {}", name))?;
if !status.success() { if !status.success() {
anyhow::bail!("tar extraction failed for {}", name); anyhow::bail!("tar extraction failed for {}", name);
} }
// nginx serves this tree; keep ownership consistent with
// what first-boot + the ISO layout expect.
let _ = tokio::process::Command::new("sudo")
.args(["chown", "-R", "archipelago:archipelago", "/opt/archipelago/web-ui"])
.status()
.await;
info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui"); info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui");
} }
_ => { _ => {
@ -339,7 +362,20 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
// Clean staging // Clean staging
let _ = fs::remove_dir_all(&staging_dir).await; let _ = fs::remove_dir_all(&staging_dir).await;
info!("Update applied. Restart service to take effect."); info!("Update applied — scheduling service restart in 2s so the RPC reply lands first");
// Restart asynchronously so the JSON-RPC response actually reaches the
// UI before systemd kills us. --no-block makes sure systemctl doesn't
// try to wait for the current service (us) to exit cleanly before
// starting the new process — it would deadlock otherwise.
tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let _ = tokio::process::Command::new("sudo")
.args(["systemctl", "--no-block", "restart", "archipelago"])
.status()
.await;
});
Ok(()) Ok(())
} }

View File

@ -663,18 +663,18 @@
"autoApply": "Auto-Apply", "autoApply": "Auto-Apply",
"autoApplyDesc": "Check daily and automatically install updates at 3 AM. Service restarts as needed.", "autoApplyDesc": "Check daily and automatically install updates at 3 AM. Service restarts as needed.",
"downloadUpdate": "Download Update", "downloadUpdate": "Download Update",
"applyUpdate": "Apply Update", "applyUpdate": "Install Update",
"checkForUpdates": "Check for Updates", "checkForUpdates": "Check for Updates",
"checking": "Checking...", "checking": "Checking...",
"rollback": "Rollback to Previous", "rollback": "Rollback to Previous",
"backToSettings": "Back to Settings", "backToSettings": "Back to Settings",
"percentComplete": "{percent}% complete", "percentComplete": "{percent}% complete",
"applyWarning": "Installing components and restarting services. Do not power off.", "applyWarning": "Installing components and restarting services. Do not power off.",
"applyTitle": "Apply Update?", "applyTitle": "Install Update?",
"applyMessage": "The backend service will restart. This may take a moment.", "applyMessage": "The backend service will restart. This may take a moment.",
"rollbackTitle": "Rollback Version?", "rollbackTitle": "Rollback Version?",
"rollbackMessage": "This will restore the previous version. The backend service will restart.", "rollbackMessage": "This will restore the previous version. The backend service will restart.",
"applyNow": "Apply Now", "applyNow": "Install Now",
"rollbackButton": "Rollback", "rollbackButton": "Rollback",
"upToDateMessage": "Your system is up to date. No updates available. Your system is running the latest version.", "upToDateMessage": "Your system is up to date. No updates available. Your system is running the latest version.",
"checkFailed": "Failed to check for updates. Check your internet connection.", "checkFailed": "Failed to check for updates. Check your internet connection.",

View File

@ -662,18 +662,18 @@
"autoApply": "Aplicaci\u00f3n autom\u00e1tica", "autoApply": "Aplicaci\u00f3n autom\u00e1tica",
"autoApplyDesc": "Buscar diariamente y aplicar actualizaciones autom\u00e1ticamente a las 3 AM. Los servicios se reinician seg\u00fan sea necesario.", "autoApplyDesc": "Buscar diariamente y aplicar actualizaciones autom\u00e1ticamente a las 3 AM. Los servicios se reinician seg\u00fan sea necesario.",
"downloadUpdate": "Descargar actualizaci\u00f3n", "downloadUpdate": "Descargar actualizaci\u00f3n",
"applyUpdate": "Aplicar actualizaci\u00f3n", "applyUpdate": "Instalar actualizaci\u00f3n",
"checkForUpdates": "Buscar actualizaciones", "checkForUpdates": "Buscar actualizaciones",
"checking": "Verificando...", "checking": "Verificando...",
"rollback": "Revertir a la versi\u00f3n anterior", "rollback": "Revertir a la versi\u00f3n anterior",
"backToSettings": "Volver a configuraci\u00f3n", "backToSettings": "Volver a configuraci\u00f3n",
"percentComplete": "{percent}% completado", "percentComplete": "{percent}% completado",
"applyWarning": "Instalando componentes y reiniciando servicios. No apague el equipo.", "applyWarning": "Instalando componentes y reiniciando servicios. No apague el equipo.",
"applyTitle": "\u00bfAplicar actualizaci\u00f3n?", "applyTitle": "\u00bfInstalar actualizaci\u00f3n?",
"applyMessage": "El servicio del backend se reiniciar\u00e1. Esto puede tomar un momento.", "applyMessage": "El servicio del backend se reiniciar\u00e1. Esto puede tomar un momento.",
"rollbackTitle": "\u00bfRevertir versi\u00f3n?", "rollbackTitle": "\u00bfRevertir versi\u00f3n?",
"rollbackMessage": "Esto restaurar\u00e1 la versi\u00f3n anterior. El servicio del backend se reiniciar\u00e1.", "rollbackMessage": "Esto restaurar\u00e1 la versi\u00f3n anterior. El servicio del backend se reiniciar\u00e1.",
"applyNow": "Aplicar ahora", "applyNow": "Instalar ahora",
"rollbackButton": "Revertir", "rollbackButton": "Revertir",
"upToDateMessage": "Su sistema est\u00e1 actualizado. No hay actualizaciones disponibles. Su sistema est\u00e1 ejecutando la \u00faltima versi\u00f3n.", "upToDateMessage": "Su sistema est\u00e1 actualizado. No hay actualizaciones disponibles. Su sistema est\u00e1 ejecutando la \u00faltima versi\u00f3n.",
"checkFailed": "Error al buscar actualizaciones. Verifique su conexi\u00f3n a internet.", "checkFailed": "Error al buscar actualizaciones. Verifique su conexi\u00f3n a internet.",