Compare commits

..
Author SHA1 Message Date
DorianandClaude Fable 5 245fb1a815 chore: release v1.7.116-alpha (signed OTA manifest)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:05:18 +01:00
DorianandClaude Fable 5 6dffd8e2e8 chore: bump version to 1.7.116-alpha + changelog/What's New
Demo images / Build & push demo images (push) Successful in 2m59s
Bundles all post-115 fixes into an OTA release: boot READY-before-recovery
+ Restart=always (no more 'server starting up'), and the app-install
daemon-kill fix (v6 relay only bridges live app ports; port-cleanup
excludes our own PID).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:59:48 +01:00
DorianandClaude Fable 5 365f3d7d18 fix(install): mesh app-port relay no longer kills the daemon on install
The v6 app-port relay preemptively bound [::]:<port> for ALL catalog
ports, even apps not installed. Installing such an app (grafana:3000,
photoprism, uptime-kuma, jellyfin — framework-pt 2026-07-27) then hit
'address already in use' from the relay, which triggered
cleanup_stale_pasta_port ->  -> killed archipelago
itself (it held the port) mid-install. The daemon crash-looped and the
half-created apps were rolled back and vanished.

Two fixes:
- relay only bridges a port that a running app already answers on over
  IPv4 (probe 127.0.0.1:port first) — an uninstalled app's port is never
  held, so its install sees a free port and never triggers the cleanup.
- cleanup_stale_pasta_port excludes our own PID from both the ss-based
  kill and the fuser kill, so freeing a port can never terminate the
  daemon even when the relay legitimately holds it (reinstall case).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:29:54 +01:00
DorianandClaude Fable 5 70bcbc4acc fix(boot): signal systemd READY before heavy boot recovery + Restart=always
Root cause of 'server starting up' forever / crash-on-install
(framework-pt, v1.7.114->115, 2026-07-26): on a node with many stacks,
the synchronous boot recovery (recover + start_stopped_containers) runs
BEFORE sd_notify(Ready), so the unit sits in 'activating' for minutes.
Anything touching the service in that window — a superseding
start/restart, an install-time reconcile churn — killed a half-started
instance; it exits 0 on SIGTERM and Restart=on-failure then never
restarts it. Node dead behind 'server starting up'.

Fixes:
- signal READY (+ start the watchdog keepalive) BEFORE boot recovery, so
  the unit reaches 'active' in seconds; recovery/reconcile/listener
  continue after. No more minutes-long activating window.
- Restart=always (was on-failure): a clean-exit SIGTERM must still bring
  the daemon back. Manual  is still honored.
- OTA restart via a PID1-owned transient timer (systemd-run --on-active=2)
  instead of a tokio-sleep child of the process being stopped, whose
  start-half was being lost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 00:31:54 +01:00
DorianandClaude Fable 5 56e4c30261 fix(companion): edge-to-edge app webview, whole-overlay touch shield, no raw IP titles — 0.5.19
Demo images / Build & push demo images (push) Successful in 3m0s
- app webview draws behind the status bar again (the inset rework
  painted an opaque black bar there); page background owns the top
- the in-app overlay eats EVERY touch its children don't handle —
  a near-miss on Close was falling through to the kiosk AIUI tab
