400 Commits

Author SHA1 Message Date
Dorian
4d8a9e66e3 release(v1.7.20-alpha): stop auto-apply scheduler killing the service
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>
2026-04-21 04:33:11 -04:00
Dorian
9fc9696dbd release(v1.7.19-alpha): kill stale available_update + numeric version compare
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>
2026-04-21 04:04:20 -04:00
Dorian
062e1fada2 release(v1.7.18-alpha): transitive peers default Trusted + update-flow logs
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>
2026-04-20 20:20:36 -04:00
Dorian
4706dd16e7 release(v1.7.17-alpha): cancel download + stall detection
Add Cancel Download button + stall detection so a wedged download can
be recovered instead of leaving the UI stuck on a frozen progress bar.

Backend:
- update.rs: DOWNLOAD_CANCEL AtomicBool + DOWNLOAD_PROGRESS_AT AtomicU64
- download loop checks cancel between chunks and during retry backoff
  (500ms slices instead of one exponential sleep, so Cancel wakes fast)
- cancel_download() wipes staging + clears update_in_progress
- update.status exposes download_progress.stalled (30s no-progress)
- RPC: update.cancel-download + dispatcher entry

Frontend:
- SystemUpdate.vue: Cancel Download button, amber stall styling,
  stalled copy, cancel-download confirm branch in modal
- i18n keys (en + es) for cancel/stall flow
- v1.7.17-alpha What's New block in AccountInfoSection

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:10:34 -04:00
Dorian
3cbfcabedf release(v1.7.16-alpha): bidirectional + transitive federation, no self-peering
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>
2026-04-20 18:12:02 -04:00
Dorian
0fad7ee431 release(v1.7.15-alpha): bulletproof downloads — resume, retry, real progress
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>
2026-04-20 17:17:58 -04:00
Dorian
923c404678 release(v1.7.14-alpha): install overlay + FIPS real fix + AIUI restore
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>
2026-04-20 16:40:25 -04:00
Dorian
30a26f94f7 release(v1.7.13-alpha): proxy app catalog server-side (CORS + CSP fix)
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>
2026-04-20 15:43:45 -04:00
Dorian
b8ab06dd47 release(v1.7.10-alpha): apply namespace fix + FIPS cascade + profile polish
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>
2026-04-20 13:46:03 -04:00
Dorian
170f8ae787 release(v1.7.4-alpha): fix Install Update tar extraction + progress overshoot
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>
2026-04-20 12:02:14 -04:00
Dorian
3a479e5b09 release(v1.7.3-alpha): sidebar version sync + FIPS reconnect + profile pic render
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>
2026-04-20 11:44:59 -04:00
Dorian
0d5128a121 release(v1.7.2-alpha): fix Install Update + identity avatar backfill + label
Three user-visible fixes shipped together.

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

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

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

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

Changelog rewritten layman-style per saved feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:25:10 -04:00
Dorian
df83163f15 feat(identity,update): default avatars, public blobs, long-running downloads
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>
2026-04-20 10:03:38 -04:00
Dorian
1fb71b4b4e fix(update): 30-min download timeout + tidier progress number
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>
2026-04-20 09:03:24 -04:00
Dorian
8b7cb0029f release(v1.7.0-alpha): bump + fix git-method update + reconciler creates
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>
2026-04-20 06:22:29 -04:00
Dorian
78e7c59e78 release(v1.6.0-alpha): refresh with bulletproof FIPS + VPN label fix
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>
2026-04-19 17:13:58 -04:00
Dorian
5eb2d34dd7 release(v1.6.0-alpha): smoke-test release for system-update flow
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>
2026-04-19 16:12:28 -04:00
Dorian
bb4b7dd640 feat(web5): anchor connectivity badge on FipsNetworkCard
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>
2026-04-19 08:42:50 -04:00
Dorian
6167913133 feat(web5): avatar + banner upload on the profile editor
Previously the profile editor only accepted external URLs for
picture/banner — typing in a URL works, but anyone without their
own image host couldn't use an avatar at all. Now there's an
"Upload" button next to each field that pushes the selected file
to /api/blob and pastes the returned capability-signed local URL
(`/blob/<cid>?cap=…&exp=…&peer=…`) straight into the form field.

