Compare commits
7 Commits
v1.7.1-alp
...
v1.7.8-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbf30e2e29 | ||
|
|
23cc78f0db | ||
|
|
9c6251c784 | ||
|
|
12f48a21c1 | ||
|
|
170f8ae787 | ||
|
|
3a479e5b09 | ||
|
|
0d5128a121 |
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.1-alpha"
|
||||
version = "1.7.8-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.1-alpha"
|
||||
version = "1.7.8-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -6,12 +6,18 @@ impl RpcHandler {
|
||||
/// Check for available system updates.
|
||||
/// Tries git-based check first (if repo exists), falls back to manifest-based.
|
||||
pub(super) async fn handle_update_check(&self) -> Result<serde_json::Value> {
|
||||
// Try git-based check first (preferred for beta nodes)
|
||||
// Manifest override: when ARCHIPELAGO_UPDATE_URL is explicitly set,
|
||||
// the operator wants OTA via manifest — typically a dev box where
|
||||
// ~/archy/.git exists but isn't the intended update surface.
|
||||
// Without this short-circuit the dev box always advertises "Pull
|
||||
// & Rebuild" and can never exercise the manifest OTA path.
|
||||
let manifest_override = std::env::var("ARCHIPELAGO_UPDATE_URL").is_ok();
|
||||
|
||||
let repo_dir = std::path::PathBuf::from(
|
||||
std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()),
|
||||
)
|
||||
.join("archy");
|
||||
if repo_dir.join(".git").exists() {
|
||||
if !manifest_override && repo_dir.join(".git").exists() {
|
||||
if let Ok(git_status) = self.git_check_update(&repo_dir).await {
|
||||
return Ok(git_status);
|
||||
}
|
||||
|
||||
@@ -265,16 +265,13 @@ impl DataModel {
|
||||
/// falling back to Cargo.toml version. This allows sequential CI build
|
||||
/// numbers to be reflected in the UI without recompiling the binary.
|
||||
fn detect_build_version() -> String {
|
||||
if let Ok(content) = std::fs::read_to_string("/opt/archipelago/build-info.txt") {
|
||||
for line in content.lines() {
|
||||
if let Some(v) = line.strip_prefix("version=") {
|
||||
let v = v.trim();
|
||||
if !v.is_empty() {
|
||||
return v.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Always use the binary's compiled-in version. The ISO installer
|
||||
// writes /opt/archipelago/build-info.txt at install time, but that
|
||||
// file is never rewritten by OTA or sideload, so trusting it made
|
||||
// the sidebar permanently advertise whatever the ISO shipped with
|
||||
// even after the running binary had moved on. CARGO_PKG_VERSION is
|
||||
// baked into the binary at compile time, so it always matches what
|
||||
// is actually running.
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
|
||||
@@ -739,6 +739,20 @@ impl IdentityManager {
|
||||
.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 {
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
@@ -749,7 +763,7 @@ impl IdentityManager {
|
||||
created_at: file.created_at,
|
||||
nostr_pubkey: file.nostr_pubkey_hex,
|
||||
nostr_npub,
|
||||
profile: file.profile,
|
||||
profile,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -277,47 +277,122 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
||||
|
||||
match name.as_str() {
|
||||
"archipelago" => {
|
||||
let dest = Path::new("/usr/local/bin/archipelago");
|
||||
fs::copy(&src, dest)
|
||||
// We're running FROM /usr/local/bin/archipelago right now,
|
||||
// so we can't rewrite it in place — `install` / `cp` would
|
||||
// hit ETXTBSY on the busy executable. Use `mv` instead:
|
||||
// rename() is atomic and doesn't modify the existing file,
|
||||
// it just re-points the path at a new inode. The currently
|
||||
// running process keeps executing off the old inode; new
|
||||
// invocations (i.e. after the post-apply systemctl
|
||||
// restart) pick up the new binary.
|
||||
let staged = src.to_string_lossy().to_string();
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["chmod", "0755", &staged])
|
||||
.status()
|
||||
.await;
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["chown", "root:root", &staged])
|
||||
.status()
|
||||
.await;
|
||||
let status = tokio::process::Command::new("sudo")
|
||||
.args(["mv", &staged, "/usr/local/bin/archipelago"])
|
||||
.status()
|
||||
.await
|
||||
.with_context(|| format!("Failed to apply {}", name))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(dest, std::fs::Permissions::from_mode(0o755))
|
||||
.context("Failed to set binary permissions")?;
|
||||
.with_context(|| format!("Failed to spawn mv for {}", name))?;
|
||||
if !status.success() {
|
||||
anyhow::bail!(
|
||||
"sudo mv failed for {} (exit {:?})",
|
||||
name,
|
||||
status.code()
|
||||
);
|
||||
}
|
||||
info!(name = %name, "Backend binary applied");
|
||||
}
|
||||
_ if name.contains("frontend") && name.ends_with(".tar.gz") => {
|
||||
let web_ui_dir = Path::new("/opt/archipelago/web-ui");
|
||||
// Back up current frontend
|
||||
let frontend_backup = backup_dir.join("web-ui-backup.tar.gz");
|
||||
if web_ui_dir.exists() {
|
||||
let status = tokio::process::Command::new("tar")
|
||||
.args([
|
||||
"-czf",
|
||||
&frontend_backup.to_string_lossy(),
|
||||
"-C",
|
||||
"/opt/archipelago",
|
||||
"web-ui",
|
||||
])
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to backup frontend")?;
|
||||
if status.success() {
|
||||
info!("Frontend backed up");
|
||||
}
|
||||
// Tarball contents are the *inside* of web-ui/ (root entries
|
||||
// `./test-aiui.html`, `./assets/`, ...). Extract into a
|
||||
// uniquely-named staging dir, then mv into place. No `rm
|
||||
// -rf` pre-cleanup — that's what hit transient EROFS on
|
||||
// .198 and aborted the apply mid-flight.
|
||||
let ts = chrono::Utc::now().timestamp_millis();
|
||||
let staging_new = format!("/opt/archipelago/web-ui.new.{}", ts);
|
||||
let staging_old = format!("/opt/archipelago/web-ui.old.{}", ts);
|
||||
let web_ui = "/opt/archipelago/web-ui";
|
||||
let backup_path = "/opt/archipelago/web-ui.bak";
|
||||
|
||||
let mk = tokio::process::Command::new("sudo")
|
||||
.args(["mkdir", "-p", &staging_new])
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to create frontend staging dir")?;
|
||||
if !mk.success() {
|
||||
anyhow::bail!("mkdir {} failed", staging_new);
|
||||
}
|
||||
// Extract new frontend
|
||||
let status = tokio::process::Command::new("tar")
|
||||
.args(["-xzf", &src.to_string_lossy(), "-C", "/opt/archipelago"])
|
||||
let extract = tokio::process::Command::new("sudo")
|
||||
.args(["tar", "-xzf", &src.to_string_lossy(), "-C", &staging_new])
|
||||
.status()
|
||||
.await
|
||||
.with_context(|| format!("Failed to extract {}", name))?;
|
||||
if !status.success() {
|
||||
if !extract.success() {
|
||||
// Best-effort cleanup of the partial extraction.
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["rm", "-rf", &staging_new])
|
||||
.status()
|
||||
.await;
|
||||
anyhow::bail!("tar extraction failed for {}", name);
|
||||
}
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["chown", "-R", "archipelago:archipelago", &staging_new])
|
||||
.status()
|
||||
.await;
|
||||
|
||||
// Swap: mv current web-ui aside, then mv new into place.
|
||||
if Path::new(web_ui).exists() {
|
||||
let mv_old = tokio::process::Command::new("sudo")
|
||||
.args(["mv", web_ui, &staging_old])
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to rotate old web-ui")?;
|
||||
if !mv_old.success() {
|
||||
anyhow::bail!("failed to move old web-ui aside");
|
||||
}
|
||||
}
|
||||
let mv_new = tokio::process::Command::new("sudo")
|
||||
.args(["mv", &staging_new, web_ui])
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to swap new web-ui into place")?;
|
||||
if !mv_new.success() {
|
||||
// Roll back the rename so nginx keeps serving.
|
||||
if Path::new(&staging_old).exists() {
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["mv", &staging_old, web_ui])
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
anyhow::bail!("failed to move new web-ui into place");
|
||||
}
|
||||
|
||||
// Rotate previous rollback aside (best-effort) and install
|
||||
// this apply's old copy as the new rollback.
|
||||
if Path::new(&staging_old).exists() {
|
||||
if Path::new(backup_path).exists() {
|
||||
// Tag the previous backup with its own ts so it
|
||||
// doesn't collide; best-effort cleanup.
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args([
|
||||
"mv",
|
||||
backup_path,
|
||||
&format!("{}.{}", backup_path, ts),
|
||||
])
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["mv", &staging_old, backup_path])
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui");
|
||||
}
|
||||
_ => {
|
||||
@@ -339,7 +414,20 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
||||
// Clean staging
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -663,18 +663,18 @@
|
||||
"autoApply": "Auto-Apply",
|
||||
"autoApplyDesc": "Check daily and automatically install updates at 3 AM. Service restarts as needed.",
|
||||
"downloadUpdate": "Download Update",
|
||||
"applyUpdate": "Apply Update",
|
||||
"applyUpdate": "Install Update",
|
||||
"checkForUpdates": "Check for Updates",
|
||||
"checking": "Checking...",
|
||||
"rollback": "Rollback to Previous",
|
||||
"backToSettings": "Back to Settings",
|
||||
"percentComplete": "{percent}% complete",
|
||||
"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.",
|
||||
"rollbackTitle": "Rollback Version?",
|
||||
"rollbackMessage": "This will restore the previous version. The backend service will restart.",
|
||||
"applyNow": "Apply Now",
|
||||
"applyNow": "Install Now",
|
||||
"rollbackButton": "Rollback",
|
||||
"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.",
|
||||
|
||||
@@ -662,18 +662,18 @@
|
||||
"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.",
|
||||
"downloadUpdate": "Descargar actualizaci\u00f3n",
|
||||
"applyUpdate": "Aplicar actualizaci\u00f3n",
|
||||
"applyUpdate": "Instalar actualizaci\u00f3n",
|
||||
"checkForUpdates": "Buscar actualizaciones",
|
||||
"checking": "Verificando...",
|
||||
"rollback": "Revertir a la versi\u00f3n anterior",
|
||||
"backToSettings": "Volver a configuraci\u00f3n",
|
||||
"percentComplete": "{percent}% completado",
|
||||
"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.",
|
||||
"rollbackTitle": "\u00bfRevertir versi\u00f3n?",
|
||||
"rollbackMessage": "Esto restaurar\u00e1 la versi\u00f3n anterior. El servicio del backend se reiniciar\u00e1.",
|
||||
"applyNow": "Aplicar ahora",
|
||||
"applyNow": "Instalar ahora",
|
||||
"rollbackButton": "Revertir",
|
||||
"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.",
|
||||
|
||||
@@ -345,10 +345,12 @@ async function downloadUpdate() {
|
||||
downloadPercent.value = 0
|
||||
statusMessage.value = ''
|
||||
|
||||
// Simulate incremental progress while waiting for the RPC
|
||||
// Simulate incremental progress while waiting for the RPC. Capped at
|
||||
// 95% so the bar never shows >100% before the real completion jumps it
|
||||
// to 100 — previously the random increment could overshoot.
|
||||
const progressInterval = setInterval(() => {
|
||||
if (downloadPercent.value < 90) {
|
||||
downloadPercent.value += Math.random() * 15
|
||||
if (downloadPercent.value < 95) {
|
||||
downloadPercent.value = Math.min(95, downloadPercent.value + Math.random() * 3)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
|
||||
@@ -18,27 +18,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3 flex-1 min-h-0">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3 shrink-0">
|
||||
<div class="p-3 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Daemon version</p>
|
||||
<p class="text-sm font-medium text-white break-all">{{ status.version || '—' }}</p>
|
||||
<p v-if="!status.installed" class="text-xs text-white/40 mt-1">Package not installed</p>
|
||||
</div>
|
||||
<div v-if="status.service_active" class="p-3 bg-white/5 rounded-lg sm:col-span-2">
|
||||
<div class="flex items-center justify-between gap-3 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="status.anchor_connected ? 'bg-cyan-400' : 'bg-orange-400'"></span>
|
||||
<span class="text-white/70">Anchor (fips.v0l.io):</span>
|
||||
<span :class="status.anchor_connected ? 'text-cyan-300' : 'text-orange-300'">
|
||||
{{ status.anchor_connected ? 'connected' : 'not reached' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-white/60">{{ status.authenticated_peer_count ?? 0 }} peer{{ (status.authenticated_peer_count ?? 0) === 1 ? '' : 's' }}</div>
|
||||
</div>
|
||||
<p v-if="!status.anchor_connected" class="mt-1 text-[11px] text-white/40">
|
||||
Without the anchor, DHT routing to unknown npubs can't bootstrap; federation + messaging will fall back to Tor until it reconnects.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center justify-between gap-2 mb-1">
|
||||
<p class="text-xs text-white/60">FIPS npub</p>
|
||||
@@ -60,6 +45,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anchor status: always full-width to keep desktop layout tidy -->
|
||||
<div v-if="status.service_active" class="p-3 bg-white/5 rounded-lg mb-3 shrink-0">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="w-2 h-2 rounded-full" :class="status.anchor_connected ? 'bg-cyan-400' : 'bg-orange-400'"></span>
|
||||
<span class="text-white/70">Anchor (fips.v0l.io):</span>
|
||||
<span :class="status.anchor_connected ? 'text-cyan-300' : 'text-orange-300'">
|
||||
{{ status.anchor_connected ? 'connected' : 'not reached' }}
|
||||
</span>
|
||||
<span class="text-white/40">·</span>
|
||||
<span class="text-white/60">{{ status.authenticated_peer_count ?? 0 }} peer{{ (status.authenticated_peer_count ?? 0) === 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="!status.anchor_connected"
|
||||
type="button"
|
||||
class="text-xs px-3 py-1.5 rounded-md bg-orange-400/15 hover:bg-orange-400/25 text-orange-200 disabled:opacity-60 transition-colors"
|
||||
:disabled="reconnecting"
|
||||
@click="reconnectAnchor"
|
||||
>
|
||||
{{ reconnecting ? 'Reconnecting…' : 'Reconnect' }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!status.anchor_connected" class="mt-2 text-[11px] text-white/40 leading-snug">
|
||||
Without the anchor, DHT routing to unknown npubs can't bootstrap; federation and messaging fall back to Tor until it reconnects. Reconnect restarts the FIPS daemon, which usually clears a stale identity cache.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="statusMessage" class="mb-3 p-3 rounded-lg text-xs" :class="statusIsError ? 'bg-red-400/10 text-red-300' : 'bg-green-400/10 text-green-300'">{{ statusMessage }}</div>
|
||||
|
||||
<div v-if="status.key_present && !status.service_active" class="flex gap-2 mt-auto pt-3 shrink-0">
|
||||
@@ -97,6 +109,7 @@ const status = ref<FipsStatus>({
|
||||
anchor_connected: false,
|
||||
})
|
||||
const installing = ref(false)
|
||||
const reconnecting = ref(false)
|
||||
const statusMessage = ref('')
|
||||
const statusIsError = ref(false)
|
||||
const copied = ref(false)
|
||||
@@ -167,5 +180,29 @@ async function installAndActivate() {
|
||||
}
|
||||
}
|
||||
|
||||
// Restart the FIPS daemon to kick it back onto the public anchor. Stale
|
||||
// identity-cache entries are the usual cause of "not reached"; systemctl
|
||||
// restart clears them and re-runs the bootstrap handshake.
|
||||
async function reconnectAnchor() {
|
||||
reconnecting.value = true
|
||||
try {
|
||||
await rpcClient.call({ method: 'fips.restart', timeout: 45_000 })
|
||||
// Give the daemon a few seconds to come back and re-populate its
|
||||
// identity cache before we re-query status.
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
await loadStatus()
|
||||
if (status.value.anchor_connected) {
|
||||
flash('Anchor reconnected')
|
||||
} else {
|
||||
flash('FIPS restarted — anchor still reporting unreachable. Check network / firewall.', true)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
flash(`Reconnect failed: ${msg}`, true)
|
||||
} finally {
|
||||
reconnecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadStatus)
|
||||
</script>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<button @click="openProfileEditor(identity)" class="relative flex-shrink-0 w-10 h-10 rounded-full overflow-hidden group" title="Edit profile">
|
||||
<img v-if="identity.profile?.picture" :src="identity.profile.picture" class="w-full h-full object-cover" @error="($event.target as HTMLImageElement).style.display = 'none'" />
|
||||
<img v-if="identity.profile?.picture" :src="displayableUrl(identity.profile.picture)" class="w-full h-full object-cover" @error="($event.target as HTMLImageElement).style.display = 'none'" />
|
||||
<div v-if="!identity.profile?.picture" class="w-full h-full flex items-center justify-center" :class="{
|
||||
'bg-blue-500/20': identity.purpose === 'personal',
|
||||
'bg-orange-500/20': identity.purpose === 'business',
|
||||
@@ -88,7 +88,8 @@
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-white font-medium text-sm">{{ identity.name }}</span>
|
||||
<span class="text-white font-medium text-sm">{{ identity.profile?.display_name || identity.name }}</span>
|
||||
<span v-if="identity.profile?.display_name && identity.profile.display_name !== identity.name" class="text-white/40 text-xs truncate max-w-[160px]" :title="`Internal name: ${identity.name}`">({{ identity.name }})</span>
|
||||
<span v-if="identity.is_default" class="text-yellow-400 text-xs" title="Default identity">★</span>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full capitalize" :class="{
|
||||
'bg-blue-500/20 text-blue-300': identity.purpose === 'personal',
|
||||
@@ -301,7 +302,7 @@
|
||||
<div class="glass-card p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="profile-editor-title">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<div class="relative w-16 h-16 rounded-full overflow-hidden bg-white/10 shrink-0">
|
||||
<img v-if="profileForm.picture" :src="profileForm.picture" class="w-full h-full object-cover" @error="($event.target as HTMLImageElement).style.display = 'none'" />
|
||||
<img v-if="profileForm.picture" :src="displayableUrl(profileForm.picture)" class="w-full h-full object-cover" @error="($event.target as HTMLImageElement).style.display = 'none'" />
|
||||
<div v-else class="w-full h-full flex items-center justify-center">
|
||||
<span class="text-2xl font-bold text-white/40">{{ profileEditorIdentity.name.charAt(0).toUpperCase() }}</span>
|
||||
</div>
|
||||
@@ -408,6 +409,20 @@ const profilePublishing = ref(false)
|
||||
const avatarUploading = ref(false)
|
||||
const bannerUploading = ref(false)
|
||||
|
||||
// The backend returns onion-based public URLs for uploaded profile
|
||||
// pictures (so they're fetchable by external Nostr clients), but the
|
||||
// local browser session isn't Tor-routed and can't resolve .onion hosts.
|
||||
// Rewrite onion-rooted `/blob/<cid>` URLs (with or without capability
|
||||
// query) to same-origin `/blob/<cid>` so they render in this UI. Data
|
||||
// URLs and plain external URLs pass through untouched.
|
||||
function displayableUrl(url: string | null | undefined): string {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('data:') || url.startsWith('/')) return url
|
||||
const onionMatch = url.match(/^https?:\/\/[a-z2-7]{16,56}\.onion(\/blob\/[0-9a-f]{64})(\?.*)?$/i)
|
||||
if (onionMatch && onionMatch[1]) return onionMatch[1]
|
||||
return url
|
||||
}
|
||||
|
||||
// Upload to the node's blob store and drop the returned public URL into
|
||||
// the profile field. The /api/blob endpoint marks these blobs public, so
|
||||
// the URL served back (`public_url`, onion-rooted when Tor is up) is
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"version": "1.7.1-alpha",
|
||||
"version": "1.7.8-alpha",
|
||||
"release_date": "2026-04-20",
|
||||
"changelog": [
|
||||
"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."
|
||||
"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."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.0-alpha",
|
||||
"new_version": "1.7.1-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.1-alpha/archipelago",
|
||||
"sha256": "7f7981bdf33af6fdb0022338c62e0a102b17c1da95f87f630b07fc2b6056eef0",
|
||||
"size_bytes": 40391760
|
||||
"current_version": "1.7.7-alpha",
|
||||
"new_version": "1.7.8-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.8-alpha/archipelago",
|
||||
"sha256": "2753daec113bb4fbbc2a68148ef1579524a26707733eea410faf400b9948094d",
|
||||
"size_bytes": 40377648
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.1-alpha.tar.gz",
|
||||
"current_version": "1.7.0-alpha",
|
||||
"new_version": "1.7.1-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.1-alpha/archipelago-frontend-1.7.1-alpha.tar.gz",
|
||||
"sha256": "dc3b63afedc45a663a023702ea23b6ac499d5b2731078a9b5a2feb57ae9a8370",
|
||||
"size_bytes": 76984288
|
||||
"name": "archipelago-frontend-1.7.8-alpha.tar.gz",
|
||||
"current_version": "1.7.7-alpha",
|
||||
"new_version": "1.7.8-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.8-alpha/archipelago-frontend-1.7.8-alpha.tar.gz",
|
||||
"sha256": "4fb796643cc9dc8469078ca3392f7cc5541071f6849979922b3259e5f20172e9",
|
||||
"size_bytes": 76984615
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
releases/v1.7.2-alpha/archipelago
Executable file
BIN
releases/v1.7.2-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.2-alpha/archipelago-frontend-1.7.2-alpha.tar.gz
Normal file
BIN
releases/v1.7.2-alpha/archipelago-frontend-1.7.2-alpha.tar.gz
Normal file
Binary file not shown.
BIN
releases/v1.7.3-alpha/archipelago
Executable file
BIN
releases/v1.7.3-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.3-alpha/archipelago-frontend-1.7.3-alpha.tar.gz
Normal file
BIN
releases/v1.7.3-alpha/archipelago-frontend-1.7.3-alpha.tar.gz
Normal file
Binary file not shown.
BIN
releases/v1.7.4-alpha/archipelago
Executable file
BIN
releases/v1.7.4-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.4-alpha/archipelago-frontend-1.7.4-alpha.tar.gz
Normal file
BIN
releases/v1.7.4-alpha/archipelago-frontend-1.7.4-alpha.tar.gz
Normal file
Binary file not shown.
BIN
releases/v1.7.5-alpha/archipelago
Executable file
BIN
releases/v1.7.5-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.5-alpha/archipelago-frontend-1.7.5-alpha.tar.gz
Normal file
BIN
releases/v1.7.5-alpha/archipelago-frontend-1.7.5-alpha.tar.gz
Normal file
Binary file not shown.
BIN
releases/v1.7.6-alpha/archipelago
Executable file
BIN
releases/v1.7.6-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.6-alpha/archipelago-frontend-1.7.6-alpha.tar.gz
Normal file
BIN
releases/v1.7.6-alpha/archipelago-frontend-1.7.6-alpha.tar.gz
Normal file
Binary file not shown.
BIN
releases/v1.7.7-alpha/archipelago
Executable file
BIN
releases/v1.7.7-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.7-alpha/archipelago-frontend-1.7.7-alpha.tar.gz
Normal file
BIN
releases/v1.7.7-alpha/archipelago-frontend-1.7.7-alpha.tar.gz
Normal file
Binary file not shown.
BIN
releases/v1.7.8-alpha/archipelago
Executable file
BIN
releases/v1.7.8-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.8-alpha/archipelago-frontend-1.7.8-alpha.tar.gz
Normal file
BIN
releases/v1.7.8-alpha/archipelago-frontend-1.7.8-alpha.tar.gz
Normal file
Binary file not shown.
Reference in New Issue
Block a user