Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:49 +00:00
commit 25d938cd1a
2315 changed files with 510085 additions and 0 deletions
@@ -0,0 +1,23 @@
---
created: 2026-07-30T22:19:31.770Z
title: Connected-nodes list must scroll at row-matched height, not grow to fit
area: ui
severity: blocker
files:
- neode-ui/src/views/web5/Web5ConnectedNodes.vue
- neode-ui/src/views/web5/Web5.vue
---
## Problem
Dorian reports (2026-07-30): the connected-nodes container's inner list was still meant to scroll, and currently it doesn't — it grows to fit all rows without a scrollbar. The intended behavior for the row layout:
- The connected-nodes container sits in a row next to another container (on the right).
- If the right-hand container is TALLER, the connected-nodes inner list should extend/scale to match the right container's height.
- Within that matched height the list must remain SCROLLABLE when it has more rows than fit — it must NOT expand to show all entries scroll-free (which stretches the row).
So: height is driven by the sibling (equal-height row), overflow inside is `scroll`, never grow-to-fit. Likely component: `Web5ConnectedNodes.vue` inside `Web5.vue`'s row layout (verify — "connected nodes" also appears in settings AccountInfoSection). This sounds like a regression/misimplementation of an earlier request ("was still meant to scroll").
## Solution
Classic equal-height-row + inner-scroll pattern: the row uses flex with stretch alignment; the connected-nodes card gets `min-height: 0` (or `h-full` with a min-h-0 flex column) and the inner list `flex-1 overflow-y-auto`, so its height tracks the taller right-hand sibling while the list scrolls internally. Confirm exact container with Dorian's screenshot/preview if ambiguous. Standing rule: change only this behavior — no other visual changes.
@@ -0,0 +1,21 @@
---
created: 2026-07-30T22:19:31.770Z
title: Fedimint gateway must not install with a pre-set password
area: security
severity: blocker
files:
- core/archipelago/src (app orchestrator / secrets materialisation)
---
## Problem
Dorian reports (2026-07-30): the Fedimint gateway app installs with a password already set up — i.e. a default/pre-baked credential rather than one generated at install time or set by the user. Default credentials on a Lightning/fedimint gateway are a security hole: anyone knowing the shipped default can access the gateway. Dorian rated this a blocker.
This also violates the repo's standing secrets invariant (CLAUDE.md): secrets must be manifest-declared (`generated_secrets`, materialised by `container::secrets`, 0600/rootless) — never hardcoded or per-app baked in.
## Solution
Tracked as requirement **FED-07** (Phase 1, ROADMAP success criterion 7 — needs a gap plan since phase 1's 10 plans predate it):
- Find where the fedimint gateway app's manifest/install path sets its password (check `/opt/archipelago/apps/*/manifest.yml` for the gateway, and the orchestrator's secrets materialisation).
- Move it to `generated_secrets` so each install gets a unique credential, or require explicit user setup.
- Migration path for existing installs still carrying the default password (migrations must never destroy data).
@@ -0,0 +1,19 @@
---
created: 2026-07-30T22:19:31.770Z
title: Keep FIPS/Tor pills on cloud files and show them on mobile
area: ui
severity: blocker
files:
- neode-ui/src/views/Cloud.vue
- neode-ui/src/views/CloudFolder.vue
---
## Problem
Dorian loves the FIPS/Tor pills shown on cloud files ("really helpful") — they must be kept and never removed in any future cleanup or refactor. However, they do not (or may not) display on mobile widths, where they are hidden or cramped. Dorian rated mobile visibility a blocker: on mobile, users genuinely can't see the security/transport state of their files.
Context: captured 2026-07-30 during phase 02 (ui-performance) execution. The pills live in the Cloud file listing UI (Cloud.vue / CloudFolder.vue area — verify exact component when picking this up).
## Solution
Ensure the FIPS/Tor pills render at mobile breakpoints in the cloud file views — likely responsive-layout work (wrap/stack/compact-pill variant) rather than new functionality. Must follow the standing rule: no changes to existing desktop visuals/animations — mobile-only additions. TBD exact approach after inspecting the component at mobile widths.
@@ -0,0 +1,54 @@
---
created: 2026-07-30T22:19:31.770Z
title: Migrate all VPS2 references to https://source.archipelago-foundation.org (stop exposing IP; TLS everywhere)
area: security
severity: blocker
files:
- apps/*/manifest.yml
- app-catalog/catalog.json
- neode-ui/public/catalog.json
- core/archipelago/src (update.rs, container/registry.rs, image_versions.rs, image_policy.rs, quadlet.rs, prod_orchestrator.rs, app_catalog.rs, companion.rs, api/rpc/package/*)
- core/container/src/podman_client.rs
- core/archipelago/src/fips/anchors.rs
- scripts/self-update.sh
- scripts/first-boot-containers.sh
- scripts/create-release.sh
- scripts/create-release-manifest.sh
- scripts/image-versions.sh
- scripts/generate-app-catalog.sh
- scripts/bootstrap-switchover.sh
- release-manifest.json
- releases/manifest.json
- tests/lifecycle/remote-lifecycle.sh
- .gitea/workflows/demo-images.yml
- neode-ui/src/components/CompanionIntroOverlay.vue
---
## Problem
Dorian (2026-07-30, CRITICAL): every VPS2 service is referenced by raw IP `146.59.87.168` — plain HTTP for most — exposing the IP in shipped artifacts and requiring insecure-registry config on every node. Everything must move to `https://source.archipelago-foundation.org` "perfectly so all the stuff we use it for works".
**Verified inventory (2026-07-30):** ~196 references.
- `:3000` (Gitea — git/raw/releases AND the OCI container registry): **194 refs** — every app manifest's `image:` field, both catalogs (app-catalog/catalog.json + neode-ui/public/catalog.json, 23 each), compiled Rust core (self-update, registry allow-list, image policy/versions, quadlet gen, package RPCs, podman client), OTA manifests (release-manifest.json, releases/manifest.json), lifecycle tests, release/first-boot/self-update scripts, CI workflows, docs, UI marketplace data.
- `:2100` (companion APK package server): 1 ref (`CompanionIntroOverlay.vue`).
- `:8444` (FIPS trust anchor, raw TLS — `ARCHY_ANCHOR_ADDR` in `fips/anchors.rs`): 1 ref.
**Domain coverage verified live (2026-07-30):**
- git clone/ls-remote over HTTPS: ✓ (works; `gitea-ai` remote already switched)
- Gitea raw + releases: ✓ (HTTP 200)
- Container registry `/v2/` through the domain: ✓ (proper 401 auth-challenge; TLS pulls will work and the insecure-registry hack becomes removable)
- `:2100` APK server: ✗ NOT behind the domain — needs an openresty route/subdomain on VPS2 (Dorian/VPS2-side)
- `:8444` FIPS anchor: ✗ raw TLS service, can't share 443 — at minimum switch the constant to a DNS name (e.g. anchor host + :8444); check whether anchors pin certs before touching (security-sensitive)
## Solution
Execute as a dedicated quick task IMMEDIATELY AFTER phase 2's 02-08 (don't disturb the dev-pair perf measurements; requires core Rust rebuild + node verification per CLAUDE.md discipline):
1. Mechanical rewrite of all `:3000` refs → `https://source.archipelago-foundation.org` (registry image refs become `source.archipelago-foundation.org/lfg2025/<img>:<tag>` — no port, implies 443/TLS).
2. **Image-ref transition safety:** running containers/quadlets store the OLD image name; migration must not orphan or re-pull-break existing apps (migrations never destroy data). Plan the transition: catalog+manifests flip, orchestrator treats old/new registry names as equivalent during adoption, or staged re-tag.
3. Rebuild core, run the lifecycle gate on a dev node (NOT the fleet), verify: self-update fetch, catalog fetch, image pull via domain, app install end-to-end.
4. Keep the IP endpoints alive on VPS2 during transition — nodes updating FROM old releases still hit IP URLs (fallback window until fleet rolls past).
5. `:2100` + `:8444`: Dorian is setting these up (2026-07-30) as:
- `companion.archipelago-foundation.org` → replaces `http://146.59.87.168:2100` (companion APK downloads; update `CompanionIntroOverlay.vue` + any packaging docs). HTTPS assumed — verify before flipping.
- `fips.archipelago-foundation.org` → replaces the raw IP in `ARCHY_ANCHOR_ADDR` (`fips/anchors.rs`), becoming `fips.archipelago-foundation.org:8444` (or whatever port Dorian lands on). SECURITY-SENSITIVE: check whether the FIPS anchor validation pins certificates/identity to the address before changing; verify anchor handshake against the new name from a dev node before fleet rollout.
- As of 2026-07-30 22:50 UTC neither subdomain resolved yet — re-verify DNS + TLS at execution time; do not flip these two refs until both probes pass.
6. Remove insecure-registry config from node provisioning once pulls verify over TLS.
@@ -0,0 +1,16 @@
---
created: 2026-07-30T22:19:31.770Z
title: Add loader states to surfaces that take time to open
area: ui
severity: major
files:
- neode-ui/src/views (inventory needed — which opens are slow with no loading affordance)
---
## Problem
Dorian (2026-07-30): "some stuff doesn't have loader states when it's gonna take time to open" — certain screens/actions with genuinely slow opens give no loading feedback, so the UI feels dead/broken while working.
## Solution
Inventory slow-open surfaces (02-08's PERF-AFTER timings + the flagged timing regressions are a starting list: Discover, Server, Web5, Fleet, AppDetails, OpenWrtGateway); add house-style skeletons/spinners where an open can exceed ~300ms, consistent with the phase 2 rule that cached revisits show no spinner. Clearest confirmed instance (02-08 classification, pre-existing since 2026-07-22): `Cloud.vue` `viewPaidItem()`'s `content.owned-get` call has a 60s timeout and zero loading indicator. `CloudFolder.vue`'s native file grid already has a skeleton (fine). Tracked as UIFIX-06 (phase 1).
@@ -0,0 +1,22 @@
---
created: 2026-07-30T22:19:31.770Z
title: Onboarding tickbox hidden below fold on short screens — make it beautifully obvious
area: ui
severity: blocker
files:
- neode-ui/src/views/OnboardingSeedGenerate.vue
- neode-ui/src/views/OnboardingWrapper.vue
---
## Problem
Dorian reports (2026-07-30): on shorter screens, the onboarding screen's tickbox (the seed-phrase confirmation checkbox in `OnboardingSeedGenerate.vue` — verify it's this step) sits below the fold inside a scrollable area, and it's not obvious it exists — users don't realize they need to scroll to find it, so onboarding appears stuck. Blocker: this stalls new-user onboarding on small displays.
## Solution
Make its presence obvious "in a beautiful way" (Dorian's words — design discretion, but it must feel intentional and match the house glass/dark style, not a bolted-on arrow). Candidate approaches to evaluate:
- A soft bottom scroll-affordance: gradient fade at the container's bottom edge plus a subtle animated chevron/pulse when unscrolled content remains, disappearing once the tickbox is in view.
- Or make the tickbox + continue button a sticky footer of the card on short viewports so it's always visible, with the seed content scrolling behind it.
- Or auto-scroll/nudge the container after the seed is revealed.
Constraints: follow the standing rule — don't alter the existing onboarding look beyond this affordance; keep desktop/tall-screen appearance unchanged; whatever ships must be verified on an actual short viewport (e.g. small laptop / mobile landscape).
@@ -0,0 +1,16 @@
---
created: 2026-07-30T22:19:31.770Z
title: Paid Files pictures open in a browser tab instead of the app lightbox
area: ui
severity: major
files:
- neode-ui/src/views/Cloud.vue (Paid Files sub-tab — locate the picture click handler)
---
## Problem
Dorian (2026-07-30, on archi-dev-box; initially said "peer files", corrected to **Paid Files**): in the Cloud tab's Paid Files section, clicking a picture opens it in a new browser tab rather than the in-app lightbox that exists elsewhere ("the lightbox we've made"). Inconsistent media UX.
## Solution
Route Paid Files image clicks through the existing lightbox component. Classified by 02-08 (pre-existing, introduced f3393581 2026-07-22): `Cloud.vue`'s `viewPaidItem()` (~line 470) calls `window.open(...)` instead of routing into `MediaLightbox.vue`. Fix: invoke MediaLightbox the way My Files does. Tracked as UIFIX-04 (phase 1).
@@ -0,0 +1,23 @@
---
created: 2026-07-30T22:19:31.770Z
title: PiP robustness — close lightbox fluidly, survive tab changes and buffering
area: ui
severity: major
files:
- neode-ui/src (lightbox/video player components — locate exact files)
---
## Problem
Dorian (2026-07-30), two reports:
1. Entering picture-in-picture on a video does not close the lightbox — it should close with a fluid on-brand animation as PiP begins.
2. "Picture in picture must survive tab changes or video buffering pauses, doesn't seem to, please fix." — PiP currently dies when switching main tabs and/or when the video pauses to buffer.
Classified by 02-08 (all pre-existing, NOT phase 2): `src/utils/pip.ts`'s `togglePip()` (introduced f72d4b92 2026-07-23) never touches `MediaLightbox.vue`'s visibility state; no `onDeactivated` teardown exists in Cloud/CloudFolder/MediaLightbox (phase 2 added none there); pre-phase-2 tab switches fully unmounted the view, so PiP tab-survival never existed. The `<video>` has zero pause/waiting/stalled listeners — buffering behavior is pure native. KeepAlive actually helps now: the element persists off-screen, so survival is achievable by not letting anything pause/unmount it.
## Solution
- PiP enter → close the lightbox with a house-style fluid animation (handoff feel).
- While PiP is active: the owning video element must not be paused/reset/unmounted by tab deactivation — exempt media playback from deactivate teardown when `document.pictureInPictureElement` is the view's video (or reparent the video to a persistent host).
- Buffering (`waiting` events / `pause` from buffering) must not close PiP or the session — only explicit user stop should.
Tracked as UIFIX-05 (phase 1 gap plan).
@@ -0,0 +1,32 @@
---
created: 2026-07-31T19:00:00.000Z
title: Build the installer ISO for v1.7.119-alpha
area: tooling
severity: minor
files:
- scripts/build-iso-release.sh
---
## Problem
v1.7.119-alpha is released and its OTA assets are published and checksum-verified, but the **installer ISO was never built**. Three attempts were blocked by a dirty working tree — `build-iso-release.sh`'s preflight hard-fails on any uncommitted change, and this checkout is shared by phase 1 executors and a separate BotFights session, so the tree is rarely clean for long. Not urgent: the ISO only matters for fresh installations; everyone updating is served by the working OTA.
## Solution
Run during a genuine pause in phase 1 execution (between waves), not racing against it:
```
TMPDIR=/home/archipelago/tmp/iso-scratch bash scripts/build-iso-release.sh
```
Keep the QEMU boot test (no `--no-qemu`).
**Preconditions:** `git status --porcelain` empty on `main`.
**Launch it as an explicit background job.** The build runs well past 10 minutes; twice it hit the tool's foreground timeout and once that killed an in-progress `cargo test` outright.
**Build from current `main` HEAD, not a detached tag checkout** — decision already made, don't re-derive. Preflight requires `git rev-parse --abbrev-ref HEAD == "main"` (a detached checkout returns `HEAD` and fails), and the archipelago version identity (Cargo.toml / package.json / CHANGELOG / manifest, all `1.7.119-alpha`) is untouched by commits made after the tag. BotFights is versioned and shipped independently via the signed catalog, so building at HEAD ships the security-fixed 1.2.11 rather than reverting to the vulnerable 1.2.9 current at tag time.
**Gates already seen passing** (mostly cached, expect them to pass fast): git-diff-check, cargo-fmt, catalog-drift, whats-new-sync, ui-type-check, ui-unit-tests (788), cargo-check. **Not yet reached:** cargo-test-weekly (~1500s), verify-artifacts, build-iso, iso-smoke, qemu-boot.
`TMPDIR` matters while `/tmp` (12G tmpfs) is tight — honored natively by mktemp-based scripts and by `test-iso-qemu.sh` since commit `6c2b6668` fixed its hardcoded `/tmp` paths.
@@ -0,0 +1,60 @@
---
created: 2026-08-01T04:05:00.000Z
title: Make archi-dev-box double as a fresh test node (both shapes, no ISO flash)
area: testing-infra
severity: major
files:
- core/archipelago/src/config.rs (the env seams: ARCHIPELAGO_DATA_DIR:125, ARCHIPELAGO_BIND:129, ARCHIPELAGO_PORT_OFFSET:155, ARCHIPELAGO_APPS_DIR)
- core/container/src/port_manager.rs (port_offset applied at :44)
- core/archipelago/src/auth.rs (:182 is_onboarding_complete — "fresh" is decided purely from state inside data_dir)
- tests/lifecycle/ (existing gate harness — candidate host for the new node profile)
---
## Problem
Dorian (2026-08-01): wants archi-dev-box to serve as a testing node that can be exercised
"as if it's a new node", running alongside the existing Linux desktop app install, **without
flashing the ISO**. Both shapes are wanted — this is a testing node, so it needs to cover
first-run UX *and* real app lifecycle.
Today the box runs one real production-ish node: `archipelago.service` (systemd, enabled,
`/usr/local/bin/archipelago`) against a heavily-populated `/var/lib/archipelago` (bitcoin,
btcpay, botfights, blobs, live LND/mesh state). That node must not be disturbed — it is the
dev-pair deploy target gated before every OTA.
## Solution
Two shapes, both ISO-free. Ship A first, then B.
**(A) Lightweight second instance, same Linux user.** Own empty `ARCHIPELAGO_DATA_DIR`, own
`ARCHIPELAGO_BIND` port, `ARCHIPELAGO_PORT_OFFSET` set, mesh/Reticulum disabled. Boots
un-onboarded, so it exercises the true first-run path: seed generation, password/setup,
identity keygen, node naming, onboarding UI. Cheap to create and destroy — the natural
regression harness for onboarding changes.
**(B) Second Linux user** (`useradd` + `loginctl enable-linger` + its own rootless podman
namespace + own data dir). Gives a genuinely independent node where app install / uninstall /
reinstall lifecycle is real, not shared. This is what makes it a *testing node* rather than a
first-run mock.
## Hazards (verified by grep 2026-08-01, must be designed around)
1. **Hardcoded paths defeat `ARCHIPELAGO_DATA_DIR`.** Several constants point at
`/var/lib/archipelago` literally and ignore the override: `bitcoin_rpc.rs:10`
(`SECRETS_PATH`), `container/lnd.rs:131` (`ARCHY_DATA_DIR`), `electrs_status.rs:15`,
`api/rpc/package/pine_ha.rs:34-36`, `bootstrap.rs:242` (secrets dir), `disk_monitor.rs:41`.
Under shape (A) a test instance that installs **Bitcoin, LND, electrumx, or Pine/HA would
read and write the LIVE node's files.** Those four are off-limits in (A); shape (B) fixes
this properly via a different user's paths — or the constants get plumbed through config,
which is arguably the real fix and a candidate follow-up.
2. **Rootless podman is per-Linux-user.** In (A) both instances share one container
namespace: `PORT_OFFSET` resolves port collisions, container *name* collisions it does not.
3. **Reticulum/mesh contention.** The live daemon holds `/dev/mesh-radio` and
`identity/node_key`; a second instance would fight it for the radio. Mesh must be off for
the test node (or the radio explicitly assigned to one of them).
## Notes
Raised while resuming quick task 260731-upz (entropy/seed audit) — shape (A) is also the
natural on-node harness for that audit's UNVERIFIED checklist, since fresh-seed generation is
exactly the `[ARCHY-1]` path under review. Sequence this after 260731-upz lands.
@@ -0,0 +1,80 @@
---
created: 2026-08-01T09:45:00.000Z
title: Verify FED-07 gateway credential rotation on a real node (01-16 Task 2)
area: security
severity: major
files:
- core/archipelago/src/container/secrets.rs
- core/archipelago/src/container/prod_orchestrator.rs
- .planning/phases/01-federation-mesh-hardening/01-16-PLAN.md
---
## Problem
FED-07's code is complete and pushed (`42652547` removed every shipped credential,
`9e2d2ef2` added detection + rotation for nodes already carrying one), but **plan 01-16's
blocking Task 2 checkpoint has never been run**, so the requirement is not closed. Two
things are unproven:
1. **That the recreate actually preserves the gateway's data.** Rotation deliberately
avoids any teardown: it changes the credential, which changes `secret_env_hash`, which
the drift check reads as a container-label mismatch, which fires the platform's own
recreate around the unchanged data directory, ports, volumes and container name. That
chain is confirmed by reading the code (`prod_orchestrator.rs:3309` computes the hash,
`:3374` compares the label) but has **never been observed running**.
2. **That the rotated credential actually authenticates** and the old shipped one is
rejected.
## Why it was deferred (2026-08-01)
Checkpoint step 1 was run read-only on archi-dev-box. Findings:
- **The node is CLEAN** — the hash file is present, `600`, `archipelago:archipelago`, and
is *not* the shipped default. The legacy `fedimint-gateway-password` file also exists.
- **There is no `fedimint-gateway` container.** The app is installed at
`/opt/archipelago/apps/fedimint-gateway`, but nothing is running and
`/var/lib/archipelago/fedimint-gateway` is empty. `archy-fedimint-ui` and
`fedimint-clientd` are running, but they are not the gateway.
So the rotation path cannot fire naturally here, and steps 56 (data survived, gateway
authenticates) have nothing to exercise. Proving it on this box means installing and
starting the gateway first, then deliberately seeding the old value — which the plan's
Planner Assumption already anticipates and requires be recorded.
The deploy itself is the blocker: **30 containers are running with 48 days uptime**
(IndeeHub relay/minio/postgres/ffmpeg, Immich, BTCPay + nbxplorer, netbird, portainer,
strfry, searxng, the archy UI set), the `archipelago` system service is active, and
CLAUDE.md's standing rule is that restarting it SIGKILLs containers until Quadlet is the
default. Step 8 then wants `tests/lifecycle/run-gate.sh` on the same box, which cycles
install/stop/start/reinstall/reboot-survive. Dorian chose to hold rather than take that
blast radius unattended.
## Solution
Run 01-16 Task 2's eight steps in a window where restarting `archipelago` on the target is
acceptable. Two viable routes:
- **On archi-dev-box:** install + start `fedimint-gateway`, put some state in its data
directory, seed the old hash into
`/var/lib/archipelago/secrets/fedimint-gateway-hash`, deploy this build, restart, then
run steps 38. Record that the affected state was seeded deliberately.
- **On a node that runs the gateway for real** (.228 is the heavy 14-app node): the
rotation path may exist naturally there, which is stronger evidence than seeding.
Either way, record in `01-16-SUMMARY.md`: the affected-or-clean verdict, the exact deploy
command, whether the `secret_env_hash` recreate was observed firing, the gate result, and
the fact that archy-x250-dev was offline (single-node verification with the second-node
gap recorded honestly is the expected pattern).
## Related, found while doing 01-16
- **Operator recovery gap:** `handle_package_credentials`
(`core/archipelago/src/api/rpc/package/install.rs:2093`) is a hardcoded if-chain covering
only `filebrowser` and `photoprism`. `fedimint-gateway` falls through to an empty list,
so after a rotation the operator has no in-UI way to get the new password — only the
`0600` file the log line names. Adding one arm closes it; the UI already renders whatever
the RPC returns.
- **`apps/photoprism/manifest.yml:35` ships `PHOTOPRISM_ADMIN_PASSWORD=archipelago`**, and
that same literal is handed to the UI. Same defect class as FED-07 on a different app —
wants its own requirement.
</content>
@@ -0,0 +1,111 @@
---
created: 2026-08-02T12:10:00.000Z
title: Companion 0.5.27 handover — node/web-side clipboard + QR scanner work
area: ui
severity: major
files:
- neode-ui/src/main.ts (:10-24 clipboard polyfill — the fake readText is the bug)
- neode-ui/src/utils/clipboard.ts (to create — one util for 30 call sites)
- neode-ui/src/views/web5/utils.ts (:39 safeClipboardWrite — best existing base)
- neode-ui/src/components/WalletScanModal.vue (prewarm, torch, don't re-init between panes)
- docs/qr-scanner-snappiness-handover.md (three factual corrections — see below)
---
## Problem
Handover from the companion-app workstream (build **0.5.27, versionCode 47**), 2026-08-02.
All of it is **node-repo / `neode-ui/` side** — none was implemented there, the companion was
the only thing changed. Captured verbatim-in-substance so it is not lost.
### 1. Companion now shims `navigator.clipboard` — do not clobber it
0.5.27 adds a native `ArchipelagoClipboard` bridge, injected on every page load, pointing
`navigator.clipboard.readText/writeText` at the Android clipboard. Fixes copy **and** paste
in-app with zero web changes, in both the kiosk WebView and the in-app browser (BTCPay, LND).
Contract to preserve:
- Shim runs at `onPageStarted` and `onPageFinished`; sets `window.__archyClipboardPatched = true`.
- Defines `navigator.clipboard` as **configurable** if absent, then assigns `readText`/`writeText`
onto whatever object is there.
- Reads return via `window.__archyClipboardResult(text)`.
- **Do not** unconditionally re-define `navigator.clipboard` after page load. **Do not**
`Object.freeze` it. Today's `main.ts` polyfill is safe *only* because it is guarded by
`if (!navigator.clipboard)`.
### 2. Web-side clipboard bugs — still open, affect plain browsers
Native is fixed; the same code is broken in any **plain-HTTP** browser (LAN/mesh — non-secure
context, so `navigator.clipboard` is undefined).
- `neode-ui/src/main.ts:10-24` — the polyfill defines `async readText() { return '' }`. That
makes `SendBitcoinModal.vue:425`'s `canReadClipboard` **true**, so "Paste invoice" renders,
fires, gets `''`, and silently does nothing. Fix: drop the fake `readText` (or define it only
when a real source exists) so the button correctly hides.
- **30 `writeText` call sites, three patterns:**
- ~8 duplicate their own `execCommand` fallback — `Server.vue:762`, `Apps.vue:708`,
`Credentials.vue:392`, `settings/AccountInfoSection.vue`, `settings/TwoFactorSection.vue`.
- ~10 are bare `navigator.clipboard.writeText(x).catch(() => {})`
`ReceiveBitcoinModal.vue:149`, `SendBitcoinModal.vue:326`/`:574`,
`OnboardingSeedVerify.vue:201`, `OnboardingDid.vue:216`/`:223`,
`settings/BackupSection.vue:270`, `PeerFiles.vue:1110`/`:1415` — these show "Copied!"
whether or not anything reached the clipboard.
- `views/web5/utils.ts:39` `safeClipboardWrite` is the best existing base.
- The `execCommand` fallbacks are fragile: no `focus()`, no `readonly`, no `setSelectionRange`,
and **the return value is never checked**, so failure is invisible.
**Suggested shape:** one `src/utils/clipboard.ts` exporting `copyText()` / `readText()` /
`canPaste()`, preferring native bridge → async Clipboard API → hardened `execCommand`, toasting
"Copied" only on real success. Repoint all 30 sites at it.
**Paste affordances that don't exist yet** (bare textareas today): `WalletScanModal.vue` paste
field, ecash token (`views/web5/Web5SendReceiveModals.vue:160`), **signed PSBT**, federation
invite code (`views/federation/JoinModal.vue:16`).
### 3. QR scanner — web-side items still open
Native items are done in 0.5.27. Remaining on the web side:
- **Pre-warm the camera** — start `getUserMedia` when the modal opens (action pane), not when
the scan pane is reached; hide the preview until needed.
- **Torch toggle** — `qr-scanner` exposes `hasFlash()` / `turnFlashOn()`.
- **Constraints** — `{ focusMode: 'continuous', width: { ideal: 1280 } }`.
- **Don't stop/start between panes** — amount → scan currently re-inits the scanner; keep the
paused stream alive for the modal's lifetime.
- Already done upstream: 10 scans/sec where `BarcodeDetector` exists.
- **New optional hook:** `window.ArchipelagoQr?.prewarm?.()` — safe to call repeatedly, safe when
absent. The companion also self-prewarms on every node page load, so this is a small extra win.
### 4. Corrections to `docs/qr-scanner-snappiness-handover.md` (fix the doc)
That doc's native section assumed ML Kit and is wrong on three points — leaving it uncorrected
invites someone to "optimise" the scanner backwards:
- The native scanner uses **ZXing** (Apache-2.0, on-device, no telemetry), **not ML Kit**. ML Kit
was rejected as a proprietary Google/Play-Services dependency, against project dependency
policy. There is therefore no model cold-start to pay.
- `FORMAT_QR_CODE`-only and `STRATEGY_KEEP_ONLY_LATEST` were **already in place** before this round.
- **Do not drop analysis resolution to 1280×720.** 1920×1080 is a deliberate 0.5.22 fix: at 720p,
dense bolt11 invoice QRs were undecodable on far-focusing lenses (e.g. Pixel 9a main) while
sparse address QRs still read — that was the original "scanner doesn't pick up invoices" report.
What 0.5.27 changed natively, for the record: two-tier decode (cheap centre-70% pass ~18/s plus
the thorough full-frame `TRY_HARDER` + inverted-retry pass ~5/s, replacing a single expensive
pass capped ~7/s); camera/decoder prewarm; torch toggle in wallet-scan and pairing scanners;
tap-to-focus with 4s suppression of periodic centre autofocus; zoom hunt alternating 1×/1.5×
after ~3s with no decode; success haptic on first hit only (so animated QRs don't buzz per frame).
### 5. Not a web issue, noted for completeness
The companion regained "swipe away in recents = restart": the retained kiosk WebView (kept so
remote ⇄ dashboard doesn't reload) is now released when the activity finishes, because the FIPS
mesh service keeps the process alive and the static WebView was surviving the swipe. A
**Restart** card was added to the hub menu as the manual path.
## Solution
Route into **Phase 11 (Wallet Experience & LND UI Parity)** rather than a standalone pass —
§2's signed-PSBT paste affordance and §3's scanner items are the same surface as **WALLET-05**
(the PSBT air-gap round trip), and `WalletScanModal.vue` is named in both. The clipboard utility
(§2) is broader than Phase 11 and can land independently; the doc corrections (§4) are a
five-minute fix that should not wait for a phase.
**Supersedes** any overlapping assumptions in `docs/qr-scanner-snappiness-handover.md` — that
doc is now known-wrong on the three points in §4.
@@ -0,0 +1,59 @@
---
created: 2026-08-02T12:30:00.000Z
title: Migrate VPS2 IP (146.59.87.168) to its domain across registry references
area: infra
severity: major
files:
- apps/*/manifest.yml (the bulk — every `image:` line)
- .gitmodules
- .github/workflows/*.yml (both)
- app-catalog/catalog.json (signed — changing it forces a re-sign)
- Android/**/FipsPreferences.kt, Android/**/PartyScreen.kt (compiled constants)
---
## Problem
**98 operational files still carry the bare IP `146.59.87.168`.** The domain
(`source.archipelago-foundation.org`) was only ever adopted for the *git remote* — the IP is
still baked into every **container registry reference**, which is a different thing entirely:
```
image: 146.59.87.168:3000/lfg2025/gatewayd:v0.10.0
```
Plus `.gitmodules`, both CI workflow files, `app-catalog/catalog.json`, and the Android
companion (`FipsPreferences.kt`, `PartyScreen.kt`). A further **117 hits live in `.planning/`
docs — those are historical records and must stay as they are.**
An IP in every manifest is exactly the kind of thing that bites when the VPS moves. It is also
conspicuous in a public repo, so the open-source-readiness work will want it done
(see `docs/OPEN-SOURCE-READINESS-PLAN.md`).
## Why this is not a find-and-replace
1. **The registry has to actually answer on the domain.** `source.archipelago-foundation.org`
currently serves Gitea over **HTTPS on 443**, while images are pulled from **:3000 over plain
HTTP**. Podman treats `host:3000` and `domain` as *different registries*, so changing the
string means every node re-pulls every image under the new name — and any node that cannot
resolve or trust the new host fails to pull at all.
2. **It invalidates the signed catalog.** `app-catalog/catalog.json` carries image references,
so changing them breaks the signature and forces a regenerate-and-re-sign — which needs the
release mnemonic, same as a release.
3. **The Android companion ships compiled constants**, so it needs its own APK rebuild to follow.
## Solution
Its own plan with a real rollout order — **not** something to slip into a release:
1. Registry serving on the domain (TLS, and a decision on whether images move to 443 or the
domain also exposes :3000)
2. Manifests
3. Catalog regenerate + re-sign (mnemonic ceremony)
4. APK rebuild
Sequencing matters because steps 24 are useless — and actively breaking — until step 1 holds.
## Notes
Analysis produced by the concurrent companion/release agent on 2026-08-02 before its session
ended; recorded here so it is not lost. It had not touched any of the work.
@@ -0,0 +1,76 @@
---
created: 2026-08-02T15:30:00.000Z
title: Add a "name your node" step to onboarding (sets the real hostname)
area: ui
severity: major
files:
- neode-ui/src/views/OnboardingName.vue (to create — match the existing onboarding step design)
- neode-ui/src/router/index.ts (:26-76 — the onboarding child routes, in flow order)
- neode-ui/src/composables/useOnboarding.ts (step persistence / resume)
- neode-ui/src/views/settings/AccountInfoSection.vue (the existing post-onboarding rename UI — reuse its validation)
- core/archipelago/src/api/rpc/system/handlers.rs (:462 hostnamectl, :58 + :765 regenerate_tls_cert — backend already exists, no change expected)
---
## Problem
Dorian (2026-08-02): wants a step in the onboarding flow to name your node — in the same
design language as the existing steps — which changes the actual hostname.
Today naming only exists **after** onboarding, in `settings/AccountInfoSection.vue`. A fresh
node keeps its install-time default until the user goes looking for the setting.
## What already exists (no backend work expected)
`server.set-name` (`dispatcher.rs:459``handle_server_set_name`) already:
- runs `sudo hostnamectl set-hostname <name>` (`system/handlers.rs:462`)
- regenerates the self-signed TLS cert with a SAN covering `<name>`, `<name>.local`,
`localhost`, `127.0.0.1` (`:58``regenerate_tls_cert`, `:765`)
- reloads nginx
Current onboarding order (`router/index.ts:26-76`): Intro → Options → Path → SeedGenerate →
SeedVerify / SeedRestore → Did → Identity → Backup → Verify → Done.
## The hazard that decides the design
**Renaming mid-flow can disconnect the user before their seed is backed up.**
The browser is connected to the node over its current hostname and current TLS cert. `set-name`
changes both: the mDNS `.local` name moves, and the cert is reissued. A user onboarding at
`https://archipelago.local` who renames to `mynode` can lose the session **in the middle of
onboarding** — potentially between seed generation and seed verification, which is the worst
possible moment to drop someone.
That makes step placement a design decision, not an implementation detail. Roughly:
1. **Last, just before Done** — everything security-critical (seed shown, verified, backed up)
is already complete, so a dropped connection costs nothing but a reload. Safest.
2. **First, before anything else** — the rename happens while there is nothing to lose, but the
user is asked to name a node before they have any context for what it is, and they may still
be mid-redirect when the cert changes.
3. **Defer the apply** — collect the name early for good UX, call `set-name` only at the end.
Best of both, at the cost of holding state across steps.
Option 3 or 1 is almost certainly right. This needs deciding explicitly rather than by
whichever screen the code lands on.
## Also needs deciding / checking
- **Validation + slugification.** Hostnames are RFC-1123: lowercase alphanumerics and hyphens,
≤63 chars, no leading/trailing hyphen. A user will type `Dorian's Node`. Decide whether to
slugify silently, show the slug live ("will be reachable at `dorians-node.local`"), or reject.
Reuse whatever `AccountInfoSection.vue` already does rather than inventing a second rule.
- **Does the rename propagate everywhere it should?** The Reticulum daemon takes a
`--display-name`, and mesh/FIPS surfaces show node names. Confirm whether `set-name` updates
those or whether the node keeps its old name on the mesh until restart.
- **Reconnection UX.** If the cert/hostname change does drop the session, the step should say so
in advance and tell the user where to come back to — not fail silently into a dead tab.
- **Skippable?** A node with no name is fine; forcing a decision at first run is friction. Decide
whether the step has a "keep the default" path.
## Solution
Own scope — a quick task or a small plan, not a freehand edit, because of the disconnect hazard
above. Sequence it **after** the in-flight `regenerate_tls_cert` atomicity fix lands (that fix
makes the rename path write the key to a staging file and validate before swapping, instead of
truncating the live key in place if openssl fails partway — renaming is exactly the path it
protects).