- Two new refs: avatarUploading / bannerUploading so each button
  shows "Uploading…" independently.
- uploadAsset(ev, 'picture' | 'banner') wraps the POST, validates
  HTTP 200 + presence of self_test_url, surfaces failures in the
  existing profileError banner.
- File input is re-cleared on completion so the user can pick the
  same file again without refreshing.
- Live preview in the <img> at the top of the editor updates
  immediately because profileForm[field] is reactive.

Image persists through Save & Publish via the existing
identity.update-profile + identity.publish-profile (both now
multi-relay). The image URL is still local-only — external nostr
clients won't resolve it until we integrate a public image host
(noted in task #29).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 05:01:57 -04:00
Dorian
996f6aa837 feat(web5): surface multi-relay publish result on profile save
Pairs with the backend change that broadcasts kind:0 to every
enabled relay. Web5Identities.vue's publishProfile() now reads the
richer response ({accepted, rejected, relays_attempted, published})
and shows one of three states:

- all relays accepted → "Published to all N relays (event_id…)"
- partial → "Published to X/N relays" plus a warning with the first
  relay's rejection reason
- zero → "Published to 0/N relays — check Manage Relays" (error)

User can now tell at a glance whether their profile actually made
it to the wider nostr network or only the local relay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:47:44 -04:00
Dorian
77206a8928 release: v1.5.0-alpha + version hygiene fixes
Versioning was drifting on three axes — fixed all of them:

1. Cargo.toml → 1.5.0-alpha (was 1.5.0). User wants `-alpha` suffix
   on every pre-stable release; this is the current state of main.
2. neode-ui/package.json was still 1.3.5 — brought in line.
3. /opt/archipelago/build-info.txt was stale on .198 (1.3.4) and
   .253 (1.3.5), absent on .116/.228. That file OVERRIDES the
   binary's CARGO_PKG_VERSION for the UI sidebar, which is why
   .198/.253 kept showing old versions even with fresh binaries.
   scripts/deploy-to-target.sh now writes build-info.txt on every
   deploy, reading the version straight from Cargo.toml — so the
   sidebar can never drift from the binary again.

Release artifacts + manifest:
- releases/v1.5.0-alpha/archipelago (40M, sha in manifest)
- releases/v1.5.0-alpha/archipelago-frontend-1.5.0-alpha.tar.gz (51M)
- releases/manifest.json bumped with full 7-line changelog covering
  FIPS-first routing, Settings toggle, transitive federation, cancel
  button, transport badges, peer listener, and the build-info fix.
- scripts/check-release-manifest.sh — new pre-publish guard. Refuses
  to pass if: Cargo.toml ≠ manifest version, changelog is empty
  (release notes are mandatory), or any component's sha256/size
  doesn't match the file on disk. Run locally or from CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 03:23:18 -04:00
Dorian
4c8c4ebc47 feat(federation): v1.5.0 bump + transport badge on each node card
Every federated node card now shows a colored badge indicating how
archipelago actually reached the peer on the most recent successful
call — FIPS / TOR / LAN / MESH — not a prediction based on available
addresses. The badge is hidden when we've never reached the peer.

Backend:
- Cargo.toml: 1.4.0 → 1.5.0 (visible in the sidebar health endpoint).
- FederatedNode gains last_transport + last_transport_at (serde
  default for back-compat with v1.4 nodes.json files).
- federation::storage::record_peer_transport(did, onion, transport)
  — writes both fields plus last_seen after each successful peer
  call. Matches by DID first, falls back to onion.
- federation::sync::sync_with_peer now calls record_peer_transport
  immediately after a successful PeerRequest return, so the badge
  on the sync'ing peer's card reflects the transport the call
  actually rode (fips vs tor).

Frontend:
- types.ts FederatedNode gains last_transport / last_transport_at
  (union-typed to the four known kinds).
- NodeList.vue: new transportBadge(node) returns {label, cls, title}
  tuned per transport. Hidden when last_transport is absent so we
  never lie. Tooltip shows "Last reached via <x> · <time ago>" so
  stale data is self-evident. Removed the predictive icon from the
  transport store — badge is now 100% ground-truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 02:51:26 -04:00
Dorian
95f52572fc feat(federation): cancel button for outbound pending peer requests
Previously the Pending Peer Requests panel only had Approve/Reject for
inbound rows; outbound rows in the 'sent' state had no action and
would sit there until the target explicitly approved or rejected. Now
you can Cancel an outbound request — the local row is dropped and a
PeerCancel nostr DM is sent so the target's inbound row also
disappears.

Backend:
- HandshakeMessage::PeerCancel {reason: Option<String>} variant.
- nostr_handshake::send_peer_cancel() mirrors send_peer_reject.
- handshake.poll handler dispatches inbound PeerCancel: finds the
  matching inbound pending row (same from_nostr_pubkey, state=Pending)
  and deletes it. Reply shape gains `cancelled_inbound: [id]`.
- federation::pending::delete() — hard-remove (set_state only
  transitions; we don't want 'Cancelled' ghosts in the audit trail).
- federation.cancel-request RPC: outbound+Sent only, default
  notify=true (cancelling silently is a footgun), best-effort DM
  (relay failure doesn't block local deletion). Wired in dispatcher.

Frontend:
- PendingRequestsPanel.vue: Cancel button appears only on
  outbound+sent rows. Emits 'cancel' event with request id.
- Federation.vue: cancelPending(id) handler calls
  rpcClient.federationCancelRequest and reloads the list.
- rpcClient.federationCancelRequest(id, reason?, notify=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 02:28:16 -04:00
Dorian
a658e924e1 fix(ui,ops): TransportPrefsCard import path + fleet unpair script
- TransportPrefsCard.vue: import from '@/api/rpc-client' (not
  '@/api/rpc') so vue-tsc resolves the module during build.
- scripts/fleet-fips-unpair.sh: companion to the fleet-pair script —
  rewrites each node's fips.yaml to anchor-only (fips.v0l.io) so we
  can prove the general-case deployment works without the LAN
  fast-path. Prints per-node peer counts + DHT AAAA resolution for
  every cross-node pair after the change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 02:08:32 -04:00
Dorian
683553dfde feat(settings): per-service FIPS/Tor transport preference
Adds a user-configurable toggle for how each peer-to-peer service
reaches federated peers. Three options per service:

- Auto (default) — FIPS preferred, Tor fallback (current behavior).
- FIPS only — fail rather than fall through to Tor.
- Tor only — explicit opt-in to onion anonymity for that service.

Services covered (matching the UI rows):
- Federation — state sync, invites, peer notifications
- Peers — address/DID rotation broadcasts
- Peer Files — content catalog download/browse/preview
- Messaging — archipelago channel + mesh bridge
- Mesh File Sharing — content_ref blob fetches

Implementation:
- settings::transport — persisted struct + process-wide OnceLock handle
  (so deep call sites don't need data_dir threaded through signatures).
  On-disk file: <data_dir>/settings/transport_preferences.json; missing
  or corrupt → defaults (Auto everywhere).
- settings::transport::init() called from main.rs after config load.
- fips::dial::PeerRequest gains a .service(kind) builder; send_* checks
  the preference before choosing a transport. FIPS-only fails loudly
  when FIPS is unavailable (so users who pick it know when something
  falls back).
- Every FIPS-first migration site tags its PeerRequest with the
  matching PeerService so the toggle actually applies.
- transport.preferences + transport.set-preference RPCs added; wired
  into the dispatcher.
- neode-ui/src/views/settings/TransportPrefsCard.vue — standalone card
  with a 5-row Auto/FIPS/Tor tri-state. Not wired into Settings.vue —
  the user places components themselves (see feedback_ui_entry_points).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:44:41 -04:00
Dorian
becdb1af5a fix(fips): fall back to upstream daemon npub on legacy/dev nodes
Nodes without a seed-derived FIPS key (legacy deploys, fresh pre-onboarding
installs) were reporting "Awaiting seed" in the dashboard even when the
upstream fips.service was running — status.npub was None unless
/data/identity/fips_key.pub existed.

- fips/service.rs: new read_upstream_npub() reads /etc/fips/fips.pub
  (bech32 text or raw 32 bytes) from the debian package.
- fips/mod.rs: FipsStatus::current() prefers the seed-derived npub,
  falls back to the upstream key. service_active is now TRUE if either
  archipelago-fips.service OR upstream fips.service is active; adds
  upstream_service_state to the status payload.
- fips/update.rs: resolve the upstream default branch from the GitHub
  repo API (jmcorgan/fips is on `master`, not `main`) instead of
  hardcoding — future repo rename just works.
- network/router.rs + api/rpc/router.rs: diagnostics gain wifi_ssid from
  `nmcli -t device` so the Network card can show the connected SSID.
- UI: Home.vue adds a FIPS row to the Local Network card; Server.vue
  mounts the new FipsNetworkCard and shows SSID + FIPS Mesh rows;
  HomeNetworkCard.vue removed (superseded by the inline rows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:42:56 -04:00
Dorian
c1cfca6212 feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into
the node stack, supervised by archipelago alongside Tor. Runs as a
system service, identity derives from the same BIP-39 master seed, and
user-triggered updates track upstream main.

Identity
  seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated
  secp256k1 key, distinct from the Nostr-node key for crypto isolation
  but still seed-recoverable
  identity.rs: writes fips_key[.pub] to /data/identity on onboarding,
  chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors

Transport
  TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4)
  → router prefers FIPS over Tor for all peer traffic
  PeerRecord gains fips_npub + last_fips fields (serde(default) for
  backward-compat with older nodes)
  transport/fips.rs: NodeTransport stub, reports unavailable until the
  daemon is live so router falls through to Tor cleanly

Federation invites
  FederatedNode and FederationInvite carry optional fips_npub
  create_invite / accept_invite / peer-joined callback thread it end
  to end; signature domain deliberately unchanged — FIPS Noise does
  its own session auth, so the unsigned hint only affects path
  selection

crate::fips
  config.rs: renders /etc/fips/fips.yaml and sudo-installs key material
  service.rs: systemctl status/activate/restart/mask wrappers
  update.rs: GitHub API check against upstream main; apply stubbed
  until per-commit .deb artefact source is decided

RPC + dashboard
  fips.status / fips.check-update / fips.apply-update / fips.install /
  fips.restart registered in dispatcher
  HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue
  when ready); shows state pill, version, FIPS npub, update button,
  activate button when key is present but service is down

ISO + systemd
  archipelago-fips.service: conditional on key presence, masked by
  default — backend unmasks after onboarding writes the key
  build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS
  .deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt
  installs it so trixie resolves deps; unit copied + masked

Version bump: 1.3.5 → 1.4.0

Tests: 33 new/updated passing (seed, identity, transport, federation,
fips module, transport::fips).

Known gaps: fips.apply-update returns a clear stub error until
upstream publishes per-commit .deb artefacts; HomeNetworkCard is not
mounted in Home.vue by default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:51 -04:00
Dorian
902e730bd2 fix(mesh): single-flight send + spinner + async federation POST
Root cause of the "every bubble shows twice" complaint after the prior
dedup fix: the frontend was firing mesh.send twice per user action. A
held/repeating Enter key on the input fires a keydown per repeat, and
handleSendMessage didn't guard on mesh.sending, so both calls queued
through the store's sendQueue and both executed against the same
contact_id (backend logs show two mesh.send RPCs 13ms apart, same text).
That's why sender and receiver both saw doubles — the envelope actually
was transmitted twice.

Mesh.vue: handleSendMessage now early-returns if mesh.sending or
sendingArch is already set. Send button replaces the `...` placeholder
with a proper spinning ring (`.mesh-send-spinner`) so the held-Enter case
stops looking like the app is ignoring the user.

mesh/mod.rs: send_typed_wire_via_federation no longer blocks on the Tor
POST. Sent MeshMessage is recorded synchronously (UI bubble appears
instantly); the HTTP goes in tokio::spawn. Tor circuit setup was the
1–5s lag the user was seeing on every send to a federation peer. Delivery
failure still shows as `delivered: false` via the read-receipt path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:57:11 -04:00
Dorian
3d7a470064 fix(mesh): correct rpcClient.call() usage in clear-all button
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 12:00:32 -04:00
Dorian
15800ae747 feat(mesh): server name in adverts + clear-all button + CI fix
- Mesh adverts now use the node's configured server name (e.g. "ThinkPad",
  "Arch Dev") instead of DID key fragments ("Archy-z6MkmkSB")
- Added mesh.clear-all RPC to reset peers, messages, contacts, and history
- Added "Clear All" button in Mesh UI peers panel
- Both glibc and musl builds verified

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:53:06 -04:00
Dorian
9dd802998c feat: deploy-to-target supports .253 + mesh/federation/VPN updates
- Add deploy_secondary() function for deploying to multiple LAN nodes
- --both now deploys to .198 and .253 (previously .198 only)
- Fleet deploy updated for 3 LAN nodes
- Mesh DM fixes: protocol frame format, DM-via-channel routing
- Federation pending requests, discover modal
- VPN status UI improvements
- Image versions and container specs updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:07:08 -04:00
Dorian
e210376e05 feat(mesh): Telegram primitives pass + attachment transport router
Bundles the Phase 2b/3/4/5 work that accumulated across prior sessions
and the new attachment chunking router from this session. Everything
ships in one shot so the full mesh surface stays coherent on-wire.

Telegram primitives (variants 13–18, 20–22):
- Reply / Reaction / ReadReceipt / Forward / Edit / Delete
- Presence heartbeat + last-seen tracking
- ChannelInvite + ContactCard payload types
- MessageKey (sender_pubkey, sender_seq) as cross-transport identity
- Action menu, reply banner, edit banner, tombstones, (edited) marker
- Debounced auto-read-receipts on scroll + message arrival

Activated prototypes (Phase 4):
- PsbtHash send RPC
- Contacts CRUD (in-memory alias/notes/pinned/blocked)
- Outbox 📤 badge, rotate-prekeys button
- Chunked send fallback (MCIIXXTT framing) as auto-failover inside
  send_typed_wire when a typed wire exceeds the LoRa per-frame budget

Unified inbox (Phase 1):
- conversations.list + conversations.messages RPCs (UI collapse deferred)

Attachment transport router (new this session):
- ContentInline variant 23 + ContentInlinePayload carrying file bytes
  directly in the envelope for small files with no Tor path
- mesh.send-content-inline RPC — mirrors to local BlobStore, rides
  send_typed_wire which auto-chunks over MCIIXXTT framing (~2.3 KB cap)
- mesh.transport-advice RPC as single source of truth for tier
  decisions: auto-mesh / choose / tor-only / impossible
- Receive arm writes inline bytes to local BlobStore so the existing
  content_ref card renderer handles both transports uniformly
- MeshState.blob_store field + order-independent propagation from
  RpcHandler::set_blob_store / set_mesh_service
- Frontend handleAttachFile calls advice first, branches into silent
  auto-send, transport-chooser modal, Tor-only path, or red error
- Transport modal with 📡 mesh / 🧅 Tor options + ETA + disabled
  state when peer has no Tor reachability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:40:19 -04:00
Dorian
26088462c5 fix(mesh): route DM-via-channel on channel 0 (channel 1 unsupported)
Firmware rejected send_channel_text(1, ...) with "Unsupported command"
because channel 1 isn't configured on the device. Revert to channel 0
for the DM wrapper — the 0xD1 marker + dest_prefix header still
disambiguates DMs from plain public-channel text. Also revert
Mesh.vue publicChannel back to index 0 so user-typed broadcasts
target the same (only) working channel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:40:28 -04:00
Dorian
d22d488638 fix(mesh): DM-via-channel tunnel + disable presence spam
Meshcore direct unicast silently drops between our two Archy nodes
(firmware reports flood sends with resp_code=6 but nothing arrives).
Wrap DMs as channel-1 broadcasts with a [0xD1][dest_prefix(6)][inner]
header; receivers filter by prefix and dispatch the inner payload
through the existing typed/base64/chunk ladder. Shrink chunk body to
125B so the wrapper still fits the 160B LoRa budget. Auto-heal
routing: CMD_RESET_PATH (0x0D) any type-1 contact with path_len=0 on
refresh so floods take over. send_text now returns the firmware's
flood/direct mode flag for diagnostics.

Disable the 120s presence heartbeat broadcaster — its CBOR payload
was being re-echoed as plaintext by the shared repeater, spamming
every visible node with garbled "Archy-…: av�…fstatusfonline…"
messages on channel 0. mesh.broadcast-presence RPC stays registered
but no longer transmits. Re-enable only once presence moves off the
shared broadcast path.

Also: MeshState.cmd_tx behind RwLock so stop()→start() cycles don't
fail with "command channel already consumed"; MeshService.send_cmd
helper; drop_message_by_id for control envelopes that shouldn't
appear as Sent bubbles; self_advert_name reflected into MeshStatus
after set; path_len/flags parsed out of RESP_CONTACT.

Frontend: unified inbox merges mesh peers with federation nodes by
DID/pubkey/name; hide presence/read_receipt/edit/channel_invite/
contact_card from chat stream; publicChannel index → 1 to match the
new DM-via-channel routing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:24:27 -04:00
Dorian
e259cf1854 feat(mesh-ui): Telegram-style action menu + Forward/Edit/Delete/ReadReceipt/rotate/outbox
* Replaces click-anywhere-on-bubble with a tiny ⋯ trigger in the meta row
  that fades in on hover (always visible on touch devices). Outside-click
  closes the menu, bubble gets a `menu-open` class so the trigger stays lit.
* Action menu gains Forward (any message) + Edit + Delete (own messages
  only, delete is red). Reaction spinner + reply preview upgraded to handle
  typed targets (attachment/invoice/location/alert) via summarizeForPreview.
* Pending-edit banner with ✎ icon mirrors the reply banner; Send flushes as
  mesh.edit-message when pendingEdit is set.
* Forwarded bubbles render "↪ Forwarded from {orig_name}" header; tombstone
  + (edited) markers; pending-reply close button upsized (28px, red hover).
* Scroll + message-arrival watcher fires a debounced 400ms read receipt
  with per-peer seq dedup so we never double-ack.
* Chat header: ⟲ rotate-prekeys button next to the shield badge; 📤 outbox
  count when mesh.outbox reports queued messages. Blob-store test widget
  removed and chat list now sorts by most-recent message timestamp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:50:08 -04:00
Dorian
5f7ebf145e fix(mesh): resolve ContentRef peer via DID + name-match fallback
Mesh peer pubkeys (LoRa advert ed25519) differ from federation node
pubkeys (archipelago identity), so matching on pubkey always missed
and attachments >160B had no transport. Match on master DID instead;
also accept an explicit peer_onion override from the frontend, which
resolves the peer by display name against federation.list-nodes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:13:36 -04:00
Dorian
8d868a1d12 feat(mesh-ui): reply banner + inline reaction chips (Phase 2a)
Tap a bubble to open an action menu with Reply + 6 quick reactions.
Reply stashes the target MessageKey and flips the Send button to
"Reply" mode, routing through mesh.send-reply. Reactions call
mesh.send-reaction immediately and render as chips under the target
bubble, collapsed per emoji with a count and self-highlight. Reaction
messages are filtered out of the main chat stream so they don't create
standalone bubbles. Reply bubbles show a "↳ quoted snippet" header
when the target is still in the local window.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:19:30 -04:00
Dorian
a530a906b8 feat(mesh-ui): receive share-to-mesh postMessage + pending attachment
App.vue listens for postMessage({type:'share-to-mesh',cid,...}) from
marketplace app iframes, stashes the payload in sessionStorage, and
routes to /mesh. Mesh.vue reads the stash on mount (and on a synthetic
'archipelago:share-to-mesh' event when already on the view), showing a
pending-attachment banner in the compose area. Send becomes Share and
flushes the CID via mesh.send-content with the input text as caption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:58:04 -04:00
Dorian
7497fd8a0d feat(mesh-ui): attach button + ContentRef card in chat
Compose row gains a 📎 attach button that uploads the file via /api/blob
and calls mesh.send-content for the selected peer. Received content_ref
bubbles render as a caption+filename card with either an inline image
preview or a Download button that calls mesh.fetch-content and swaps in
the returned local_url.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:10:59 -04:00
Dorian
a0fdb3f550 feat(blobs): HTTP upload+download routes and UI round-trip widget
Plumbs the BlobStore from blobs.rs into ApiHandler. The HMAC capability
key is derived from the node's Ed25519 signing key via a domain-separated
SHA-256 — rotating the identity rotates every outstanding cap (intentional
so a replaced node cannot honour old tokens).

New routes (added to nginx config in both server blocks):
- POST /api/blob — session-authenticated raw upload, returns
  {cid, size, mime, filename, self_test_url}. The self_test_url is a
  pre-signed cap pointing at the local node so the UI can verify the
  round-trip without needing a peer pubkey.
- GET /blob/<cid>?cap=<hex>&exp=<epoch>&peer=<pubkey> — peer-facing,
  HMAC-verified in constant time, expiry-checked, then streams bytes.

Mesh.vue gets a minimal "Attachment test (blob store)" section: file
picker → upload → cid display → "Verify round-trip" and "Open in new
tab" buttons. This validates Phase 3a end-to-end before we layer the
ContentRef typed envelope variant on top.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:48:48 -04:00
Dorian
ca7119df8c fix(rpc-client): 15min timeout on package.install for multi-GB stacks
IndeedHub, Bitcoin, and Penpot installs routinely exceed the default
RPC timeout on first pull. Bump package.install specifically to
900s so the frontend doesn't drop the request while the backend is
still downloading images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:01:31 -04:00
Dorian
07ca6ca286 feat(mesh-ui): render tx/lightning relay typed messages and skip self-send
Adds renderers for tx_relay, tx_relay_response, tx_confirmation,
lightning_relay, and lightning_relay_response message types so these
appear as rich cards in the chat stream. sendArchMessage now looks up
our own onion via getTorAddress and skips federation peers that match,
preventing the duplicate "echoed back to self" message we were seeing
on single-node test federations. Empty-federation error message is
also clearer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:01:21 -04:00
Dorian
53bea2124d chore: remove CLAUDE.md and stale config files 2026-04-12 12:11:00 -04:00
Dorian
c71d543f4c fix: 23.182.128.160:3000 is primary registry everywhere
Swapped all registry references: image-versions.sh, marketplaceData.ts,
curatedApps.ts, catalog.json. git.tx1138.com is now fallback only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:43:01 -04:00
Dorian
c520109108 fix: gitea direct port access, push to registry, no PROXY_APPS
Gitea image pushed to Archipelago registry. PROXY_APPS stays empty
per user preference - direct port only. Gitea config uses
INSTALL_LOCK + dark theme.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:52:15 -04:00
Dorian
61e251b8ca fix: gitea always uses nginx proxy for iframe compatibility
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:46:07 -04:00
Dorian
96ca70e7a4 fix: 60s timeout on image pull, gitea port 3001, wireguard first-boot
Image pulls now timeout after 60s and fall through to dynamic registry
fallback instead of hanging forever when primary is unreachable.
Gitea external port corrected to 3001. WireGuard key generation
added to first-boot for fresh installs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:00:06 -04:00
Dorian
bf7bc7f104 fix: ISO install - fallback registry, filebrowser noauth, registries
1. registries.conf includes docker.io search + fallback 23.182.128.160
2. First-boot pull_with_fallback() tries primary then fallback registry
3. FileBrowser created with noauth config on persistent volume
4. Backend dynamic registries.json pre-created in ISO
5. Filebrowser password secret created for token flow

Fixes: apps stuck at 0% download, filebrowser not working, dynamic
catalog not loading on fresh installs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 09:06:12 -04:00
Dorian
ff5ef2951f feat: dynamic app catalog, Gitea app polish, registry sync
App catalog served from Gitea repos (app-catalog) with 35 apps.
Nodes fetch catalog dynamically — new apps appear without frontend
rebuild. Test app added and removed to verify pipeline.

Gitea manifest updated with internal_port/nginx_proxy for iframe.
Updated catalog.json, nginx configs, app session configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 08:20:18 -04:00
Dorian
1165e52c92 fix: Gitea iframe uses proxy path, not direct port
Added gitea to PROXY_APPS so it always routes through /app/gitea/
nginx proxy (same origin as parent page). Fixes X-Frame-Options
SAMEORIGIN rejection when loading via direct port.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 07:05:32 -04:00