Compare commits

..
11 Commits
Author SHA1 Message Date
archipelago 687196ad3b chore: prepare release v1.8.7-alpha
Demo images / Build & push demo images (push) Failing after 37s
2026-08-31 17:08:37 -04:00
archipelago e2bd6330a1 test(app-catalog): pin the signed-catalog body gate 2026-08-31 17:08:37 -04:00
archipelago 7c0a492c43 fix(ui): launch apps on the page's scheme over HTTPS
New-tab apps and the companion WebView got hardcoded http:// URLs, so a
node reached over HTTPS opened Vaultwarden, BTCPay, Grafana et al in
cleartext. Every app port is gate-owned and serves TLS on the same port
(appgate/tls.rs), so directAppUrl(), the legacy open() path, and
resolveRuntimeLaunchUrl() now follow the page's scheme. HTTP pages (the
kiosk, LAN) are unchanged; netbird keeps its unconditional https.
2026-08-31 17:08:26 -04:00
archipelago 3089624969 Merge remote-tracking branch 'gitea-vps2/main' 2026-08-31 16:15:17 -04:00
archipelago 5b658cec67 feat(app-catalog): serve the signed catalog from the node first 2026-08-31 16:15:13 -04:00
archipelago 21b8d4b1ee catalog: add Cuprate (0.1.0-preview) 2026-08-31 16:10:38 -04:00
lfg2025 6f05f5583f Merge pull request 'docs: session record — companion 0.5.28 shipped + deployment playbook' (#151) from companion/session-2026-08-31 into main 2026-08-31 20:03:03 +00:00
Dorian 02ac4396d1 docs: session record — companion 0.5.28 shipped + the deployment playbook
Full state at session end (all public surfaces verified byte-identical
at 0.5.28/vc48; only node web-bundle redeploys outstanding), the feature
map, and the operational playbook next sessions need: Tor SOCKS proxy
for Gitea API/curl (the 'unreachable API' was a missing proxy flag),
token scopes, protected-main ship flow via -ship branch + PR + API merge,
stale local main lineage, the foundation server's two surfaces, demo CI
auto-redeploy, build/test commands, and the open items.
2026-08-31 21:02:59 +01:00
archipelago 5ffdcc9936 docs(release): explain the v1.8.7 correction
Demo images / Build & push demo images (push) Failing after 39s
2026-08-31 15:46:18 -04:00
archipelago 9cf07e1eac fix(release): enforce the v1.8 What's New floor
Demo images / Build & push demo images (push) Failing after 39s
2026-08-31 15:44:48 -04:00
archipelago e7854702c0 chore: publish release v1.8.6-alpha
Demo images / Build & push demo images (push) Failing after 36s
2026-08-31 15:40:23 -04:00
21 changed files with 1056 additions and 218 deletions
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## v1.8.7-alpha (2026-08-31)
- **What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.
- **The installer carries the same corrected release and Companion 0.5.28.** Its artifact gate now checks the companion APK version and the v1.8.0 What's New floor inside the finished ISO, so a stale frontend or phone app cannot be published under the current release label.
- **Crash dumps work on fresh installs as well as upgraded nodes.** The installer gate checks every kdump package inside the finished ISO, and `makedumpfile` is installed explicitly rather than accidentally relying on a recommended dependency that the minimal image deliberately omits.
- **Apps open over HTTPS when your node does.** Connect to your node over HTTPS and every app you open — Vaultwarden in its own tab, BTCPay, Grafana, and the rest, on a remote browser or in the phone's in-app browser — now opens on the same secure connection instead of silently dropping to plain HTTP. The node's app gate already served TLS on every app port; the dashboard was handing out `http://` addresses regardless of how you reached it. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.
- **Newly signed apps appear in the App Store immediately.** The App Store now serves the release-signed catalog the node has already fetched and verified — so publishing a signed app (like Cuprate) makes it appear for every updated node without waiting for a dashboard release. The unsigned community catalog remains only as a fallback for nodes that can't reach the registry. Cuprate was invisible on updated nodes for exactly this reason; it is now in both.
## v1.8.6-alpha (2026-08-31)
- **Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.
+588
View File
@@ -0,0 +1,588 @@
{
"version": 2,
"updated": "2026-04-22T00:00:00Z",
"registry": "source.archipelago-foundation.org/lfg2025",
"featured": {
"id": "indeedhub",
"banner": "/assets/img/featured/indeedhub-banner.jpg",
"headline": "Stream Sovereignty",
"description": "Bitcoin documentaries with Nostr identity.",
"tag": "NOSTR IDENTITY // YOUR NODE"
},
"apps": [
{
"id": "bitcoin-knots",
"title": "Bitcoin Knots",
"version": "28.1.0",
"description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.",
"icon": "/assets/img/app-icons/bitcoin-knots.webp",
"author": "Bitcoin Knots",
"category": "money",
"tier": "core",
"dockerImage": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210",
"repoUrl": "https://github.com/bitcoinknots/bitcoin"
},
{
"id": "bitcoin-core",
"title": "Bitcoin Core",
"version": "28.4.0",
"description": "Reference Bitcoin Core node with dynamic prune/full-mode startup based on host disk.",
"icon": "/assets/img/app-icons/bitcoin-core.svg",
"author": "Bitcoin Core contributors",
"category": "money",
"tier": "optional",
"dockerImage": "source.archipelago-foundation.org/lfg2025/bitcoin:28.4",
"repoUrl": "https://github.com/bitcoin/bitcoin"
},
{
"id": "lnd",
"title": "LND",
"version": "0.18.4",
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
"icon": "/assets/img/app-icons/lnd.png",
"author": "Lightning Labs",
"category": "money",
"tier": "core",
"dockerImage": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
"repoUrl": "https://github.com/lightningnetwork/lnd",
"requires": [
"bitcoin-knots"
]
},
{
"id": "btcpay-server",
"title": "BTCPay Server",
"version": "2.4.3",
"description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.",
"icon": "/assets/img/app-icons/btcpay-server.png",
"author": "BTCPay Server Foundation",
"category": "commerce",
"tier": "core",
"dockerImage": "docker.io/btcpayserver/btcpayserver:2.4.3",
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
"requires": [
"bitcoin-knots"
]
},
{
"id": "mempool",
"title": "Mempool Explorer",
"version": "3.0.0",
"description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.",
"icon": "/assets/img/app-icons/mempool.webp",
"author": "Mempool",
"category": "money",
"tier": "core",
"dockerImage": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1",
"repoUrl": "https://github.com/mempool/mempool",
"requires": [
"bitcoin-knots",
"electrumx"
]
},
{
"id": "electrumx",
"title": "ElectrumX",
"version": "1.18.0",
"description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.",
"icon": "/assets/img/app-icons/electrumx.png",
"author": "Luke Childs",
"category": "money",
"tier": "core",
"dockerImage": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0",
"repoUrl": "https://github.com/spesmilo/electrumx",
"requires": [
"bitcoin-knots"
]
},
{
"id": "indeedhub",
"title": "IndeeHub",
"version": "1.0.0",
"description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.",
"icon": "/assets/img/app-icons/indeedhub.png",
"author": "IndeeHub",
"category": "community",
"dockerImage": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0",
"repoUrl": "https://github.com/indeedhub/indeedhub"
},
{
"id": "botfights",
"title": "BotFights",
"version": "1.2.11",
"description": "Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners.",
"icon": "/assets/img/app-icons/botfights.svg",
"author": "BotFights",
"category": "community",
"dockerImage": "source.archipelago-foundation.org/lfg2025/botfights:1.2.11",
"repoUrl": "https://botfights.net",
"containerConfig": {
"ports": [
"9100:9100"
],
"volumes": [
"/var/lib/archipelago/botfights:/app/server/data"
],
"env": [
"NODE_ENV=production",
"PORT=9100",
"FIGHT_LOOP_ENABLED=true",
"ARCHY_EMBEDDED=1"
]
}
},
{
"id": "gitea",
"title": "Gitea",
"version": "1.23",
"description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
"icon": "/assets/img/app-icons/gitea.svg",
"author": "Gitea",
"category": "development",
"dockerImage": "docker.io/gitea/gitea:1.23",
"repoUrl": "https://gitea.com",
"containerConfig": {
"ports": [
"3001:3000",
"2222:22"
],
"volumes": [
"/var/lib/archipelago/gitea/data:/data",
"/var/lib/archipelago/gitea/config:/etc/gitea"
],
"env": [
"GITEA__database__DB_TYPE=sqlite3",
"GITEA__server__SSH_PORT=2222",
"GITEA__server__SSH_LISTEN_PORT=22",
"GITEA__server__LFS_START_SERVER=true",
"GITEA__packages__ENABLED=true",
"GITEA__repository__ENABLE_PUSH_CREATE_USER=true",
"GITEA__repository__ENABLE_PUSH_CREATE_ORG=true",
"GITEA__security__X_FRAME_OPTIONS="
]
},
"tier": "optional"
},
{
"id": "filebrowser",
"title": "File Browser",
"version": "2.27.0",
"description": "Baseline Archipelago file manager service.",
"icon": "/assets/img/app-icons/file-browser.webp",
"author": "File Browser",
"category": "data",
"tier": "core",
"dockerImage": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
"repoUrl": "https://github.com/filebrowser/filebrowser",
"containerConfig": {
"ports": [
"8083:80"
],
"volumes": [
"/var/lib/archipelago/filebrowser:/srv",
"/var/lib/archipelago/filebrowser-data:/data"
],
"args": [
"--database=/data/database.db",
"--root=/srv",
"--address=0.0.0.0",
"--port=80"
]
}
},
{
"id": "nostr-rs-relay",
"title": "Nostr Relay (Rust)",
"version": "0.10.0",
"description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.",
"icon": "/assets/img/app-icons/nostrudel.svg",
"author": "Nostr RS Relay",
"category": "community",
"tier": "recommended",
"dockerImage": "scsibug/nostr-rs-relay:0.10.0",
"repoUrl": "https://github.com/scsibug/nostr-rs-relay",
"containerConfig": {
"ports": [
"8081:8080"
],
"volumes": [
"/var/lib/archipelago/nostr-relay:/usr/src/app/db"
],
"env": [
"RELAY_NAME=Archipelago Nostr Relay",
"RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago"
]
}
},
{
"id": "vaultwarden",
"title": "Vaultwarden",
"version": "1.30.0",
"description": "Self-hosted password vault with zero-knowledge encryption.",
"icon": "/assets/img/app-icons/vaultwarden.webp",
"author": "Vaultwarden",
"category": "data",
"tier": "recommended",
"dockerImage": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.1-alpine",
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
"containerConfig": {
"ports": [
"8082:80"
],
"volumes": [
"/var/lib/archipelago/vaultwarden:/data"
]
}
},
{
"id": "searxng",
"title": "SearXNG",
"version": "1.0.0",
"description": "Privacy-respecting metasearch engine. Search the web without tracking.",
"icon": "/assets/img/app-icons/searxng.png",
"author": "SearXNG",
"category": "data",
"tier": "recommended",
"dockerImage": "source.archipelago-foundation.org/lfg2025/searxng:latest",
"repoUrl": "https://github.com/searxng/searxng",
"containerConfig": {
"ports": [
"8888:8080"
],
"volumes": [
"/var/lib/archipelago/searxng:/etc/searxng"
]
}
},
{
"id": "fedimint",
"title": "Fedimint Guardian",
"version": "0.10.0",
"description": "Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.",
"icon": "/assets/img/app-icons/fedimint.png",
"author": "Fedimint",
"category": "money",
"dockerImage": "source.archipelago-foundation.org/lfg2025/fedimintd:v0.10.1",
"repoUrl": "https://github.com/fedimint/fedimint"
},
{
"id": "fedimint-clientd",
"title": "Fedimint Client",
"version": "0.8.0",
"description": "Fedimint ecash client daemon (fmcd). Lets the node hold Fedimint ecash and join federations; the wallet talks to it over a local REST API.",
"icon": "/assets/img/app-icons/fedimint.png",
"author": "Fedimint",
"category": "money",
"tier": "core",
"dockerImage": "source.archipelago-foundation.org/lfg2025/fmcd:0.8.1",
"repoUrl": "https://github.com/minmoto/fmcd"
},
{
"id": "fedimint-gateway",
"title": "Fedimint Gateway",
"version": "0.10.0",
"description": "Fedimint gateway service with automatic LND-or-LDK backend selection.",
"icon": "/assets/img/app-icons/fedimint.png",
"author": "Fedimint",
"category": "money",
"dockerImage": "source.archipelago-foundation.org/lfg2025/gatewayd:v0.10.1",
"repoUrl": "https://github.com/fedimint/fedimint",
"containerConfig": {
"ports": [
"8176:8176",
"9737:9737"
],
"volumes": [
"/var/lib/archipelago/fedimint-gateway:/data",
"/var/lib/archipelago/lnd:/lnd:ro"
]
}
},
{
"id": "barkd",
"title": "Ark Wallet",
"version": "0.3.0",
"description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.",
"icon": "/assets/img/app-icons/bark.png",
"author": "Second",
"category": "money",
"dockerImage": "source.archipelago-foundation.org/lfg2025/barkd:0.3.0",
"repoUrl": "https://gitlab.com/ark-bitcoin/bark",
"containerConfig": {
"ports": [
"3535:3535"
],
"volumes": [
"/var/lib/archipelago/barkd:/data"
]
}
},
{
"id": "jellyfin",
"title": "Jellyfin",
"version": "10.8.13",
"description": "Free media server. Stream movies, music, and photos.",
"icon": "/assets/img/app-icons/jellyfin.webp",
"author": "Jellyfin",
"category": "data",
"dockerImage": "source.archipelago-foundation.org/lfg2025/jellyfin:10.11.11",
"repoUrl": "https://github.com/jellyfin/jellyfin",
"containerConfig": {
"ports": [
"8096:8096"
],
"volumes": [
"/var/lib/archipelago/jellyfin/config:/config",
"/var/lib/archipelago/jellyfin/cache:/cache"
]
}
},
{
"id": "immich",
"title": "Immich",
"version": "2.7.4",
"description": "Self-hosted photo and video backup with mobile apps and search.",
"icon": "/assets/img/app-icons/immich.png",
"author": "Immich",
"category": "data",
"dockerImage": "source.archipelago-foundation.org/lfg2025/immich-server:release",
"repoUrl": "https://github.com/immich-app/immich"
},
{
"id": "homeassistant",
"title": "Home Assistant",
"version": "2026.7.3",
"description": "Open source home automation platform. Control and monitor your smart home devices.",
"icon": "/assets/img/app-icons/homeassistant.png",
"author": "Home Assistant",
"category": "home",
"dockerImage": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.2",
"repoUrl": "https://github.com/home-assistant/core",
"containerConfig": {
"ports": [
"8123:8123"
],
"volumes": [
"/var/lib/archipelago/home-assistant:/config"
],
"env": [
"TZ=UTC"
]
}
},
{
"id": "pine",
"title": "Pine",
"version": "1.3.0",
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node — block height, sync, peers, Lightning balance — and, when a Claude API key is set, anything else.",
"icon": "/assets/img/app-icons/pine.svg",
"author": "Archipelago",
"category": "home",
"dockerImage": "docker.io/library/nginx:1.31.4-alpine",
"repoUrl": "https://github.com/rhasspy/wyoming"
},
{
"id": "grafana",
"title": "Grafana",
"version": "10.2.0",
"description": "Analytics and monitoring platform. Visualize metrics and create dashboards.",
"icon": "/assets/img/app-icons/grafana.png",
"author": "Grafana Labs",
"category": "data",
"tier": "recommended",
"dockerImage": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
"repoUrl": "https://github.com/grafana/grafana",
"containerConfig": {
"ports": [
"3000:3000"
],
"volumes": [
"/var/lib/archipelago/grafana:/var/lib/grafana"
],
"env": [
"GF_PATHS_DATA=/var/lib/grafana",
"GF_USERS_ALLOW_SIGN_UP=false"
]
}
},
{
"id": "tailscale",
"title": "Tailscale",
"version": "1.78.0",
"description": "Zero-config VPN with WireGuard mesh networking.",
"icon": "/assets/img/app-icons/tailscale.webp",
"author": "Tailscale",
"category": "networking",
"tier": "recommended",
"dockerImage": "source.archipelago-foundation.org/lfg2025/tailscale:stable",
"repoUrl": "https://github.com/tailscale/tailscale",
"containerConfig": {
"ports": [
"8240:8240"
],
"volumes": [
"/var/lib/archipelago/tailscale:/var/lib/tailscale"
],
"env": [
"TS_STATE_DIR=/var/lib/tailscale"
],
"args": [
"sh",
"-c",
"tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait"
]
}
},
{
"id": "portainer",
"title": "Portainer",
"version": "2.19.4",
"description": "Container management web UI for the local Podman socket.",
"icon": "/assets/img/app-icons/portainer.webp",
"author": "Portainer",
"category": "development",
"tier": "optional",
"dockerImage": "source.archipelago-foundation.org/lfg2025/portainer:2.39.6",
"repoUrl": "https://github.com/portainer/portainer",
"containerConfig": {
"ports": [
"9000:9000"
],
"volumes": [
"/var/lib/archipelago/portainer:/data",
"/run/user/1000/podman/podman.sock:/var/run/docker.sock"
],
"notes": "Uses the manifest-owned Podman socket bind mount preparation path."
}
},
{
"id": "netbird",
"title": "NetBird",
"version": "2.38.0",
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point — a TLS proxy in front of the dashboard + server.",
"icon": "/assets/img/app-icons/netbird.svg",
"author": "NetBird",
"category": "networking",
"tier": "recommended",
"dockerImage": "docker.io/library/nginx:1.31.4-alpine",
"repoUrl": "https://github.com/netbirdio/netbird",
"containerConfig": {
"ports": [
"8087:80",
"8086:80",
"3478:3478/udp"
],
"volumes": [
"/var/lib/archipelago/netbird:/var/lib/netbird"
],
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
}
},
{
"id": "uptime-kuma",
"title": "Uptime Kuma",
"version": "1.23.0",
"description": "Self-hosted uptime monitoring.",
"icon": "/assets/img/app-icons/uptime-kuma.webp",
"author": "Uptime Kuma",
"category": "data",
"tier": "recommended",
"dockerImage": "source.archipelago-foundation.org/lfg2025/uptime-kuma:1",
"repoUrl": "https://github.com/louislam/uptime-kuma",
"containerConfig": {
"ports": [
"3002:3001"
],
"volumes": [
"/var/lib/archipelago/uptime-kuma:/app/data"
],
"env": [
"TZ=UTC"
],
"args": [
"--",
"node",
"server/server.js"
]
}
},
{
"id": "photoprism",
"title": "PhotoPrism",
"version": "240915",
"description": "AI-powered photo management with facial recognition.",
"icon": "/assets/img/app-icons/photoprism.svg",
"author": "PhotoPrism",
"category": "data",
"dockerImage": "source.archipelago-foundation.org/lfg2025/photoprism:240915",
"repoUrl": "https://github.com/photoprism/photoprism",
"containerConfig": {
"ports": [
"2342:2342"
],
"volumes": [
"/var/lib/archipelago/photoprism:/photoprism/storage"
],
"env": [
"PHOTOPRISM_ADMIN_PASSWORD=archipelago",
"PHOTOPRISM_DEFAULT_LOCALE=en"
]
}
},
{
"id": "nextcloud",
"title": "Nextcloud",
"version": "29",
"description": "Your own private cloud. File sync, calendars, contacts.",
"icon": "/assets/img/app-icons/nextcloud.webp",
"author": "Nextcloud",
"category": "data",
"dockerImage": "source.archipelago-foundation.org/lfg2025/nextcloud:29",
"repoUrl": "https://github.com/nextcloud/server",
"containerConfig": {
"ports": [
"8085:80"
],
"volumes": [
"/var/lib/archipelago/nextcloud:/var/www/html"
]
}
},
{
"id": "alby-hub",
"title": "Alby Hub",
"version": "1.23.0",
"description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect — one hub, every app pays through it.",
"icon": "/assets/img/app-icons/alby-hub.svg",
"author": "Alby",
"category": "money",
"tier": "optional",
"dockerImage": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.24.0",
"repoUrl": "https://github.com/getAlby/hub"
},
{
"id": "phoenixd",
"title": "phoenixd",
"version": "0.9.0",
"description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own — it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.",
"icon": "/assets/img/app-icons/phoenixd.svg",
"author": "ACINQ",
"category": "money",
"tier": "optional",
"dockerImage": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0",
"repoUrl": "https://github.com/ACINQ/phoenixd"
},
{
"id": "cuprate",
"title": "Cuprate",
"version": "0.1.0-preview",
"description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.",
"icon": "/assets/img/app-icons/cuprate.svg",
"author": "Cuprate contributors",
"category": "money",
"tier": "optional",
"dockerImage": "source.archipelago-foundation.org/lfg2025/cuprate:0.1.0-preview-18-g618ff14",
"repoUrl": "https://github.com/Cuprate/cuprate"
}
]
}
+1 -1
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]]
name = "archipelago"
version = "1.8.6-alpha"
version = "1.8.7-alpha"
dependencies = [
"anyhow",
"archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.8.6-alpha"
version = "1.8.7-alpha"
edition = "2021"
license.workspace = true
description = "Archipelago Bitcoin Node OS - Native backend"
+13
View File
@@ -145,6 +145,19 @@ impl ApiHandler {
/// URL so the App Store still renders on nodes that haven't persisted
/// a registry config yet. 15s total timeout.
async fn handle_app_catalog_proxy(&self) -> Result<Response<hyper::Body>> {
// The daemon already refreshes and verifies releases/app-catalog.json.
// Serve that release-root-anchored cache first so a newly published app
// appears immediately, without a frontend release. The old external UI
// catalog below is emergency compatibility only; it must never override
// a healthy signed catalog (Cuprate was invisible for exactly that reason).
if let Ok(body) = crate::container::app_catalog::verified_catalog_body(&self.config.data_dir).await {
return Ok(Response::builder()
.status(hyper::StatusCode::OK)
.header("Content-Type", "application/json")
.header("Cache-Control", "no-cache")
.body(hyper::Body::from(body))?);
}
let mut upstreams: Vec<String> = Vec::new();
if let Ok(config) = crate::container::registry::load_registries(&self.config.data_dir).await
{
@@ -24,6 +24,7 @@
//! Unknown fields are ignored (no `deny_unknown_fields`), so adding fields on the
//! publisher side never breaks older nodes.
use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
@@ -194,6 +195,25 @@ fn entry_for(app_id: &str) -> Option<AppCatalogEntry> {
load_catalog().apps.get(app_id).cloned()
}
/// Return the cached catalog bytes only when they carry a signature anchored
/// to the release root. This is the browser App Store's source: newly signed
/// apps must appear without waiting for a frontend OTA, while unsigned or
/// self-signed registry data must never become an install button.
pub async fn verified_catalog_body(data_dir: &Path) -> anyhow::Result<String> {
let path = data_dir.join(APP_CATALOG_FILE);
let body = tokio::fs::read_to_string(&path)
.await
.with_context(|| format!("read signed app catalog {}", path.display()))?;
let raw: serde_json::Value = serde_json::from_str(&body)?;
match crate::trust::verify_detached(&raw)? {
crate::trust::SignatureStatus::Verified { anchored: true, .. } => Ok(body),
crate::trust::SignatureStatus::Verified { anchored: false, .. } => {
anyhow::bail!("app catalog signer is not anchored to the release root")
}
crate::trust::SignatureStatus::Unsigned => anyhow::bail!("app catalog is unsigned"),
}
}
/// Primary image for an app per the remote catalog, if covered.
pub fn catalog_primary_image(app_id: &str) -> Option<String> {
entry_for(app_id).and_then(|e| e.image)
@@ -641,4 +661,23 @@ mod tests {
]
);
}
// The signed-catalog body served to the browser must be the anchored,
// release-root-verified bytes — and nothing else. Unsigned caches (the
// migration-window form) and self-consistent-but-unanchored signatures
// must both be refused so a tampered mirror can never become an install
// button (same posture as the OTA manifest supply-chain gate).
#[tokio::test]
async fn verified_catalog_body_rejects_unsigned_cache() {
let dir = tempfile::tempdir().unwrap();
write_cache(dir.path(), r#"{"schema":1,"apps":{"demo":{"version":"1"}}}"#).unwrap();
let err = verified_catalog_body(dir.path()).await.unwrap_err();
assert!(err.to_string().contains("unsigned"));
}
#[tokio::test]
async fn verified_catalog_body_rejects_missing_cache() {
let dir = tempfile::tempdir().unwrap();
assert!(verified_catalog_body(dir.path()).await.is_err());
}
}
+154
View File
@@ -0,0 +1,154 @@
# SESSION — companion 0.5.28: shipped, published, playbook (2026-08-31)
**For: the companion agent (next session) + anyone shipping a companion
release.** Session that closed the 2026-08-30 companion-agent queue (#61
residual, #128, #139) and shipped 0.5.28 end-to-end.
## Release state at session end — ALL LIVE
Companion **0.5.28 / versionCode 48**, main @ PR #149 (`9f1a289d`), deploy
handoff merged as PR #150 (`91374392`). Every public surface verified
byte-identical (`shasum -a 256` = `fc786b46c704c5752f04fe603371365524c749734f17bd8858cf02fa2dbc34ca`):
| Surface | URL | State |
|---|---|---|
| Gitea raw-on-main | `http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/neode-ui/public/packages/archipelago-companion.apk` | ✅ 0.5.28, v1+v2+v3 verified on download |
| Foundation static `/packages/` (real-node QR URL) | `https://source.archipelago-foundation.org/packages/archipelago-companion.apk` | ✅ 0.5.28 |
| Foundation Gitea-raw proxy | `…/lfg2025/archy/raw/branch/main/…` | ✅ 0.5.28 (6h cache — may lag after pushes) |
| Demo `:2100` | `http://146.59.87.168:2100/packages/archipelago-companion.apk` | ✅ 0.5.28 (auto: CI + Portainer webhook) |
Only remaining live-surface step: **node web-bundle redeploys** so each
node's own served copy is 0.5.28 — archi-dev-box's standard step, written up
in `docs/HANDOFF-2026-08-31-companion-0.5.28-deploy.md` (its §1/§3 were
already done by the time of this doc — only §2 outstanding).
Tracker: #128 and #139 closed with what-shipped comments; #61 (already
closed) got a residual-fix follow-up. Signing cert unchanged (`d622e07e…`),
so phones update in place.
## What shipped in 0.5.28 (map)
- **#61 residual (web)**: `isCompanionApp()` gates on `CompanionBanner.vue`
render, `openCompanionIntro()` (useCompanionIntro.ts), and the overlay's
manual-open watcher; overlay moved to the canonical helper. Vitest suite
green (1013 tests).
- **#128 Backup & Restore**: `Android/rust/archy-fips-core/src/backup.rs`
(ADR-005 envelope, node-compatible), `BackupManager.kt`, hub sub-page
`ui/components/BackupSection.kt`. Doc: `companion-backup-restore.md`.
- **#139 Remote Signer**: `src/nostr.rs` (NIP-44 v2 + NIP-04 + BIP-340,
official vectors), `nostr/BunkerManager.kt` + `NostrSignerPreferences.kt`,
hub sub-page `ui/components/SignerSection.kt`, `nostrconnect://` deep link
via `SignerLaunch`. Harness: `Android/tools/nip46-test-client.py`.
Doc: `companion-nip46-remote-signer.md`.
- **Hub modal redesign** (field feedback): both features are sub-pages like
Nodes/FIPS; panel height cap 70%; scanner hosted by NESMenu outside the
panel; back-arrow → hub.
- **Extras**: node mesh ULA shown/copyable in the Nodes list (`MenuItem`
subtitle); `Android/tools/fipssh` (npub→ULA is pure: `fd ‖ sha256(pubkey)[0..15]`,
pinned by `npub_derives_the_same_mesh_ula_as_the_fips_identity` test).
- **Node-side handoffs written**: `HANDOFF-2026-08-31-ssh-over-mesh.md`
(SSH-over-mesh toggle) and the 0.5.28 deploy handoff.
## The deployment playbook (learned the hard way this session)
### Networking — everything goes through the Tor SOCKS proxy
Direct connections to `146.59.87.168` fail from this box ("Bad file
descriptor"); git works because `~/.gitconfig` sets
`proxy = socks5h://127.0.0.1:9050`. **For curl/Gitea API you must pass it
explicitly:**
```bash
curl -s --socks5-hostname 127.0.0.1:9050 ... # works
curl -s ... # HTTP 000, "unreachable"
```
This is why earlier sessions concluded "Gitea API unreachable" — wrong; it
just needs the proxy flag.
### Gitea API + auth
- Base: `http://146.59.87.168:3000/api/v1` (v1.27.1), via the proxy.
- The keychain git credential (`security find-internet-password -s
146.59.87.168`, acct `v4v`) is a **`write:repository`-only token** — fine
for git, CANNOT read/write issues.
- Issue ops need `write:issue`. This session the user pasted a broad token
(activitypub+misc+notification+organization+package+issue+repository) —
**revocation still pending** (it's in chat scrollback). Ask the user for a
scoped `write:issue` token next time.
### main is PROTECTED — ship via -ship branch + PR + API merge
`git push origin main` is rejected by pre-receive. The working sequence:
```bash
git fetch origin
git checkout main && git reset --hard origin/main # local main is STALE (see below)
git merge --no-ff companion/<ver> -m "Companion <ver> — …"
./Android/ship-companion.sh # builds, signs v1+v2+v3, stages APK+meta, commits
# its `git push` FAILS on protected main — expected. Push the branch instead:
git push origin main:companion/<ver>-ship
# then create + merge the PR via API:
curl ... POST repos/lfg2025/archy/pulls {"head":"companion/<ver>-ship","base":"main",...}
curl ... POST repos/lfg2025/archy/pulls/<n>/merge -d '{"Do":"merge"}'
```
(Refinement for next time: run `ship-companion.sh` ON the `-ship` branch
from the start — it pushes the current branch, which for a `-ship` branch
succeeds directly.)
- **Local `main` is the pre-open-source-import lineage** (1115 stale
commits, unrelated history). Always `reset --hard origin/main` before
using it; never merge into it without the reset.
- A **stale tag ref** (`v1.7.115-alpha`) can make `git fetch` fail
("did not send all necessary objects") — `rm .git/refs/tags/v1.7.115-alpha`.
- Last release's `-ship` branch for reference: `origin/companion/0.5.27-ship`.
### Build + verify (per release)
- Version lives in `Android/app/build.gradle.kts` (`versionCode` must
strictly increase; meta json is auto-generated by the publish script from
it). 0.5.28 → next is **0.5.29/vc49**.
- APK package is `com.archipelago.app.debug` (the served artifact IS the
debug build, committed repo keystore, cert SHA-256 `d622e07e…ec2664d`).
Local `Android/app/debug.keystore` is untracked but produces that cert —
verify per release: `apksigner verify --print-certs` on old vs new.
- Build: `cd Android && JAVA_HOME=/opt/homebrew/opt/openjdk@17
ANDROID_HOME=$HOME/Library/Android/sdk ./gradlew :app:assembleDebug`
(builds the Rust via cargo-ndk, NDK under `~/Library/Android/sdk/ndk/`).
Test build for the user: copy to `~/Desktop/archipelago-companion-<ver>.apk`.
- Rust: `cd Android/rust/archy-fips-core && cargo test --lib` (24 tests at
session end) + clippy. neode-ui: `npm ci` first (node_modules not kept),
`npx vitest run`, `npm run type-check`.
- Post-ship verify block: aapt2 badging, shasum vs Gitea raw, apksigner
v1/v2/v3, the three public URLs' meta json (table above), foundation
raw-proxy may serve up to 6h stale (cache-control: max-age=21600).
### Infrastructure facts
- `source.archipelago-foundation.org` = openresty on vps2 with **two
surfaces**: static `/packages/` (manual mirror; the real-node QR URL) and
a Gitea-raw proxy (6h cache, auto). Demo `:2100` redeploys automatically:
`.gitea/workflows/demo-images.yml` fires on `main` pushes touching
`neode-ui/**`, then calls the Portainer webhook.
- **No SSH to vps2 from this box**: `archy_146_release` key declined for
root/archipelago/dorian/lfg2025/deploy/git. Server-side work needs the
archi-dev-box agent or the user.
## Open items for next session
1. **Confirm node web-bundle redeploys** happened (archi-dev-box; deploy
handoff §2) — a paired node's own `/packages/` should serve vc48.
2. **Token revocation** (user) + request a `write:issue`-scoped one.
3. **Node-side roadmap** fed by this release: SSH-over-mesh toggle
(`HANDOFF-2026-08-31-ssh-over-mesh.md`), node NIP-46 client (login flow B),
node-side storage for companion backup envelopes.
4. **On-device follow-ups**: the user's full 0.5.28 pass — signer e2e via
the harness (`/tmp/nip46env/bin/python Android/tools/nip46-test-client.py`),
backup round-trip on a wipe, and the zxing-cpp decision trigger
(move-to-the-code; sketch is verified online:
`io.github.zxing-cpp:android:3.1.1`, still NOT-actioned by design).
5. Untracked on this box, deliberately left: `Android/app/debug.keystore`,
`docs/1.8-alpha-improvements-tracker.md`,
`docs/SESSION-1.8.0-OTA-PROGRESS.md`, `image-recipe/branding/source-logos/`
(other workstreams' files).
@@ -573,7 +573,7 @@ RUN mkdir -p /etc/polkit-1/rules.d && \
# already-deployed nodes over OTA (idempotent no-op here once applied).
RUN set -eu; \
apt-get update; \
apt-get install -y --no-install-recommends kdump-tools kexec-tools rasdaemon; \
apt-get install -y --no-install-recommends kdump-tools kexec-tools makedumpfile rasdaemon; \
apt-get clean; rm -rf /var/lib/apt/lists/*; \
CONF=/etc/default/kdump-tools; \
sed -i 's|^#\?USE_KDUMP=.*|USE_KDUMP="1"|' "$CONF"; \
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "neode-ui",
"version": "1.8.6-alpha",
"version": "1.8.7-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neode-ui",
"version": "1.8.6-alpha",
"version": "1.8.7-alpha",
"dependencies": {
"@scure/bip39": "^2.2.0",
"@types/dompurify": "^3.0.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "neode-ui",
"private": true,
"version": "1.8.6-alpha",
"version": "1.8.7-alpha",
"type": "module",
"scripts": {
"start": "./start-dev.sh",
@@ -256,6 +256,46 @@ describe('useAppLauncherStore', () => {
)
})
// An HTTPS connection must never hand the remote browser (or the phone
// webview) a cleartext app URL: same-host app ports are gate-owned and
// serve TLS on the same port. Plain-http pages keep http exactly as before
// — pinned by every test above this one.
it('upgrades same-host app URLs to https on an https page', () => {
Object.defineProperty(window, 'location', {
value: { origin: 'https://192.0.2.10', protocol: 'https:', hostname: '192.0.2.10' },
writable: true,
configurable: true,
})
const store = useAppLauncherStore()
store.open({ url: 'http://192.0.2.10:8082', title: 'Vaultwarden' })
expect(store.isOpen).toBe(false)
expect(store.panelAppId).toBe(null)
expect(mockWindowOpen).toHaveBeenCalledWith(
'https://192.0.2.10:8082',
'_blank',
'noopener,noreferrer',
)
})
it('never upgrades a different host on an https page', () => {
Object.defineProperty(window, 'location', {
value: { origin: 'https://192.0.2.10', protocol: 'https:', hostname: '192.0.2.10' },
writable: true,
configurable: true,
})
const store = useAppLauncherStore()
store.open({ url: 'http://192.168.1.100:8082', title: 'Vaultwarden' })
expect(mockWindowOpen).toHaveBeenCalledWith(
'http://192.168.1.100:8082',
'_blank',
'noopener,noreferrer',
)
})
it('opens Gitea path URL in new tab', () => {
const store = useAppLauncherStore()
+20 -42
View File
@@ -4,7 +4,7 @@ import { rpcClient } from '@/api/rpc-client'
import { recordAppLaunch } from '@/utils/appUsage'
import { requestExternalOpen } from '@/api/remote-relay'
import { openInAppOrNewTab, isCompanionApp, type InAppLaunchMeta } from '@/utils/openExternal'
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
import { directAppUrl, HTTPS_APP_IDS, resolveAppUrl } from '@/views/appSession/appSessionConfig'
import { useAppStore } from '@/stores/app'
import { resolveAppIcon } from '@/views/apps/appsConfig'
import { IS_DEMO, isDemoApp, isDemoExternal, demoAppUrl } from '@/composables/useDemoIntro'
@@ -60,9 +60,6 @@ const NEW_TAB_APP_IDS = new Set([
'netbird',
])
// Apps served over HTTPS (self-signed) rather than plain HTTP.
const HTTPS_APP_IDS = new Set(['netbird'])
function mustOpenInNewTab(url: string): boolean {
try {
const u = new URL(url)
@@ -147,33 +144,7 @@ const PORT_TO_APP_ID: Record<string, string> = {
'50002': 'electrumx',
}
const APP_ID_TO_PORT: Record<string, string> = {
'btcpay-server': '23000',
grafana: '3000',
photoprism: '2342',
homeassistant: '8123',
vaultwarden: '8082',
nextcloud: '8085',
portainer: '9000',
tailscale: '8240',
'nginx-proxy-manager': '8081',
'uptime-kuma': '3002',
gitea: '3001',
// Without this, directAppUrl('netbird') returns null and netbird falls
// through to the iframe (and never gets its https URL) — issue #15.
netbird: '8087',
}
function directAppUrl(appId: string): string | null {
const port = APP_ID_TO_PORT[appId]
if (!port || typeof window === 'undefined') return null
const scheme = HTTPS_APP_IDS.has(appId) ? 'https' : 'http'
return `${scheme}://${window.location.hostname}:${port}`
}
const APPROVED_ORIGINS_KEY = 'neode_nostr_approved_origins'
function getApprovedOrigins(): Set<string> {
try {
const stored = localStorage.getItem(APPROVED_ORIGINS_KEY)
@@ -285,18 +256,25 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
let launchUrl = normalizeLaunchUrl(payload.url, titleHintId)
const resolvedId = resolveAppIdFromUrl(launchUrl) || titleHintId
// Apps served over HTTPS (e.g. netbird, which needs a secure context for
// its OIDC dashboard) must be launched over https — a stale http URL hits
// the TLS port and 400s. Upgrade the scheme defensively in every path.
if (resolvedId && HTTPS_APP_IDS.has(resolvedId)) {
try {
const u = new URL(launchUrl, window.location.origin)
if (u.protocol === 'http:') {
u.protocol = 'https:'
launchUrl = u.href
}
} catch { /* leave as-is */ }
}
// Scheme discipline for everything launched on this host. App ports are
// owned by the app gate, which serves TLS on the same port whenever the
// node has a certificate — so on an HTTPS connection every same-host
// app URL must be https: plain http is a silent downgrade at best and
// mixed-content-blocked at worst (remote browsers, the companion
// webview). Apps that are ALWAYS https (netbird's secure-context OIDC
// dashboard) upgrade regardless of the page, and external hosts keep
// their own scheme.
try {
const u = new URL(launchUrl, window.location.origin)
const sameHost = u.hostname === window.location.hostname
const alwaysHttps = !!resolvedId && HTTPS_APP_IDS.has(resolvedId)
const httpsPage = window.location.protocol === 'https:'
if (u.protocol === 'http:' && (alwaysHttps || (httpsPage && sameHost && (resolvedId || mustOpenInNewTab(launchUrl))))) {
// Pure prefix swap — never re-serialize the URL (URL.href would add
// a trailing slash and change the string the caller handed over).
launchUrl = launchUrl.replace(/^http:\/\//i, 'https://')
}
} catch { /* leave as-is */ }
if (!isMobileViewport() && payload.openInNewTab) {
if (resolvedId) recordAppLaunch(resolvedId)
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { NEW_TAB_APPS, resolveAppUrl } from '../appSessionConfig'
import { NEW_TAB_APPS, directAppUrl, resolveAppUrl } from '../appSessionConfig'
import { GENERATED_NEW_TAB_APPS } from '../generatedAppSessionConfig'
describe('appSessionConfig', () => {
@@ -68,4 +68,51 @@ describe('appSessionConfig', () => {
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:18083')).toBe('http://192.0.2.10:18083')
})
// The direct-port launch path (new-tab apps on desktop, the companion's
// native WebView on phones) used to hardcode http:// — so a node reached
// over HTTPS opened Vaultwarden and friends in cleartext. These pin the
// scheme-following contract on both page schemes.
it('builds direct app URLs on the page scheme — https page, https app', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10', protocol: 'https:' },
writable: true,
configurable: true,
})
expect(directAppUrl('vaultwarden')).toBe('https://192.0.2.10:8082')
expect(directAppUrl('gitea')).toBe('https://192.0.2.10:3001')
expect(directAppUrl('btcpay-server')).toBe('https://192.0.2.10:23000')
})
it('keeps plain-http direct app URLs on a plain-http page', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10', protocol: 'http:' },
writable: true,
configurable: true,
})
expect(directAppUrl('vaultwarden')).toBe('http://192.0.2.10:8082')
})
it('always launches secure-context apps over https, on either page scheme', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10', protocol: 'http:' },
writable: true,
configurable: true,
})
expect(directAppUrl('netbird')).toBe('https://192.0.2.10:8087')
})
it('resolves session app URLs on the page scheme too (https page)', () => {
Object.defineProperty(window, 'location', {
value: { hostname: '192.0.2.10', protocol: 'https:' },
writable: true,
configurable: true,
})
expect(resolveAppUrl('mempool')).toBe('https://192.0.2.10:4080')
expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:18083')).toBe('https://192.0.2.10:18083')
})
})
@@ -166,6 +166,49 @@ function pageScheme(): string {
return p === 'https:' || p === 'http:' ? p : 'http:'
}
/** Apps served over HTTPS (self-signed) rather than plain HTTP, regardless of
* the page's scheme. */
export const HTTPS_APP_IDS = new Set(['netbird'])
/** App ID -> direct launch port for the paths that bypass the in-app session:
* new-tab apps and the companion's native WebView. Every port here is owned
* by the app gate (manifest `auth: gated`/`open` + `bind: 127.0.0.1`), which
* serves TLS on the same port whenever the node has a certificate. */
export const DIRECT_APP_PORTS: Record<string, string> = {
'btcpay-server': '23000',
grafana: '3000',
photoprism: '2342',
homeassistant: '8123',
vaultwarden: '8082',
nextcloud: '8085',
portainer: '9000',
tailscale: '8240',
'nginx-proxy-manager': '8081',
'uptime-kuma': '3002',
gitea: '3001',
// Without this, directAppUrl('netbird') returns null and netbird falls
// through to the iframe (and never gets its https URL) — issue #15.
netbird: '8087',
}
/** Direct-port launch URL for an app, on the page's scheme.
*
* These are the apps that open OUTSIDE the dashboard's own origin — a new
* browser tab on the desktop, or the companion's in-app WebView on a phone.
* The URL is handed to a context with no dashboard chrome, so it must carry
* the scheme the remote browser actually reached the node on: on an HTTPS
* connection, `http://host:port` is at best a silent downgrade to cleartext
* and at worst blocked outright as mixed content. Every port in
* DIRECT_APP_PORTS is served by the app gate with TLS on the same port
* (see appgate/tls.rs), so following the page scheme is always answerable.
* Plain-HTTP dashboards keep today's behaviour exactly. */
export function directAppUrl(appId: string): string | null {
const port = DIRECT_APP_PORTS[appId]
if (!port || typeof window === 'undefined') return null
const scheme = HTTPS_APP_IDS.has(appId) || pageScheme() === 'https:' ? 'https' : 'http'
return `${scheme}://${window.location.hostname}:${port}`
}
/** Resolve a human-readable title for an app */
export function resolveAppTitle(id: string): string {
return APP_TITLES[id] || id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
+8 -2
View File
@@ -3,7 +3,7 @@
import type { Ref } from 'vue'
import { computed } from 'vue'
import { PackageState, type PackageDataEntry } from '@/types/api'
import { resolveAppUrl } from '../appSession/appSessionConfig'
import { matchPageScheme, resolveAppUrl } from '../appSession/appSessionConfig'
import { isAutoTabApp } from '@/utils/autoTabApps'
export type AppsTab = 'apps' | 'websites' | 'services'
@@ -299,7 +299,13 @@ export function launchBlockedReason(id: string, pkg?: PackageDataEntry | null):
export function resolveRuntimeLaunchUrl(pkg: PackageDataEntry): string {
const addr = runtimeLanAddress(pkg)
if (!addr || typeof window === 'undefined') return addr
return addr.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, `http://${window.location.hostname}`)
const local = addr.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, `http://${window.location.hostname}`)
// The backend reports runtime URLs as http:// because that is how the app
// binds locally — on an HTTPS connection that is a cleartext downgrade
// (and mixed-content-blocked when opened from the dashboard). The gate
// serves TLS on every app port, so follow the page's scheme, exactly like
// resolveAppUrl() does for the same runtime URLs.
return matchPageScheme(local)
}
export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string {
@@ -362,6 +362,20 @@ init()
</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.8.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.8.7-alpha</span>
<span class="text-xs text-white/40">August 31, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p><strong>What's New really does stop at v1.8.0 now.</strong> The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.</p>
<p><strong>The installer carries the same corrected release and Companion 0.5.28.</strong> Its artifact gate now checks the companion APK version and the v1.8.0 What's New floor inside the finished ISO, so a stale frontend or phone app cannot be published under the current release label.</p>
<p><strong>Crash dumps work on fresh installs as well as upgraded nodes.</strong> The installer gate checks every kdump package inside the finished ISO, and makedumpfile is installed explicitly rather than accidentally relying on a recommended dependency that the minimal image deliberately omits.</p>
<p><strong>Apps open over HTTPS when your node does.</strong> Connect to your node over HTTPS and every app you open — Vaultwarden in its own tab, BTCPay, Grafana, and the rest, on a remote browser or in the phone's in-app browser — now opens on the same secure connection instead of silently dropping to plain HTTP. The node's app gate already served TLS on every app port; the dashboard was handing out http:// addresses regardless of how you reached it. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.</p>
<p><strong>Newly signed apps appear in the App Store immediately.</strong> The App Store now serves the release-signed catalog the node has already fetched and verified — so publishing a signed app (like Cuprate) makes it appear for every updated node without waiting for a dashboard release. The unsigned community catalog remains only as a fallback for nodes that can't reach the registry. Cuprate was invisible on updated nodes for exactly this reason; it is now in both.</p>
</div>
</div>
<!-- v1.8.6-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
@@ -459,100 +473,6 @@ init()
<p><strong>The Receive window now tells you when the money is on its way.</strong> Previously it showed a QR code and left you to check elsewhere whether anything happened. Now, the moment the sender's transaction is broadcast, the QR gives way to a clock: the amount, the transaction ID (tap to copy), and a note that the funds arrive on their own — with a single Done button. If you keep the window open, the clock becomes a green check at the first confirmation. Verified live on a real node: payment detected within seconds of broadcast.</p>
</div>
</div>
<!-- alpha.9 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.9</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Security Hardening Complete</h4>
<p>All 12 pentest findings fixed. CSRF tokens now survive restarts. Password hashing upgraded to Argon2id. Bitcoin RPC gets a unique random password on every install. Federation messages require ed25519 signatures.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">7 Bugs Squashed</h4>
<p>Random logouts fixed (P0). Uninstall dialog is now a proper full-screen modal with an "Uninstalling..." overlay. App cards no longer flicker between Start/Launch during container scans. ElectrumX index estimate corrected.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">Bitcoin Sync on Dashboard</h4>
<p>Homepage System card now shows Bitcoin Core sync progress, block height, and green/orange status indicator when Bitcoin is running.</p>
</div>
</div>
</div>
<!-- alpha.8 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.8</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Pentest Remediation (9/12)</h4>
<p>Fixed 9 of 12 security findings: session auth on LND connect info, DEV_MODE removed from production, ed25519 signature verification on node messages, path traversal protection, NIP-07 origin validation, AIUI session checks, strict onion validation.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">UI Polish Batch</h4>
<p>Fedimint renamed to "Fedimint Guardian". Tab-launch icons. Marketplace sorts installed apps to end. Mesh mobile layout fixed. On-Chain first in receive modals. Federation shows names instead of DIDs. Cleaner iframe error screens.</p>
</div>
</div>
</div>
<!-- alpha.7 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.7</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Marketplace & Credentials</h4>
<p>29 containers running rootless. Marketplace app aliases working. Credential injection for inter-container authentication.</p>
</div>
</div>
</div>
<!-- alpha.4-6 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.4-6</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Rootless Podman Migration</h4>
<p>Migrated all containers from root to rootless Podman. UID namespace mapping, volume ownership fixes, sysctl tuning. Bitcoin RPC verified, all web services confirmed healthy. 29 containers up and running.</p>
</div>
</div>
</div>
<!-- alpha.2-3 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.2-3</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Systemd Hardening Restored</h4>
<p>Full systemd security sandbox restored now that containers run rootless. NoNewPrivileges, restricted namespaces, and system call filtering re-enabled. Session persistence and boot sequence fixes.</p>
</div>
</div>
</div>
<!-- alpha.1 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.1</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Mesh Radio & Container Stability</h4>
<p>LoRa mesh radio auto-detects USB port changes with a new Connect button. Fixed container crash loops — all apps start cleanly and stay stable. Apps starting up show progress instead of re-appearing in the store. Tor routing enabled by default for Bitcoin and Lightning.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">Off-Grid Bitcoin</h4>
<p>Receive Bitcoin block headers over mesh radio. Dead man's switch broadcasts location to trusted contacts if you go silent. GPS sharing is opt-in only.</p>
</div>
</div>
</div>
</div>
<button @click="showReleaseNotes = false" class="glass-button w-full mt-4 py-2 text-sm shrink-0">Close</button>
</div>
+16 -18
View File
@@ -1,31 +1,29 @@
{
"changelog": [
"**Cuprate — an independent Monero node — is now an app.** Monero consensus validated by a second, unrelated codebase (Rust), the same layer of security-in-depth Bitcoin gets from Knots. Review caught two problems before anything shipped: the unrestricted RPC that can move funds stayed bound to the container's loopback (never published to the node, let alone the LAN — anything on the node could previously have reached it), and its restricted RPC moved off port 18089 to avoid colliding with Penpot. Honest caveat: upstream has cut no stable release yet, so the pin tracks an exact preview build (0.1.0-preview-18-g618ff14) and moves to their first tagged release when there is one.",
"**A frozen node now explains itself — and comes back on its own.** The host now captures a memory dump into /var/crash when the kernel panics *or* wedges (a hung kiosk used to sit dead until someone power-cycled it; now it dumps, reboots itself, and leaves the evidence behind), and records failing-memory signals (ECC errors) into a database as they happen. This is the first change delivered by a new host-update channel: the node's own updater now carries OS-level packages and settings to already-deployed machines — the crash-kernel's memory reservation is the one part that waits for a reboot, and the node says so rather than pretending.",
"**Uninstalling an app can no longer report success when it failed.** The declarative path used to swallow every teardown error and report the app uninstalled, leaving the tile behind and the truth in the logs. A failed uninstall now stops and shows the real per-app errors, so \"still there\" is never presented as \"gone\".",
"**Pictures to internet-only mesh contacts work now.** Sending an attachment inline always took the radio path and failed with \"Peer is federation-only (no radio twin)\" for contacts reachable only over the internet — and the size-adviser kept recommending a radio transfer those peers can't receive. Both fixed: inline sends route over the federation when that's the only way to reach the peer, and the advice no longer offers radio-only transfers to radio-unreachable contacts.",
"**Disk cleanup finally has honest numbers.** Space \"free\" on a drive was counted including the slice the filesystem keeps reserved for root — roughly 5% of the disk, 92 GB on one dev box — so the automatic cleanup that's supposed to kick in at 90% never triggered and stale container images piled up unnoticed. Reserved space now counts as used, which is what the threshold was always meant to measure."
"**Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.",
"**What's New now starts cleanly at v1.8.0 and is guaranteed to be newest-first.** Older alpha history no longer overwhelms the useful recent changes, the three stray v1.7 entries that appeared above current releases are gone, and the release check now fails if either the ordering or the v1.8.0 history floor drifts again.",
"**A release can no longer advertise itself before its files exist.** New releases are prepared behind a pending manifest; the publisher uploads the backend and frontend, downloads both back and verifies their size and hash, and only then promotes the signed manifest to the path nodes read. The manifest generator also includes every curated What's New item instead of silently stopping after the first ten physical changelog lines."
],
"components": [
{
"current_version": "1.8.5-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.5-alpha/archipelago",
"current_version": "1.8.6-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago",
"name": "archipelago",
"new_version": "1.8.5-alpha",
"sha256": "54e91944c6395a53c8ac87ea97f61e8a7fc5ffd133f017180931ba4fa9566239",
"size_bytes": 63934504
"new_version": "1.8.6-alpha",
"sha256": "6171e1f46b0e2c2754a18e668f16810338a09b71a68dc365d65173ca1e4d400a",
"size_bytes": 63930112
},
{
"current_version": "1.8.5-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.5-alpha/archipelago-frontend-1.8.5-alpha.tar.gz",
"name": "archipelago-frontend-1.8.5-alpha.tar.gz",
"new_version": "1.8.5-alpha",
"sha256": "a5d773e8225bfd8a34dfc74acf38bdc2c39bc138c62bf9f4137d0efd72cce3e9",
"size_bytes": 97657151
"current_version": "1.8.6-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago-frontend-1.8.6-alpha.tar.gz",
"name": "archipelago-frontend-1.8.6-alpha.tar.gz",
"new_version": "1.8.6-alpha",
"sha256": "1a5b54c516f91a4ea4491e08182f0f9521ee7e27935388814007f91695f06e0f",
"size_bytes": 97769923
}
],
"release_date": "2026-08-31",
"signature": "f0bcec4935588ee428ebabfbb6e08e00dbd42202af92763073a7f98f4785925ca3565bf96fecbdd1695eff77fcf906783fd9053d82730b11c75441c6f5a60b05",
"signature": "c21dc5cb5a6caa3253c08835b65d0417a312bc8c51acdf92df990ad2110ae762b77287c479a523a8521827ec7b4695114ff9c39924cf1d30d0d622abb53b9208",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.5-alpha"
"version": "1.8.6-alpha"
}
+16 -18
View File
@@ -1,31 +1,29 @@
{
"changelog": [
"**Cuprate — an independent Monero node — is now an app.** Monero consensus validated by a second, unrelated codebase (Rust), the same layer of security-in-depth Bitcoin gets from Knots. Review caught two problems before anything shipped: the unrestricted RPC that can move funds stayed bound to the container's loopback (never published to the node, let alone the LAN — anything on the node could previously have reached it), and its restricted RPC moved off port 18089 to avoid colliding with Penpot. Honest caveat: upstream has cut no stable release yet, so the pin tracks an exact preview build (0.1.0-preview-18-g618ff14) and moves to their first tagged release when there is one.",
"**A frozen node now explains itself — and comes back on its own.** The host now captures a memory dump into /var/crash when the kernel panics *or* wedges (a hung kiosk used to sit dead until someone power-cycled it; now it dumps, reboots itself, and leaves the evidence behind), and records failing-memory signals (ECC errors) into a database as they happen. This is the first change delivered by a new host-update channel: the node's own updater now carries OS-level packages and settings to already-deployed machines — the crash-kernel's memory reservation is the one part that waits for a reboot, and the node says so rather than pretending.",
"**Uninstalling an app can no longer report success when it failed.** The declarative path used to swallow every teardown error and report the app uninstalled, leaving the tile behind and the truth in the logs. A failed uninstall now stops and shows the real per-app errors, so \"still there\" is never presented as \"gone\".",
"**Pictures to internet-only mesh contacts work now.** Sending an attachment inline always took the radio path and failed with \"Peer is federation-only (no radio twin)\" for contacts reachable only over the internet — and the size-adviser kept recommending a radio transfer those peers can't receive. Both fixed: inline sends route over the federation when that's the only way to reach the peer, and the advice no longer offers radio-only transfers to radio-unreachable contacts.",
"**Disk cleanup finally has honest numbers.** Space \"free\" on a drive was counted including the slice the filesystem keeps reserved for root — roughly 5% of the disk, 92 GB on one dev box — so the automatic cleanup that's supposed to kick in at 90% never triggered and stale container images piled up unnoticed. Reserved space now counts as used, which is what the threshold was always meant to measure."
"**Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.",
"**What's New now starts cleanly at v1.8.0 and is guaranteed to be newest-first.** Older alpha history no longer overwhelms the useful recent changes, the three stray v1.7 entries that appeared above current releases are gone, and the release check now fails if either the ordering or the v1.8.0 history floor drifts again.",
"**A release can no longer advertise itself before its files exist.** New releases are prepared behind a pending manifest; the publisher uploads the backend and frontend, downloads both back and verifies their size and hash, and only then promotes the signed manifest to the path nodes read. The manifest generator also includes every curated What's New item instead of silently stopping after the first ten physical changelog lines."
],
"components": [
{
"current_version": "1.8.5-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.5-alpha/archipelago",
"current_version": "1.8.6-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago",
"name": "archipelago",
"new_version": "1.8.5-alpha",
"sha256": "54e91944c6395a53c8ac87ea97f61e8a7fc5ffd133f017180931ba4fa9566239",
"size_bytes": 63934504
"new_version": "1.8.6-alpha",
"sha256": "6171e1f46b0e2c2754a18e668f16810338a09b71a68dc365d65173ca1e4d400a",
"size_bytes": 63930112
},
{
"current_version": "1.8.5-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.5-alpha/archipelago-frontend-1.8.5-alpha.tar.gz",
"name": "archipelago-frontend-1.8.5-alpha.tar.gz",
"new_version": "1.8.5-alpha",
"sha256": "a5d773e8225bfd8a34dfc74acf38bdc2c39bc138c62bf9f4137d0efd72cce3e9",
"size_bytes": 97657151
"current_version": "1.8.6-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago-frontend-1.8.6-alpha.tar.gz",
"name": "archipelago-frontend-1.8.6-alpha.tar.gz",
"new_version": "1.8.6-alpha",
"sha256": "1a5b54c516f91a4ea4491e08182f0f9521ee7e27935388814007f91695f06e0f",
"size_bytes": 97769923
}
],
"release_date": "2026-08-31",
"signature": "f0bcec4935588ee428ebabfbb6e08e00dbd42202af92763073a7f98f4785925ca3565bf96fecbdd1695eff77fcf906783fd9053d82730b11c75441c6f5a60b05",
"signature": "c21dc5cb5a6caa3253c08835b65d0417a312bc8c51acdf92df990ad2110ae762b77287c479a523a8521827ec7b4695114ff9c39924cf1d30d0d622abb53b9208",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.5-alpha"
"version": "1.8.6-alpha"
}
@@ -1,29 +0,0 @@
{
"changelog": [
"**Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.",
"**What's New now starts cleanly at v1.8.0 and is guaranteed to be newest-first.** Older alpha history no longer overwhelms the useful recent changes, the three stray v1.7 entries that appeared above current releases are gone, and the release check now fails if either the ordering or the v1.8.0 history floor drifts again.",
"**A release can no longer advertise itself before its files exist.** New releases are prepared behind a pending manifest; the publisher uploads the backend and frontend, downloads both back and verifies their size and hash, and only then promotes the signed manifest to the path nodes read. The manifest generator also includes every curated What's New item instead of silently stopping after the first ten physical changelog lines."
],
"components": [
{
"current_version": "1.8.6-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago",
"name": "archipelago",
"new_version": "1.8.6-alpha",
"sha256": "6171e1f46b0e2c2754a18e668f16810338a09b71a68dc365d65173ca1e4d400a",
"size_bytes": 63930112
},
{
"current_version": "1.8.6-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago-frontend-1.8.6-alpha.tar.gz",
"name": "archipelago-frontend-1.8.6-alpha.tar.gz",
"new_version": "1.8.6-alpha",
"sha256": "1a5b54c516f91a4ea4491e08182f0f9521ee7e27935388814007f91695f06e0f",
"size_bytes": 97769923
}
],
"release_date": "2026-08-31",
"signature": "c21dc5cb5a6caa3253c08835b65d0417a312bc8c51acdf92df990ad2110ae762b77287c479a523a8521827ec7b4695114ff9c39924cf1d30d0d622abb53b9208",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.6-alpha"
}
+24 -5
View File
@@ -165,16 +165,35 @@ else
sudo rm -rf "$TMPBIN"
fi
# ── Frontend payload present ─────────────────────────────────────────
if [ -f "$MNT/archipelago/web-ui/index.html" ]; then
# ── Frontend + companion payload ─────────────────────────────────────
WEB_UI="$MNT/archipelago/web-ui"
if [ -f "$WEB_UI/index.html" ]; then
ok "frontend payload (archipelago/web-ui/index.html)"
if [ -f "$MNT/archipelago/web-ui/aiui/index.html" ]; then
if [ -f "$WEB_UI/aiui/index.html" ]; then
ok "AIUI included in frontend payload"
else
warn "AIUI missing from archipelago/web-ui (verify rootfs copy before shipping)"
bad "AIUI missing from archipelago/web-ui"
fi
COMPANION_META="$WEB_UI/packages/archipelago-companion.json"
COMPANION_APK="$WEB_UI/packages/archipelago-companion.apk"
if [ -s "$COMPANION_APK" ] && [ "$(jq -r '.versionName // empty' "$COMPANION_META" 2>/dev/null)" = "0.5.28" ] \
&& [ "$(jq -r '.versionCode // empty' "$COMPANION_META" 2>/dev/null)" = "48" ]; then
ok "Companion 0.5.28 (versionCode 48) APK included"
else
bad "Companion 0.5.28 APK/metadata missing or stale"
fi
SETTINGS_BUNDLE="$(grep -rlF "v$EXPECTED_VERSION" "$WEB_UI/assets" 2>/dev/null | head -1)"
if [ -n "$SETTINGS_BUNDLE" ] \
&& grep -qF 'v1.8.0-alpha' "$SETTINGS_BUNDLE" \
&& ! grep -qE 'v1\.[0-7]\.|v1\.2\.0-alpha' "$SETTINGS_BUNDLE"; then
ok "What's New is v1.8.6-first with a v1.8.0 history floor"
else
bad "What's New payload is missing v1.8 history or still contains pre-v1.8 entries"
fi
else
warn "no archipelago/web-ui payload on ISO (frontend may live in rootfs.tar only)"
bad "no archipelago/web-ui payload on ISO"
fi
echo
+15 -3
View File
@@ -73,10 +73,15 @@ def undated_versions():
def ordered_versions():
"""Return modal versions in display order (top to bottom)."""
"""Return generated modal versions in display order (top to bottom)."""
return re.findall(r"<!-- (v\d+\.\d+\.\d+\S*) -->", MODAL.read_text())
def legacy_blocks():
"""Return old hand-written alpha blocks that predate generated markers."""
return re.findall(r"<!-- (alpha\.[^ ]+) -->", MODAL.read_text())
def version_key(version):
match = re.match(r"v(\d+)\.(\d+)\.(\d+)", version)
return tuple(map(int, match.groups()))
@@ -85,7 +90,10 @@ def version_key(version):
def sort_modal_blocks(entries):
"""Re-render current release-note blocks newest-first and remove old history."""
lines = MODAL.read_text().splitlines(keepends=True)
marker = re.compile(r"^\s*<!-- (v\d+\.\d+\.\d+\S*) -->\s*$")
# Include the old hand-written `alpha.*` blocks in the replace range so
# normalization can delete them. Previously the checker saw only generated
# vX.Y.Z markers and falsely claimed the v1.8.0 history floor was enforced.
marker = re.compile(r"^\s*<!-- ((?:v\d+\.\d+\.\d+\S*)|(?:alpha\.[^ ]+)) -->\s*$")
blocks = []
for start, line in enumerate(lines):
@@ -115,7 +123,10 @@ def sort_modal_blocks(entries):
raise RuntimeError("unexpected content between What's New release blocks")
by_version = {entry["ver"]: entry for entry in entries}
retained = [b for b in blocks if version_key(b[2]) >= MIN_VISIBLE_VERSION]
retained = [
b for b in blocks
if b[2].startswith("v") and version_key(b[2]) >= MIN_VISIBLE_VERSION
]
sorted_segments = [
render_block(by_version[b[2]]).splitlines(keepends=True)
if b[2] in by_version else b[3]
@@ -181,6 +192,7 @@ def main():
expected_order = sorted(displayed, key=version_key, reverse=True)
out_of_order = displayed != expected_order
too_old = [v for v in displayed if version_key(v) < MIN_VISIBLE_VERSION]
too_old.extend(legacy_blocks())
if not missing and not out_of_order and not too_old:
changed = False if check else sort_modal_blocks(entries)