- New "Served by {mirror}" line on the System Update page so operators can see
which mirror actually served the available manifest (vs. which is configured
primary). Backend threads the served URL through UpdateState.manifest_mirror.
- New update.test-mirror RPC + per-row lightning-bolt button that pings a
mirror and renders reachable/latency or error inline under the URL.
- UI polish on the mirrors section: Set Primary, Remove, and the new Test
action are compact icon buttons; add-mirror form moved into a dialog.
- "What's New" block prepended for v1.7.27-alpha.
21/21 update module tests pass. vue-tsc + vite build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a multi-mirror manifest fetch. `check_for_updates` walks a
configurable list (data_dir/update-mirrors.json) in priority order
and falls through to the next mirror on any HTTP / parse / timeout
failure. Two defaults bake in: Server 1 (git.tx1138.com) and Server 2
(23.182.128.160:3000).
Critical fix: after parsing a manifest, rewrite every component's
`download_url` so its origin matches the manifest URL we fetched.
Before this, the manifest hard-coded absolute URLs pointing at one
specific server — so even when a node fetched the manifest from a
faster mirror, the actual 200MB download went back to the slow
original. Now the faster mirror wins end-to-end.
New RPCs: update.list-mirrors, update.add-mirror, update.remove-mirror,
update.set-primary-mirror. New UI section on the System Update page
for operator management. 5 new unit tests for origin parsing and
manifest rewriting (21/21 green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-adds the TCP transport (`0.0.0.0:8443`) to the rendered fips.yaml
alongside UDP. Upstream factory default enables both; we had
inadvertently narrowed to UDP-only when the yaml rewriter was last
touched, which left nodes unable to reach fips.v0l.io (the public
anchor only answers on TCP right now) or talk across networks that
block UDP.
Backend startup now compares the installed yaml against the current
rendered schema and restarts whichever fips unit is active when they
differ — so OTA-upgrading nodes pick up the new transport without
anyone having to click Reconnect.
Dropped the earlier plan to auto-add federated peers as seed anchors:
invites don't carry a FIPS-reachable IP:port, and once TCP reconnects
the public mesh, federated peers become npub-routable without needing
a seed entry.
Seed Anchors modal cleanup: replaced malformed header icon with a
three-arc broadcast glyph, and the close button now matches the
What's New modal (embedded in the card header, same icon + hover
style) instead of the earlier floating off-design placeholder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The npm run build step in the release ritual had been silently failing
for roughly seven releases. vue-tsc died with EACCES on a root-owned
node_modules/.tmp, exited non-zero, and my `tail -5` of the build
output happened to only show vite's precache summary — which makes
vite look successful even when the typecheck that precedes it failed.
The resulting archipelago-frontend-*.tar.gz files were rebuilds from
whatever content happened to live in web/dist/neode-ui/ at the moment
(files left over from v1.7.9, owned root:root from an earlier sudo'd
operation, unchanged since).
Fixed by chowning both paths back to the archipelago user and
rebuilding. Every published frontend tarball from v1.7.17 through
v1.7.23 therefore shipped the same frozen UI; v1.7.24 is the first
release in that stretch whose frontend actually matches its backend.
Recorded the build-verification rule as a persistent feedback memory
(feedback_frontend_build_verify.md) — future ships must grep the
packaged tarball for the new version string before push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a gear button next to the FIPS Mesh card's status pill that
opens a Teleport-ed modal containing FipsSeedAnchorsCard. The card
was landed on disk in v1.7.21 but never wired into a UI entry point
per the entry-point convention, so users couldn't access the
Add/Remove/Apply controls at all. One gear click now opens them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fips::service::active_unit() picks whichever fips unit is running
(archipelago-fips.service vs upstream fips.service) so
handle_fips_restart and handle_fips_reconnect don't silently no-op
on hosts where the archipelago-managed unit was never created.
- peer_connectivity_summary(anchor_candidates) replaces the old
identity-cache check. anchor_connected is now true when at least
one authenticated peer's npub matches the public anchor OR any
entry in seed-anchors.json, which matches what the user actually
cares about ("am I in the mesh?") rather than what the card used
to claim ("is this one specific public anchor reachable?").
- FipsStatus::query takes data_dir now (so it can read seed-anchors)
rather than identity_dir. All call-sites updated.
- handle_fips_reconnect re-pushes seed anchors after restart so the
new daemon gets dialed without waiting for the 5-min apply loop.
- FipsNetworkCard label drops "(fips.v0l.io)" — misleading now that
multiple anchors may be configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a local seed-anchor list at <data_dir>/seed-anchors.json. Each
entry is {npub, address, transport, label}. On archipelago startup
and every 5 minutes the list is pushed into the running fips daemon
via `fipsctl connect <npub> <addr> <transport>`, so a cluster can
anchor itself independently of the global fips.v0l.io. A flaky or
unreachable public anchor no longer strands a fresh install.
New RPCs:
- fips.list-seed-anchors
- fips.add-seed-anchor (validates npub1… + host:port)
- fips.remove-seed-anchor
- fips.apply-seed-anchors (on-demand re-dial)
New standalone UI card at views/server/FipsSeedAnchorsCard.vue. Not
wired into Home.vue / Server.vue — operator places it per the
entry-point convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 3AM auto-update path called std::process::exit(0) immediately
after apply_update returned. apply_update had already spawned a 2s-
delayed systemctl restart, but exit(0) killed the runtime before that
spawned task could run — and the unit's Restart=on-failure does not
trigger on a clean exit 0, so the service stayed dead until someone
SSH'd in and started it manually (.253 hit this today).
Scheduler now returns from the task without killing the process;
apply_update's existing restart path (same one the UI's Install
Update button uses) brings the new version up cleanly.
Also hardens the ISO CI: the AIUI inclusion step now falls back to
extracting from the newest release tarball if the runner's cached
/opt/archipelago/web-ui/aiui path is missing, so a reprovisioned
runner can't silently ship a frontend tarball without AIUI. The ISO
build step also sanity-checks the binary exists before invoking the
builder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
load_state now drops any stored available_update whenever the running
binary version differs from what's on disk — the old migration only
cleared it when the stale entry happened to match the new version, so
skipping releases (e.g. sideloading 1.7.16 → 1.7.18 without 1.7.17)
left a pointer to an intermediate version as the "update available",
which the UI then offered as a downgrade prompt.
check_for_updates also uses a numeric version comparator so a stale or
cached manifest with an older version can't offer itself as an
update, and 1.7.10 correctly outranks 1.7.9 past the single-digit
patch boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flip transitively-discovered federation peers to Trusted instead of
Observer. Hints are already only ingested from peers we trust and only
peers we trust are re-exported via build_local_state, so the chain of
trust is already vetted end-to-end — making the user promote each
newcomer by hand was friction with no security win.
Backend:
- federation/sync.rs: merge_transitive_peers now inserts TrustLevel::Trusted
(doc comment updated to explain the transitive-trust rationale)
- update.rs: info! log at download start (version, components, total_bytes,
staging path), cancel (staging wiped?, marker cleared?), and apply (backup
path) so journalctl reveals where a stuck update actually is
Frontend:
- SystemUpdate What's New block gets a v1.7.18-alpha entry
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Federation join flow now notifies the inviter with the joiner's name and
immediately bumps state so the Federation UI reloads without a manual
Sync click. Accepting an invite that points back at the local node is
rejected up front (DID/pubkey/onion match). After a peer joins, we spawn
a transitive sync that pulls the new peer's federated peer hints so all
nodes in the federation learn about each other as Observer entries.
Federation.vue polls every 5s while mounted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
download_update
Each component download is now resumable via HTTP Range requests
(Range: bytes=N-) and retried up to 6 times with exponential
backoff (5/15/30/60/120/180s). On a dropped connection the next
attempt picks up at the last written byte offset instead of
restarting at zero. Streams via reqwest::Response::chunk() to the
staging file so a 160 MB frontend tarball doesn't sit in RAM. SHA
is verified over the complete file at the end of each component;
mismatch nukes the staged file and restarts from scratch.
Real download progress counters
New AtomicU64 globals DOWNLOAD_BYTES/DOWNLOAD_TOTAL are updated
from the chunk loop. update.status exposes them as
download_progress.{bytes_downloaded, total_bytes, active}. The
SystemUpdate.vue progress bar now polls update.status every
second instead of incrementing a fake random counter — and
crucially, if the user navigates away and back, the component
picks up the in-progress download from the backend atomics
immediately.
Update-check retries
handle_update_check now retries the manifest fetch up to 3 times
with a 5s gap if the first try hits a transport error, so a
momentary gitea hiccup doesn't make a node report "up to date"
when there actually is a new release. Tight 10s connect timeout
per attempt keeps the total bounded.
Artefacts:
archipelago 1070c87f…c081c162b 40584792
archipelago-frontend-1.7.15-alpha.tar.gz 8e630eba…63fd43f 162078068
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Install UX
SystemUpdate.vue now shows a full-screen overlay after apply: the
BitcoinFaceAscii logo, a target-version label, an indeterminate
progress stripe (solid orange; solid green on ready), and an
elapsed-time readout. Polls /health every 1.5s and auto-reloads
once the backend reports the new version. 3-min stall → "Reload
now" button. Download UI also shows a spinner + "Finishing
download — verifying checksum…" while the fake bar sits at 95%.
FIPS reconnect — for real this time
New fips.reconnect RPC does stop → start → wait 20s → re-poll →
classify. Classification buckets: connected / daemon_down /
no_seed_key / no_outbound_udp_or_anchor_down / peers_but_no_anchor,
each with a plain-language hint surfaced verbatim by the Reconnect
button. The real reason nodes like .198/.253 couldn't reach the
anchor: identity::write_fips_key_from_seed was writing fips_key.pub
as a bech32 npub TEXT file, but upstream fips expects 32 raw
bytes. The daemon silently authenticated with garbage. Fix:
PublicKey::to_bytes() → raw 32 bytes, and new
fips::config::normalize_pub_file migrates legacy files by decoding
the npub and rewriting in place. fips.reconnect also re-installs
the config + healed keys to /etc/fips before restarting.
AIUI preservation + restore
apply_update was wiping /opt/archipelago/web-ui/aiui because the
Vue build doesn't include it — every OTA lost the Claude sidebar.
The preserve block now copies aiui/ + archipelago-companion.apk
from the old web-ui into the staging dir before the swap, and
prefers new-tar versions if present. To restore it on the three
nodes that already lost it (.116/.198/.253), this release bundles
the 85 MB aiui build into the frontend tarball. Frontend component
size is now ~155 MB.
Download / install timeouts
Backend download client timeout 1800s → 3600s (1 h). Larger
tarball + slow gitea raw throughput put us above the old cap.
Frontend update.download rpc timeout 30 min → 65 min to match.
package.install rpc timeout 15 min → 45 min — IndeedHub pulls
6 images and was timing out mid-install.
UI nit
"Rollback to Previous" → "Rollback Available".
App-catalog proxy already landed in v1.7.13.
Artefacts:
archipelago 725e18e6…3c525e6 40462288
archipelago-frontend-1.7.14-alpha.tar.gz c35284be…ff2c16 162077052 (+aiui)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Discover / Marketplace page fetched the app catalog directly from
git.tx1138.com/lfg2025/app-catalog/raw/.../catalog.json in the
browser. Two blockers hit the fleet simultaneously: (1) tx1138's
Gitea doesn't emit Access-Control-Allow-Origin so the HTTPS fetch
got CORS-blocked; (2) the HTTP IP-port fallback
(http://23.182.128.160:3000/...) falls outside the node's
`connect-src` CSP. Users saw the hardcoded fallback instead of the
live catalog.
Backend: new authenticated GET /api/app-catalog handler uses reqwest
to pull catalog.json server-side (15s timeout) and returns it with
application/json + 1h Cache-Control. Tries the HTTPS URL first,
HTTP IP-port second.
Frontend: curatedApps.ts now calls /api/app-catalog (same-origin,
no CORS/CSP) with credentials included so the session cookie
authenticates the proxy. Baked /catalog.json stays as the last
resort.
Artefacts:
archipelago 0aaf7262…b979f22c 40371192
archipelago-frontend-1.7.13-alpha.tar.gz 27505811…efc6f4142 76982505
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version-only bump. Sits above v1.7.11-alpha which user has verified
runs the full Install Update pipeline end-to-end (check → download
→ install → auto-restart). Freshly-installed nodes from the 1.7.11
ISO will see 1.7.12 as their first OTA target.
Frontend tarball byte-identical to v1.7.11 (same sha).
Artefacts:
archipelago 247f65c2…54f40df9 40385472
archipelago-frontend-1.7.12-alpha.tar.gz 0644a436…54f58 76983846 (reused)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version-only bump. Frontend tarball byte-identical to v1.7.10. First
OTA-testable release where the running backend (v1.7.10) has the
host_sudo/systemd-run apply fix — clicking Install Update should
walk through check → download → install → auto-restart with no
manual intervention.
Artefacts:
archipelago cf003f62…65465f 40378752
archipelago-frontend-1.7.11-alpha.tar.gz 0644a436…54f58 76983846 (reused)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
THE apply fix
archipelago.service uses ProtectSystem=strict, so /opt and /usr are
read-only inside the service's mount namespace. sudo inherits that
namespace — every sudo mkdir/mv/chown from apply_update was hitting
EROFS even as root. Every prior "Failed to apply update" was a
symptom of this. New `host_sudo()` helper wraps every filesystem
call in `sudo systemd-run --wait --collect --pipe -- <cmd>`, which
spawns a transient unit with systemd's default (no ProtectSystem)
protections — the command runs in the host namespace and can touch
/opt/archipelago + /usr/local/bin normally.
FIPS cascade (#2)
Home.vue and Server.vue both carry a FIPS row that previously only
looked at {installed, service_active, key_present}. Now they also
read anchor_connected + authenticated_peer_count and mirror the
full FIPS card: green "Active · N peers" when healthy, orange "No
anchor" when the DHT bootstrap has failed.
Profile paste URL fallback (#4)
Web5Identities.vue list + editor previously had `@error="display:none"`
on the <img>, which hid the tag without re-rendering the fallback —
a broken pasted URL showed up blank. Replaced with reactive
pictureLoadFailed / listPictureFailed flags plus a watcher that
resets on URL change. Broken URL now falls back to the initial (or
identicon for seed-derived identities).
Small-upload data URL (#3)
Uploaded profile pictures ≤ 64 KB are now inlined as
`data:image/png;base64,...` into profile.picture on the client
before calling update-profile. That kind-0 event is fetchable by
any Nostr client — no Tor needed. Larger uploads fall back to the
onion-rooted public_url with a hint telling the user to paste a
public https:// URL for broader visibility.
Deferred: #1 FIPS Reconnect "actually fixes" — the current Reconnect
calls fips.restart which clears the daemon state, but when the
anchor is truly unreachable (UDP 8668 blocked by network/ISP), no
amount of restart can help. A richer diagnostic is out of scope for
this bundle.
Artefacts:
archipelago 4a77c704…82aa6f8 40379696
archipelago-frontend-1.7.10-alpha.tar.gz 0644a436…54f58 76983846
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version-only bump. First release where .116/.198/.253 (running v1.7.8
with the mv-based apply) should walk through Check → Download →
Install → auto-restart cleanly via UI, no sideload intervention.
Artefacts:
archipelago 1ec7383d…301629 40378536
archipelago-frontend-1.7.9-alpha.tar.gz 4fb79664…0172e9 76984615 (reused)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apply_update's binary swap called `sudo install -m 0755 src
/usr/local/bin/archipelago`. install opens the destination for write
with O_TRUNC; the kernel returns ETXTBSY (exit 1) when the path is a
currently-running executable, which it always is during apply because
apply_update is called by the archipelago RPC handler — running as
archipelago itself. Every previous "Failed to apply update" was this
one root cause; the manual sideload path only worked because we
stopped the service first.
rename() doesn't modify the file it replaces — it repoints the path
at a new inode while the old inode stays alive for any process that
has it mapped. `mv` uses rename(). Switched to `sudo mv` (with prior
chmod+chown on the staging file) so the swap is atomic and tolerant
of the running binary.
Frontend tarball byte-identical to v1.7.7-alpha; only the binary
version string changes.
Artefacts:
archipelago 2753daec…48094d 40377648
archipelago-frontend-1.7.8-alpha.tar.gz 4fb79664…0172e9 76984615 (reused)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure version bump. No code changes. First release shipped with the
reinforced apply_update (timestamped staging + all-mv) and frontend
with 95% progress cap — this OTA should walk through cleanly from
.116/.198/.253 without any sideload intervention.
Artefacts:
archipelago e3f1740d…006025 40373392
archipelago-frontend-1.7.7-alpha.tar.gz 4fb79664…0172e9 76984615 (reused)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apply_update frontend swap
Transient EROFS on .198 (filesystem hiccup — root FS mounts with
errors=remount-ro so a fleeting glitch can bounce /opt to RO for a
moment) caught the pre-cleanup `rm -rf web-ui.new web-ui.bak` mid-
stride and aborted the apply. Rewrote the swap to use a timestamped
staging dir (web-ui.new.<ms>) and a timestamped old-copy path so
nothing needs to be rm'd before the extract. After the new tree is
mv'd into place, the previous rollback copy is rotated aside with a
.<ms> suffix (best-effort) and this apply's old copy becomes the new
web-ui.bak. If the final mv fails, the staged old is restored so
nginx keeps serving.
handle_update_check manifest override
handle_update_check takes the git path whenever ~/archy/.git exists.
On the dev box (.116) that meant the Pull & Rebuild button was
always the only option even though the manifest-path OTA was
already wired via ARCHIPELAGO_UPDATE_URL. Now: if that env var is
set, we skip the git detection entirely and use the manifest path.
The regular fleet (no env var, no repo) hits the manifest branch
naturally; beta dev nodes (repo + no env var) still get Pull &
Rebuild; dev nodes with the env var explicitly set can finally test
the manifest OTA end-to-end.
Artefacts:
archipelago 356e78cc…91a6dd 40372288
archipelago-frontend-1.7.6-alpha.tar.gz 4fb79664…0172e9 76984615 (reused)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trivial version-only bump. No code changes; binary differs only in its
embedded CARGO_PKG_VERSION string. Frontend tarball is byte-identical
to v1.7.4-alpha's (same sha), copied under the new filename to satisfy
the manifest component naming.
This exists so the fleet nodes (.116/.198/.253), all now running
v1.7.4-alpha with the fixed apply_update tar flow, can exercise the
full OTA pipeline from the UI: Check → Download (30-min timeout) →
Install (sudo install binary + sudo tar to web-ui.new + atomic swap) →
auto-restart (systemctl --no-block) → sidebar updates → state sync.
Artefacts:
archipelago 7422a695…a1a2a6 40362432
archipelago-frontend-1.7.5-alpha.tar.gz 4fb79664…0172e9 76984615 (reused)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apply_update was extracting the frontend tarball with
`tar -xzf -C /opt/archipelago`, but the tar contents are the *inside*
of web-ui/ (root entries are ./test-aiui.html, ./assets/, etc.). So
the files landed directly in /opt/archipelago instead of under web-ui/,
and tar bailed on nginx-owned paths mid-extraction. First end-to-end
OTA test (.198) found it: "tar: ./assets/SystemUpdate-…js: Cannot
open: No such file or directory".
Now extracts into web-ui.new, chowns, then atomically swaps: move
existing web-ui → web-ui.bak, then web-ui.new → web-ui. Same pattern
as the manual sideload that's been working.
Frontend: SystemUpdate.vue fake download progress was capped at "<90"
with a Math.random()*15 increment — the last tick could push to
~104.99%. Capped at 95% with a smaller step so it stops at 95 and the
real RPC completion jumps it to 100.
Artefacts:
archipelago a14ad7e4…2a2be3 40361984
archipelago-frontend-1.7.4-alpha.tar.gz 4fb79664…0172e9 76984615
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sidebar version
detect_build_version() no longer reads /opt/archipelago/build-info.txt
first. That file was written by the ISO installer at flash time and
never rewritten by OTA or sideload, so after any binary swap the
sidebar kept advertising whatever the ISO shipped with. Now just
returns env!("CARGO_PKG_VERSION") unconditionally — always matches the
running binary.
FIPS card
The two-column grid in FipsNetworkCard.vue placed version/npub boxes
side-by-side on mobile but the anchor-status panel forced col-span-2,
creating an unbalanced empty column at every desktop width. Anchor
status moves to its own full-width row below the grid. When the
anchor is not reached, a "Reconnect" button appears next to the
status line; it calls fips.restart (45s timeout), waits 5s for the
daemon to come back, then reloads fips.status. Surfaces whether the
restart actually recovered the anchor in a status flash.
Profile picture render
Uploaded profile pictures are stored with an onion-rooted URL so
external Nostr clients can fetch them. The local browser isn't
Tor-routed though, so the <img src> silently 404'd and the UI fell
back to showing initials. Added a displayableUrl() helper on
Web5Identities.vue that rewrites http://<onion>/blob/<cid>[?...] to
same-origin /blob/<cid> for rendering, while the stored URL keeps
its onion prefix so publishing to Nostr still works for external
viewers. Pass-through for data: URLs and already-relative paths.
Identity row title
The identity list header now renders profile.display_name (when set)
and keeps identity.name as a muted parenthetical. Before, only the
internal name was shown and a user who'd customised their Nostr
display_name saw a mismatch between their own UI and what peers
rendered.
Artefacts:
archipelago 99184b95…22dc1b 40350664
archipelago-frontend-1.7.3-alpha.tar.gz 7b933cf4…74a8bc 76987031
Changelog layman-style per the saved feedback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three user-visible fixes shipped together.
1. update.apply permission-denied
apply_update() was doing fs::copy into /usr/local/bin/archipelago and
tar xzf into /opt/archipelago as the archipelago user — both root-owned.
The backup step succeeded (it wrote to data_dir) but the swap failed
with a silent permission denied, wrapped as "Failed to apply archipelago".
Now uses `sudo install -m 0755` for the binary and `sudo tar -xzf` for
the frontend, plus a post-apply `sudo systemctl --no-block restart
archipelago` scheduled 2s after the RPC reply so the UI sees success.
2. Apply → Install label
en/es locale strings: applyUpdate / applyTitle / applyNow changed from
"Apply" to "Install". Matches the user's mental model and distinguishes
the user-facing verb from the internal apply_update() function.
3. Identity avatar backfill
Identities created before df83163f had profile=None on disk and so
rendered as initials. load_record() now synthesizes an IdentityProfile
with a default picture (identicon for regular identities, the hex node
SVG for derivation_index=0) when profile is missing. The synthetic
profile lives only in the returned record; the file stays untouched so
a later explicit Save persists whatever the user actually chose.
Artefacts:
archipelago 70e5444e…67c589 40381960
archipelago-frontend-1.7.2-alpha.tar.gz 806b027b…358a824 76983699
Changelog rewritten layman-style per saved feedback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trivial bump on top of df83163f. No code changes — this exists purely so
the fleet nodes now sitting on 1.7.0-alpha have a real target to exercise
the OTA pipeline against: check → download → apply → restart → state
reconciliation. The binary content differs only in the embedded
CARGO_PKG_VERSION string.
Frontend tarball reused from v1.7.0-alpha (same bytes, copied to a new
filename to match the manifest component name convention).
Artefacts:
archipelago 7f7981bd…56eef0 40391760
archipelago-frontend-1.7.1-alpha.tar.gz (dup of 1.7.0) dc3b63af…e9a8370 76984288
Manifest changelog is a single plain-language sentence explaining that
this is the test release — per the saved feedback about keeping
fleet-facing strings readable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 1fb71b4b on the same v1.7.0-alpha line.
Identity avatars
• New module `avatar.rs` generates two deterministic SVG styles keyed
off the pubkey: a 5×5 mirrored identicon for sub-identities and a
hexagonal-network motif for the master (seed index 0) identity.
Both returned as base64 data URLs, so a fresh identity has a
recognisable picture before the user uploads anything.
• `IdentityManager::create()` and `create_from_seed()` populate
`profile.picture` on creation. Index 0 gets the node SVG; all
other seed-derived + ad-hoc identities get the identicon.
Blob store — public flag for profile assets
• `BlobMeta.public` (default false) added; `BlobStore::put()` takes
a `public: bool`. Missing in legacy meta files = false.
• `POST /api/blob` now stores uploads with public=true and returns
`public_url` alongside `self_test_url`. public_url is
`http://<node-onion>/blob/<cid>` (no cap) if Tor has published the
archipelago hidden service, else falls back to the local path.
• `GET /blob/<cid>` bypasses the HMAC capability check when the
requested blob is flagged public — external Nostr clients fetching
a kind-0 `picture` URL can't hold a cap.
• Mesh callers (content_ref attachments, dispatch rehydration) pin
public=false explicitly so nothing leaks out of the mesh path.
Profile editor UX
• Collapsed Save + Save & Publish into one button — the Save action
now persists locally AND publishes the kind-0 metadata event in
one step. Uploads store `public_url` into `profile.picture` /
`profile.banner` so the published URL is reachable by external
clients.
Update client — the 15-second cliff
• Frontend `rpcClient.call` for `update.download` now has an
explicit 30-minute timeout (was falling back to the default 15 s).
`update.apply` gets 5 min, `update.git-apply` gets 15 min. Matches
what the backend is actually willing to wait for.
• Backend `load_state()` reconciles `state.current_version` with
`CARGO_PKG_VERSION` on every start. Sideloaded or reflashed nodes
were stuck advertising the old version even with a new binary in
place, which kept re-offering the same release as an update.
Manifest changelog rewritten for fleet readers per the saved feedback
(no function names, no file paths). Artefacts refreshed:
binary 12f838c5…5ba82d 40381864
frontend dc3b63af…e9a8370 76984288
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 56d4875b, same v1.7.0-alpha shipping band.
Backend download timeout bumped from 300s to 1800s (update.rs) with an
explicit 30s connect timeout. git.tx1138.com raw-file throughput can sit
around 70–80 KB/s, which meant OTA downloads were timing out at ~55%
through the 40 MB binary even though the SHA would have matched on a
full pull. 30 min gives ample headroom for the worst LAN-to-VPS link we
actually hit.
Frontend: SystemUpdate.vue now formats downloadPercent with toFixed(2)
via a new computed, so the progress card shows "45.23%" instead of
"45.270894%". Cosmetic only; the underlying ref still tracks raw floats.
Manifest changelog rewritten in user-facing language per the saved
feedback — no file paths, function names, or "root cause" phrasing.
Artifacts refreshed:
binary d85a71c5…982f4 40360936
frontend 8adcdacf…e687f6 76986852
ISO at image-recipe/results/archipelago-installer-unbundled-x86_64.iso
(Apr 20 09:00) carries both fixes for fresh installs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 8b7cb002 (no version bump — same v1.7.0-alpha manifest):
* WireGuard peer persistence. Kernel peer state is ephemeral; the add-peer
RPC wrote each peer to data_dir/nostr-vpn/peers/*.json but nothing
re-pushed them on reboot. Result on .198: wg0 came up listening with zero
peers after last night's reboot. Added vpn::restore_wg_peers() — reads
the peers dir, waits up to 30s for wg0 to exist, then replays each via
`archipelago-wg add-peer`. Spawned from main.rs alongside the other
startup tasks.
* Reconcile + filebrowser drift. scripts/container-specs.sh load_spec_
filebrowser now declares SPEC_NETWORK="archy-net" (to match what
first-boot-containers.sh creates) and pins the filebrowser-data volume
+ wget-style healthcheck so the reconciler stops reporting network
drift. Without this, reconcile would kill the healthy first-boot
filebrowser container and recreate it on bridge, breaking the archy-net
DNS name the backend proxies to.
Manifest binary sha/size refreshed:
6c178a76…3582cc, 40361912 bytes.
Rebuilt ISO at image-recipe/results/archipelago-installer-unbundled-x86_64.iso
(Apr 20 07:10) carries both fixes baked in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes bundled into the OTA:
1. update.download hard-fail on git-path nodes. handle_update_check's git
branch reported update_available=true + update_method="git" but never
populated state.available_update, so update.download returned "No update
available to download" even though the UI showed one. SystemUpdate.vue
now routes update_method=="git" through update.git-apply (pull+rebuild+
restart via self-update.sh); manifest-path nodes keep the download→apply
flow. i18n strings + confirm modal added for the git path.
2. Reconciler creating containers behind the user's back. On fresh
unbundled installs (.198, .253) archy-mempool-db and archy-btcpay-db
materialised ~10 min after first boot because reconcile-containers.sh
walked container-specs.sh's canonical tier list and created any
"missing" container. reset_spec() now defaults SPEC_OPTIONAL="true",
so reconcile is strictly a repair tool — baseline comes from
first-boot-containers.sh (filebrowser on unbundled), everything else
from the install RPC.
Also forces OTA trigger for nodes on 1.6.0-alpha that otherwise saw
"I'm at manifest.version, nothing to do" and skipped the refreshed 1.6
artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refreshes release binary to include 6b78bd69:
- FIPS auto-activates at onboarding end (spawn_post_onboarding_fips_activate
fires from handle_seed_generate/restore the moment fips_key lands on disk).
Previously the startup-time auto-activate ran once at boot and exited
before the user ever got to onboarding, so fresh installs still needed
a manual Activate click.
- Kiosk health-poll window 60s -> 5 min (TimeoutStartSec=360) so slower
hardware like .198 doesn't race Chromium against a not-yet-ready
backend and white-screen on first boot.
Frontend tarball unchanged — no frontend diff since 78e7c59e.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. FIPS auto-activate at server startup only fires if fips_key already
exists on disk, which on a fresh install is never true until AFTER
onboarding. By the time the user completes seed-generate/restore,
archipelago has been running for minutes and the startup task has
long since exited. User still had to hit Activate.
Fix: call spawn_post_onboarding_fips_activate() from the tail of
handle_seed_generate and handle_seed_restore — the moment the
fips_key materialises, a detached task runs `fips::config::install`
+ `archipelago-fips.service activate`. Logged only, never blocks
the onboarding RPC.
2. Kiosk health-poll window was 30 × 2s (configs/ copy was 60 × 2s
but unused — the heredoc in build-auto-installer-iso.sh is what
actually lands on disk). On .198's slower hardware archipelago
/health wasn't ready within 60s, so Chromium launched against a
not-yet-running backend → blank window until manual reboot. Bumped
to 150 × 2s (5 min) + TimeoutStartSec=360. .253 was already well
within the window; this protects the slower box too. Standalone
configs/archipelago-kiosk.service updated in lockstep so the two
copies don't drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supersedes the earlier v1.6.0-alpha artifacts from today (which were
identical to v1.5.0-alpha and only existed for the update-flow smoke
test). This drop actually changes behaviour:
- archipelago-fips auto-activates on startup when fips_key exists; no
Activate button needed.
- fips_key on-disk format migrated to bech32 nsec; legacy raw-byte
files from v1.5.0-alpha self-heal when this version reads them.
- fips.yaml schema matches upstream jmcorgan/fips 0.3+.
- VPN status row shows "Not configured" instead of "Starting…" when
wg0 isn't up — no VPN peer added yet is not a failure state.
New SHA256s + sizes in manifest.json. Fleet nodes .116/.228/.253 will
notice within 30 min (periodic update-check). Also lets .198 self-heal
its crashlooping archipelago-fips when it picks up the update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Problems addressed (all observed on .198):
* fips_key was written as raw 32 bytes; upstream fips daemon reads it
with read_to_string() and bailed with "stream did not contain valid
UTF-8", crashlooping indefinitely.
* Activate button racy: user had to hit it, and it would keep failing
silently because the daemon couldn't parse its own config.
* FIPS schema drift (already fixed in 7d8a5864) put the config write
path behind the same broken "Activate" flow, so the fix alone
didn't help existing nodes.
* Journal was on tmpfs — every reboot wiped install/onboarding history,
making post-hoc debugging impossible.
Changes:
* identity.rs: write fips_key as bech32 nsec + newline. load_fips_keys
now auto-migrates legacy 32-byte files to bech32 the first time it
reads them, so OTA updates from v1.5.0-alpha self-heal without user
action.
* server.rs: post-onboarding auto-activate task runs on every
archipelago startup. If fips_key exists it ensures /etc/fips/fips.yaml
is schema-current and starts archipelago-fips.service. Pre-onboarding
nodes stay quiet (guarded on fips_key_exists).
* ISO build: un-mask archipelago-fips + archipelago-wg + wg-address —
all use ConditionPathExists on their key files, so systemd silently
skips them pre-onboarding (no MOTD [FAILED]). Only nostr-vpn stays
masked (legacy service, superseded by upstream fips).
* Journald made persistent via /var/log/journal + 500M cap, so
install and first-boot logs survive reboots for diagnosis.
After this, a fresh install + onboarding should bring FIPS up automatically
with no user interaction. The UI "Activate" button can stay as an escape
hatch (the RPC is still there) but is no longer on the critical path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No functional changes from v1.5.0-alpha — this release exists only to
validate the in-app update pipeline end-to-end (manifest check → staged
download → apply → restart → version bump in UI sidebar).
Dropping just the manifest + artifacts; no manual deploy to the fleet.
.116/.228/.253 should notice within 30 min (periodic update-check
interval) and surface the update in the dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anchored regex was too strict — `strings` concatenates adjacent printable
bytes so the version never sits on its own line. The 1.5.0-alpha binary
DOES contain the version but as part of `1.5.0-alpharpcNot Found`. Fixed
by switching to `grep -qF $VERSION`: substring match is safe because the
version string is specific enough that accidental collisions are
vanishingly unlikely.
Caught mid-build today: check rejected the correct local binary, fell
through to container source-build — ISO still produced correctly but
wasted ~10 min on an unnecessary rebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. FIPS daemon config schema drifted: upstream jmcorgan/fips now takes
`node.identity.persistent: true` (keys read from config-dir/fips.key)
and `transports.udp.bind_addr: "0.0.0.0:PORT"` instead of
`identity.key_file/pub_file` + `transports.udp.enabled/port`. The
`tor:` transport was dropped entirely; archipelago handles Tor
fallback itself. fips.yaml generated by archipelago::fips::config
now matches the upstream schema, and archipelago-fips.service stops
crashlooping on Activate. Observed on .198: 52 restarts with
"data did not match any variant of untagged enum TransportInstances
at line 7 column 3".
2. ISO backend-binary capture didn't verify that the captured binary
matched the checked-out Cargo.toml version. Today's 14:40 ISO
shipped a stale 1.4.0 binary because `core/target/release/archipelago`
pre-dated the 1.5.0-alpha bump — the build grabbed it via the
first-priority "local release build" path without looking at it.
All four capture sources now go through verify_backend_version()
which greps the binary for the expected version string; mismatches
are skipped so the build falls through to the source-build path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On this host (and potentially others with a particular podman/overlay
state), passing the multi-hundred-line stage-2 script via
`debian:trixie bash -c '...'` caused debootstrap to fail at
"Extracting apt... tar failed" on the very first package — no matter
what patch, storage cleanup, or env-reset we tried.
Running the exact same script body via a bind-mounted file
(`bash /installer-env.sh`) succeeds. So: write the body to a temp
file in WORK_DIR, bind-mount it read-only, and have the container
bash execute it from the file. Same behavior, different invocation,
works.
Was blocking every ISO rebuild since ~10:57 local. First successful
build since: 14:40, sha256 41fad2ff…, 2.3GB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Debian Trixie apt 3.0.3's data.tar has duplicate entries for the same
path (regular file + symlink at e.g. libapt-private.so.0.0), and tar
bails on the second entry with "Cannot create symlink: File exists",
failing debootstrap on the very first package. Patch debootstrap's
tar invocation to use --skip-old-files so the duplicate is ignored.
Was blocking every unbundled ISO rebuild since the Trixie apt bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous build's STEP 47/50 log showed:
RUN for svc in nostr-vpn archipelago-wg archipelago-wg-address; do
rm -f /etc/systemd/system/.service
ln -sf /dev/null /etc/systemd/system/.service
done
The Dockerfile is generated via <<DOCKERFILE heredoc in the build
script, so unescaped $svc resolved in the outer bash BEFORE Docker
ever saw it, leaving nostr-vpn/wg masks as a hidden `.service` file
with no effect. nostr-vpn still tries to start on boot → [FAILED].
Fixed with \$svc so the literal lands in the Dockerfile for Docker's
shell to expand per iteration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fresh install of .198 reported "FIPS has an npub but says inactive".
The debian package writes /etc/fips/fips.pub during install (whence
the npub) but leaves the upstream fips.service disabled. Result:
FipsStatus.service_active = false, dashboard shows "inactive" until
the user hits Activate. Explicit `systemctl enable fips.service`
in the Dockerfile so first boot brings the daemon up immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. nostr-vpn still failing despite last mask attempt — confirmed in
the 6th ISO's rootfs.tar: the .service file was present but
not in multi-user.target.wants. Previous `systemctl mask` silently
no-oped because the real file was already there. Fixed properly
with explicit `rm -f` + `ln -sf /dev/null` for nostr-vpn,
archipelago-wg, and archipelago-wg-address — same /dev/null
symlink state that `mask` would produce on a clean install.
2. Kiosk didn't come up on first boot, only on reboot. Extended the
ExecStartPre health-poll from 30s → 120s (unbundled ISO takes
longer to settle on first boot: archipelago initializes state,
pulls FileBrowser, frontend settles), raised TimeoutStartSec to
180s, and added After=systemd-user-sessions.service +
After=network-online.target so X / Chromium aren't racing.
3. /init: line 29: can't create /root/etc/network/interfaces error
on installer boot — debootstrap --variant=minbase omits ifupdown
so the target has no /etc/network/ directory, and live-boot's
init tries to seed it. Non-fatal but noisy. Added ifupdown +
isc-dhcp-client to the debootstrap --include list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5th ISO attempt died in rustables's build.rs (which uses bindgen to
wrap libnftnl) with "couldn't find any valid shared libraries
matching: libclang". bindgen requires libclang.so at build time
to parse C headers. rustables also needs libnftnl-dev + libmnl-dev
for the actual wrappers.
Added to the fips-builder stage apt install line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third time's the charm. The upstream fips Cargo.toml puts fips-gateway
behind features.gateway = ["dep:rustables"], so the previous two
attempts (--bins, --workspace --bins) never produced the binary —
only the default feature set was compiled. cargo deb --no-build then
panics looking for the missing binary.
Inspected /tmp/fips-investigate (fresh clone of upstream main on
2026-04-19) to confirm — the feature flag is the gate, not a
workspace layout issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plain `cargo build --release --bins` only built the root crate's
binary targets. fips-gateway is a workspace member, so we need
--workspace to pull every member's bins. Without it cargo deb
--no-build panics looking for target/release/fips-gateway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consumes the new authenticated_peer_count + anchor_connected fields
from fips.status. Shows a cyan dot + "connected" when fips.v0l.io is
in the identity cache (DHT routing to unknown npubs will work), or
an orange "not reached" with a one-line explainer that federation
and messaging will fall back to Tor until the anchor reconnects.
Peer count appears on the same row so users see "3 peers" when the
fleet-pair script has been run, or "0 peers" on a fresh install
still waiting for the anchor handshake.
Block only renders when service_active — pre-onboarding the FIPS
package is masked so there's nothing meaningful to report.
Covers the UI half of task #20. Multi-anchor defaulting is still
open (need real anchor addresses beyond fips.v0l.io).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new fields on the /rpc fips.status payload:
- authenticated_peer_count: how many FIPS peers the daemon has an
authenticated session to right now. 0 means isolated / not on
the mesh; >0 means traffic to any known npub can DHT-route.
- anchor_connected: true when the public anchor (fips.v0l.io,
npub1zv58cn7…) is present in the daemon's identity cache. The
anchor bootstraps DHT routing for general-case deployments, so
this is the best single-value indicator the UI can show for
"will federation traffic over FIPS work between previously-
unknown peers?"
Implementation: fips::service::peer_connectivity_summary shells
out to `sudo -n fipsctl show peers` + `... show identity-cache`
(archipelago user already has NOPASSWD:ALL per the ISO sudoers
and live fleet nodes, confirmed). Failure returns (0, false) so
the UI degrades to "unknown" state without crashing.
Only queried when service_active — pre-onboarding / daemon-down
nodes skip the fipsctl call entirely.
UI side (FipsNetworkCard) consumes the full status JSON, so the
two new fields are available via existing prop plumbing; visual
treatment can come later.
Also fixes ISO build (commit 3e04456c wasn't sufficient): the
Dockerfile needs `cargo build --release --bins` — upstream FIPS
added a `fips-gateway` binary target, and plain `cargo build
--release` only builds the default bin list, which caused
`cargo deb --no-build` to fail hunting for the missing binary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>