- loading screen never titles itself with a raw mesh IPv6/IP host
- re-land vc37 connect fixes vc38 shipped without: pairing restarts
  the mesh immediately when consent exists; session warmer probes all
  targets concurrently (5s) instead of 20s each in sequence

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 23:31:48 +01:00
17 changed files with 210 additions and 81 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "com.archipelago.app"
minSdk = 26
targetSdk = 35
versionCode = 38
versionName = "0.5.18"
versionCode = 39
versionName = "0.5.19"
vectorDrawables {
useSupportLibrary = true
@@ -153,19 +153,28 @@ class ArchyVpnService : VpnService() {
} catch (_: Exception) {
emptyList()
}.distinct()
for ((ula, port) in targets) {
try {
java.net.Socket().use { s ->
s.connect(
java.net.InetSocketAddress(java.net.InetAddress.getByName(ula), port),
20_000,
)
if (round == 0) Log.i(TAG, "session warmer: ${targets.map { it.first }}")
// Probe all targets CONCURRENTLY with a short timeout — the
// old sequential 20s-per-target loop let one cold node starve
// every other target for the whole aggressive window.
targets.map { (ula, port) ->
launch {
try {
java.net.Socket().use { s ->
s.connect(
java.net.InetSocketAddress(
java.net.InetAddress.getByName(ula),
port,
),
5_000,
)
}
} catch (_: Exception) {
// Cold path / node away — the attempt still drove
// session establishment; try again next round.
}
} catch (_: Exception) {
// Cold path / node away — the connect attempt still
// drove session establishment; try again next round.
}
}
}.forEach { it.join() }
round++
// Aggressive for the first ~minute (session bring-up), then a
// slow keep-warm tick that costs nearly nothing.
@@ -41,7 +41,15 @@ object FipsManager {
ensureIdentity(prefs)
prefs.upsertNodePeer(info, alias)
peersDirty = true
_consentNeeded.value = true
// Restart the mesh with the new peer RIGHT NOW when consent already
// exists — relying on the consentNeeded collector left a running
// mesh on the OLD peer list whenever the collector wasn't active
// (fresh pairings looked dead until a full app restart).
if (VpnService.prepare(context) == null) {
startService(context)
} else {
_consentNeeded.value = true
}
}
/** Generate-once mesh identity. Returns null only if the RNG/native fails. */
@@ -897,7 +897,17 @@ private fun InAppBrowser(
fun isSameNode(u: String): Boolean =
isSameHost(u, serverUrl) || (meshUrl != null && isSameHost(u, meshUrl))
var browser by remember { mutableStateOf<WebView?>(null) }
var title by remember { mutableStateOf(android.net.Uri.parse(url).host ?: url) }
// Loader title: never show a raw IP host — a mesh ULA like
// [fd79:1aa:…] is technically the host but reads as garbage on the
// loading screen. Show a neutral name until the page reports its
// real <title> (onReceivedTitle upgrades it).
var title by remember {
mutableStateOf(
android.net.Uri.parse(url).host
?.takeUnless { it.contains(':') || it.matches(Regex("^\\d+(\\.\\d+){3}$")) }
?: "Archipelago",
)
}
var favicon by remember { mutableStateOf<Bitmap?>(null) }
var progress by remember { mutableIntStateOf(0) }
var loading by remember { mutableStateOf(true) }
@@ -935,12 +945,21 @@ private fun InAppBrowser(
modifier = Modifier
.fillMaxSize()
.background(SurfaceBlack)
// Bottom inset handled by the touch-shield strip below the bar —
// NOT by padding: a padded area only paints, it doesn't consume,
// so taps in the gesture strip fell straight THROUGH this overlay
// into the kiosk's tab bar behind it (accidental AIUI-tab hits).
// Whole-overlay touch shield: every touch not handled by a child
// (control-bar gaps, inset strips) dies here instead of falling
// through to the kiosk's tab bar behind (a near-miss on Close
// was opening the AIUI tab underneath).
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
)
// Bottom inset handled by the touch-shield strip below the bar.
// No TOP inset padding: the WebView draws edge-to-edge behind the
// status bar so the app's own background fills it — the padded
// version painted an opaque black bar there (user-rejected look).
.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
),
) {
// WebView + loading overlay fill the area above the bottom control bar.
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## v1.7.116-alpha (2026-07-27)
- Nodes no longer get stuck on "server starting up" after an update or reboot. On a node running many apps, the backend used to spend minutes recovering containers before it told the system it was ready, and anything that touched it during that window could leave it down for good. It now reports ready immediately and recovers in the background, and it always restarts itself if it ever does go down.
- Installing apps no longer crashes the node. A change that made app screens reachable over the mesh was accidentally holding onto every app's network port in advance — so installing an app like Grafana, Photoprism, Uptime Kuma, or Jellyfin collided with it and the port-cleanup step took the whole backend down, rolling the install back. Installs are now clean and the backend can never be caught by that cleanup.
- Rolls up everything from v1.7.115: app screens and the dashboard load over the mesh out of the box (firewall openings shipped automatically, IPv6 support end to end), and nodes rejoin the mesh in seconds after their rendezvous point restarts.
## v1.7.115-alpha (2026-07-26)
- The companion app can reach your node's screen from anywhere again. The recent security hardening locked down the node's mesh interface so tightly that the dashboard itself was blocked — the phone would pair and connect, then sit on a blank screen. The node now explicitly opens its own web interface (and only that) through the mesh firewall on every install and upgrade, so the phone's view of your node works out of the box, on any network, and can't silently break in a future update.
+1 -1
View File
@@ -95,7 +95,7 @@ dependencies = [
[[package]]
name = "archipelago"
version = "1.7.115-alpha"
version = "1.7.116-alpha"
dependencies = [
"anyhow",
"archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.115-alpha"
version = "1.7.116-alpha"
edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"]
@@ -2324,17 +2324,28 @@ async fn cleanup_start_conflict(package_id: &str, stderr: &str) -> bool {
}
async fn cleanup_stale_pasta_port(port: &str) {
// NEVER kill our own process. The daemon holds catalog app ports over
// IPv6 (the mesh app-port relay), so a blunt `fuser -k <port>/tcp` would
// terminate archipelago itself mid-install — installs failed and apps
// vanished on framework-pt 2026-07-27. Kill every listener on the port
// EXCEPT our PID (and our process group), leaving the relay/daemon alive.
let self_pid = std::process::id();
let kill_listener = format!(
"ss -ltnp 'sport = :{}' 2>/dev/null | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p' | xargs -r kill 2>/dev/null || true",
port
"ss -ltnp 'sport = :{port}' 2>/dev/null | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p' | \
while read p; do [ \"$p\" = \"{self_pid}\" ] || kill \"$p\" 2>/dev/null; done || true",
);
let _ = tokio::process::Command::new("sh")
.args(["-c", &kill_listener])
.output()
.await;
let _ = tokio::process::Command::new("sudo")
.args(["fuser", "-k", &format!("{}/tcp", port)])
// sudo fuser -k, but exclude our own PID: fuser prints the PIDs holding
// the port; kill each except self. (`fuser -k` has no exclusion flag.)
let fuser_kill = format!(
"for p in $(sudo fuser {port}/tcp 2>/dev/null); do [ \"$p\" = \"{self_pid}\" ] || sudo kill \"$p\" 2>/dev/null; done || true",
);
let _ = tokio::process::Command::new("sh")
.args(["-c", &fuser_kill])
.output()
.await;
+23 -10
View File
@@ -199,6 +199,26 @@ async fn main() -> Result<()> {
// Now mark this instance as running so the next startup can detect a crash.
crash_recovery::write_pid_marker(&config.data_dir).await?;
// Signal READY *before* the heavy synchronous boot recovery below. On a
// node with many stacks that recovery takes minutes, and the unit sat in
// `activating` the whole time — so anything that touched the service in
// that window (a superseding start/restart, a start-timeout) killed a
// half-started instance, which then exited 0 and (under the old
// Restart=on-failure) never came back: "server starting up" forever,
// reproduced on framework-pt installing apps on 2026-07-26. The daemon's
// real work (recovery, reconcile, listener) continues after READY; being
// "active" early is honest — the process is up and doing its job.
let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Ready]);
// Watchdog pings must run DURING the long recovery too, or a slow boot
// trips WatchdogSec. Spawn the keepalive here rather than after serve().
tokio::spawn(async {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(120));
loop {
interval.tick().await;
let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Watchdog]);
}
});
// Run crash recovery before starting the manifest reconciler. Both paths
// mutate Podman; running them concurrently can corrupt transient runtime
// state and leave netavark/conmon unable to start containers.
@@ -479,16 +499,9 @@ async fn main() -> Result<()> {
// Notify systemd that we're ready (Type=notify)
// Note: first param `false` keeps NOTIFY_SOCKET so watchdog pings work
let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Ready]);
// Spawn systemd watchdog ping (WatchdogSec=300, ping every 120s)
tokio::spawn(async {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(120));
loop {
interval.tick().await;
let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Watchdog]);
}
});
// READY + watchdog keepalive were already signalled/spawned earlier
// (before boot recovery) so the unit reaches `active` in seconds instead
// of sitting in `activating` through a minutes-long recovery.
// Graceful shutdown: wait for SIGTERM or SIGINT
let mut sigterm = signal::unix::signal(signal::unix::SignalKind::terminate())
+22 -2
View File
@@ -1103,12 +1103,32 @@ async fn app_port_v6_relay_loop(mut shutdown_rx: tokio::sync::watch::Receiver<bo
if bridged.contains(&port) {
continue;
}
// ONLY bridge a port that a running app already answers on
// over IPv4. Binding [::]:port for an app that isn't
// installed is actively harmful: it makes that app's
// later install hit "address already in use", and the
// install's port-free step (`fuser -k <port>/tcp`) then
// kills THIS daemon, which holds the port — the exact
// cause of installs failing + apps vanishing on
// framework-pt 2026-07-27. No v4 listener → skip; the
// next rescan picks it up once the app is up.
let v4_up = tokio::time::timeout(
std::time::Duration::from_millis(300),
tokio::net::TcpStream::connect(("127.0.0.1", port)),
)
.await
.ok()
.and_then(|r| r.ok())
.is_some();
if !v4_up {
continue;
}
let addr = SocketAddr::new(
std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
port,
);
// EADDRINUSE = the app itself (or a prior relay) already
// answers on v6 — exactly when we must stay out of the way.
// EADDRINUSE = the app itself already answers on v6 —
// exactly when we must stay out of the way.
let Ok(listener) = bind_v6_only(addr) else { continue };
bridged.insert(port);
debug!("v6 relay bridging [::]:{port} -> 127.0.0.1:{port}");
+32 -7
View File
@@ -1868,13 +1868,38 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
// 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;
// systemctl talks to PID 1 over D-Bus — doesn't need the host
// mount namespace, but routing through host_sudo keeps the
// apply flow's sudo calls uniform.
let _ = host_sudo(&["systemctl", "--no-block", "restart", "archipelago"]).await;
});
// PID1-owned timer transient: submit NOW (synchronously, while we are
// definitely alive), fire in 2s from systemd itself. The old approach —
// tokio sleep + `systemd-run --wait -- systemctl --no-block restart` —
// ran as a child of the process being stopped; on v1.7.114->115 the
// stop landed but the start never fired and the node sat dead all
// night. A timer unit owned by PID1 cannot be killed by our own death,
// and Restart=always on the unit is the second net.
let submitted = tokio::process::Command::new("sudo")
.args([
"systemd-run",
"--collect",
"--on-active=2",
"--timer-property=AccuracySec=100ms",
"--",
"systemctl",
"restart",
"archipelago",
])
.status()
.await;
match submitted {
Ok(st) if st.success() => {}
other => {
tracing::warn!(
"detached restart submission failed ({other:?}) — falling back to in-process restart"
);
tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let _ = host_sudo(&["systemctl", "--no-block", "restart", "archipelago"]).await;
});
}
}
Ok(())
}
+7 -1
View File
@@ -31,7 +31,13 @@ ExecStartPre=+/bin/bash -c 'mkdir -p /var/lib/archipelago && chown archipelago:a
# "-" so a missing/failed guard can never block the service itself.
ExecStartPre=+-/opt/archipelago/scripts/ota-crash-guard.sh
ExecStart=/usr/local/bin/archipelago
Restart=on-failure
# always (not on-failure): the OTA restart path once stopped the daemon
# cleanly and the queued start never fired (framework-pt, v1.7.114->115,
# 2026-07-26) — the node sat dead all night behind "server starting up".
# Restart=always self-heals any lost start job; an explicit
# `systemctl stop` is still honored (systemd never auto-restarts after
# a manual stop).
Restart=always
RestartSec=5
WatchdogSec=300
TimeoutStartSec=300
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "neode-ui",
"private": true,
"version": "1.7.115-alpha",
"version": "1.7.116-alpha",
"type": "module",
"scripts": {
"start": "./start-dev.sh",
Binary file not shown.
@@ -362,6 +362,18 @@ init()
</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.7.116-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.116-alpha</span>
<span class="text-xs text-white/40">July 27, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Nodes no longer get stuck on "server starting up" after an update or reboot. The backend now reports ready immediately and recovers its apps in the background, and it always restarts itself if it ever goes down the days-long "server starting up" hang is gone.</p>
<p>Installing apps no longer crashes the node. A recent change that made app screens reachable over the mesh was holding onto every app's port in advance, so installing an app collided with it and the port-cleanup took the backend down and rolled the install back. Installs are clean now.</p>
<p>Rolls up v1.7.115: app screens and the dashboard load over the mesh out of the box, with IPv6 support end to end, and nodes rejoin the mesh in seconds after their rendezvous point restarts.</p>
</div>
</div>
<!-- v1.7.115-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
+17 -17
View File
@@ -1,29 +1,29 @@
{
"changelog": [
"The companion app can reach your node's screen from anywhere again. The recent security hardening locked down the node's mesh interface so tightly that the dashboard itself was blocked — the phone would pair and connect, then sit on a blank screen. The node now explicitly opens its own web interface (and only that) through the mesh firewall on every install and upgrade, so the phone's view of your node works out of the box, on any network, and can't silently break in a future update.",
"The node's web interface also answers on IPv6 everywhere it answers on IPv4 — the mesh runs entirely on IPv6, and one v4-only listener was enough to make a working connection show nothing.",
"Nodes now come back onto the mesh in seconds instead of minutes after their rendezvous anchor restarts: the fast-reconnect tuning proven on the phone this week is now baked into every node's mesh configuration, and it survives upgrades."
"Nodes no longer get stuck on \"server starting up\" after an update or reboot. On a node running many apps, the backend used to spend minutes recovering containers before it told the system it was ready, and anything that touched it during that window could leave it down for good. It now reports ready immediately and recovers in the background, and it always restarts itself if it ever does go down.",
"Installing apps no longer crashes the node. A change that made app screens reachable over the mesh was accidentally holding onto every app's network port in advance — so installing an app like Grafana, Photoprism, Uptime Kuma, or Jellyfin collided with it and the port-cleanup step took the whole backend down, rolling the install back. Installs are now clean and the backend can never be caught by that cleanup.",
"Rolls up everything from v1.7.115: app screens and the dashboard load over the mesh out of the box (firewall openings shipped automatically, IPv6 support end to end), and nodes rejoin the mesh in seconds after their rendezvous point restarts."
],
"components": [
{
"current_version": "1.7.115-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.115-alpha/archipelago",
"current_version": "1.7.116-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.116-alpha/archipelago",
"name": "archipelago",
"new_version": "1.7.115-alpha",
"sha256": "d4e2d7cb65bf7ac6461eb11642fb8ff9254be636c173a016ad1834d4073a7cee",
"size_bytes": 51768592
"new_version": "1.7.116-alpha",
"sha256": "376cde3ff9691c7141e67b1b87e746634c45a20167c578938c4a33801eefe46e",
"size_bytes": 51787208
},
{
"current_version": "1.7.115-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.115-alpha/archipelago-frontend-1.7.115-alpha.tar.gz",
"name": "archipelago-frontend-1.7.115-alpha.tar.gz",
"new_version": "1.7.115-alpha",
"sha256": "9c131c881016a4e451e2ea3688bc9114302adcb5a2b58e485f3273abcebf07bb",
"size_bytes": 178060942
"current_version": "1.7.116-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.116-alpha/archipelago-frontend-1.7.116-alpha.tar.gz",
"name": "archipelago-frontend-1.7.116-alpha.tar.gz",
"new_version": "1.7.116-alpha",
"sha256": "7008f26d16c7b7d5dacc8edf55bd30b58c02602d3419359516bbf2497591db58",
"size_bytes": 178062490
}
],
"release_date": "2026-07-26",
"signature": "80a76615a43e4b715b564143ffc7acf4e3897a921c0b6338c757963becfc6c979c118a7f32554c82ff32de283a4a0c0a566f27c98d4ffd7b6d93747a1f3ea103",
"release_date": "2026-07-27",
"signature": "455e0ce176a0f8c9adf3bbfd7bf949739ad0e5e23c40af9ed6c89b367a42f5d52dbfe97f5ef53f838f3cd17122087b2081c1e5549065504f1f9ea101c8ffc30e",
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
"version": "1.7.115-alpha"
"version": "1.7.116-alpha"
}
+17 -17
View File
@@ -1,29 +1,29 @@
{
"changelog": [
"The companion app can reach your node's screen from anywhere again. The recent security hardening locked down the node's mesh interface so tightly that the dashboard itself was blocked — the phone would pair and connect, then sit on a blank screen. The node now explicitly opens its own web interface (and only that) through the mesh firewall on every install and upgrade, so the phone's view of your node works out of the box, on any network, and can't silently break in a future update.",
"The node's web interface also answers on IPv6 everywhere it answers on IPv4 — the mesh runs entirely on IPv6, and one v4-only listener was enough to make a working connection show nothing.",
"Nodes now come back onto the mesh in seconds instead of minutes after their rendezvous anchor restarts: the fast-reconnect tuning proven on the phone this week is now baked into every node's mesh configuration, and it survives upgrades."
"Nodes no longer get stuck on \"server starting up\" after an update or reboot. On a node running many apps, the backend used to spend minutes recovering containers before it told the system it was ready, and anything that touched it during that window could leave it down for good. It now reports ready immediately and recovers in the background, and it always restarts itself if it ever does go down.",
"Installing apps no longer crashes the node. A change that made app screens reachable over the mesh was accidentally holding onto every app's network port in advance — so installing an app like Grafana, Photoprism, Uptime Kuma, or Jellyfin collided with it and the port-cleanup step took the whole backend down, rolling the install back. Installs are now clean and the backend can never be caught by that cleanup.",
"Rolls up everything from v1.7.115: app screens and the dashboard load over the mesh out of the box (firewall openings shipped automatically, IPv6 support end to end), and nodes rejoin the mesh in seconds after their rendezvous point restarts."
],
"components": [
{
"current_version": "1.7.115-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.115-alpha/archipelago",
"current_version": "1.7.116-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.116-alpha/archipelago",
"name": "archipelago",
"new_version": "1.7.115-alpha",
"sha256": "d4e2d7cb65bf7ac6461eb11642fb8ff9254be636c173a016ad1834d4073a7cee",
"size_bytes": 51768592
"new_version": "1.7.116-alpha",
"sha256": "376cde3ff9691c7141e67b1b87e746634c45a20167c578938c4a33801eefe46e",
"size_bytes": 51787208
},
{
"current_version": "1.7.115-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.115-alpha/archipelago-frontend-1.7.115-alpha.tar.gz",
"name": "archipelago-frontend-1.7.115-alpha.tar.gz",
"new_version": "1.7.115-alpha",
"sha256": "9c131c881016a4e451e2ea3688bc9114302adcb5a2b58e485f3273abcebf07bb",
"size_bytes": 178060942
"current_version": "1.7.116-alpha",
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.116-alpha/archipelago-frontend-1.7.116-alpha.tar.gz",
"name": "archipelago-frontend-1.7.116-alpha.tar.gz",
"new_version": "1.7.116-alpha",
"sha256": "7008f26d16c7b7d5dacc8edf55bd30b58c02602d3419359516bbf2497591db58",
"size_bytes": 178062490
}
],
"release_date": "2026-07-26",
"signature": "80a76615a43e4b715b564143ffc7acf4e3897a921c0b6338c757963becfc6c979c118a7f32554c82ff32de283a4a0c0a566f27c98d4ffd7b6d93747a1f3ea103",
"release_date": "2026-07-27",
"signature": "455e0ce176a0f8c9adf3bbfd7bf949739ad0e5e23c40af9ed6c89b367a42f5d52dbfe97f5ef53f838f3cd17122087b2081c1e5549065504f1f9ea101c8ffc30e",
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
"version": "1.7.115-alpha"
"version": "1.7.116-alpha"
}