- first-boot-containers + image-versions for fmcd/fedimint - dual-ecash, meshroller-integration, and remaining-issues design docs - Android remote-input two-finger scroll + external-open handling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
Remaining issues — implementation plans
Written 2026-06-17. Covers the open Gitea issues not closeable in the single-box dev env. Each plan lists the files to touch, the approach, and how to verify (most need .116 + .198, a companion phone, or funded wallets). Issues #3 (VPN) and #5 (OpenWRT/TollGate) are intentionally out of scope per the user.
Status of the rest at time of writing:
- #31 group chat over Tor — dedup-by-
msg_idfix already shipped (open only for a 2-node Tor confirmation). See its Gitea comment. - #43 install on .70 — blocked: .70 unreachable. Plan below is a code-side hardening that doesn't depend on .70's logs.
#46 — Pay for peer files (local wallet OR invoice+QR to seller)
Status (2026-06-17): Phase 1 DONE & compiles (LN invoice + QR + release). Seller:
content_invoice.rsentitlement store,GET /content/{id}/invoice
/invoice-status/{hash}, invoice-paid path inserve_content(X-Invoice-Hash), LNDcreate_invoice/invoice_is_settled. Buyer:content.request-invoice/.invoice-status/.download-peer-invoice+PeerFiles.vuepicker modal + QR + poll. Phases 2 (on-chain) and 3 (local LN/on-chain methods) remain; needs live funded-wallet verify. Issue left open.
Goal. At the paid-download step in Cloud → peer files, let the buyer choose how to pay: (a) their local wallet (ecash today; LN/on-chain later), or (b) get an invoice with a QR drawn on the selling node's wallet, pay from any external wallet, and have the file release on confirmation.
What exists already
- Buyer ecash auto-pay:
content.download-peer-paid(mints ecash, downloads atomically) — wired inneode-ui/src/views/PeerFiles.vuedownloadFile(). - Payer-side builder:
streaming.prepare-paymentRPC +wallet/ecash.rs(build_payment_token, cross-mint),swarm/payment.rs. - Free streaming download:
/api/peer-content/:onion/:id(Range-capable). - LND invoice RPC:
lnd.createinvoice; ecash balance:wallet.ecash-balance.
Backend work
- Seller-side invoice RPC (new), e.g.
content.request-invoice{ onion, content_id }→ asks the selling node (over the existing/archipelago/...peer transport, same path machinery ascontent.download-peer-paid) to produce a payment request forprice_sats:- LN:
lnd.createinvoiceon the seller, returnbolt11+payment_hash. - on-chain:
lnd.newaddresson the seller, returnaddress+amount. - Seller records a pending entitlement keyed by
payment_hash/address → content_id → buyer.
- LN:
- Payment confirmation + release: seller polls its own LND
(
lnd.lookup-invoice/ address watch); on settle, marks the entitlement paid. Buyer side pollscontent.invoice-status { payment_hash }→ when paid, downloads via the existing/api/peer-content(gate now passes because the entitlement is satisfied). Reuse the streaming gate instreaming/— add an "invoice-paid" path alongside the ecash-token path. - Keep
content.download-peer-paid(local-ecash) as the (a) fast path.
Frontend work (PeerFiles.vue)
- Before a paid download, open a small payment-method picker modal:
- "Pay from this node's wallet" → existing ecash flow (show balance; if insufficient, the LN/on-chain local options when those land).
- "Pay from another wallet (QR)" → call
content.request-invoice, render thebolt11/address as a QR (add a tiny QR lib or reuse one already in the bundle — checkpackage.json), show amount + a live "waiting for payment…" state pollingcontent.invoice-status, then auto-download.
- Reuse the existing
purchaseError/downloadingstate +triggerDownload.
Verify: .116 (seller) + .198 (buyer), a funded regtest/LN wallet. Buyer picks QR, pays from a 3rd wallet, file releases. Then the local-ecash path.
Effort: large (multi-day). Phase it: (1) LN-invoice + QR + release, (2) on-chain, (3) local LN/on-chain methods.
#18 — Companion app: "open in external browser" apps don't work
Status (2026-06-17): DONE & compiles (Rust + TS); Android unbuilt here. Reverse relay hop added:
external_open_txchannel, kiosk publishes{"t":"o","url"}on/ws/remote-relay(URL-validated), forwarded to the companion's/ws/remote-input.requestExternalOpen()inremote-relay.tswired into all fourappLauncher.tsexternal-open sites;InputWebSocket.kt
RemoteInputScreen.ktopen it viaACTION_VIEW. Issue closed; live pairing test pending.
Goal. Apps configured to open in a new/external browser should launch on the phone when driven from the companion controller, using the phone-default- browser request pattern.
What exists
- Relay protocol in
neode-ui/src/api/remote-relay.ts— message casesm(move cursor),c(click),s(scroll, just fixed in #7). Click resolves the element under the virtual cursor viadeepElementFromPoint. - The kiosk side runs the dashboard; "open external" apps currently try to
window.openon the kiosk, which the phone never sees.
Approach
- Detect external-open intent on the kiosk: when a click lands on an
element that would open externally (anchor with
target=_blank/ an app flaggedopensExternally, or an interceptedwindow.open), instead of opening locally, send a new relay message to the phone:{ t: 'open-url', url }over the/ws/remote-relaychannel (the kiosk is the relay server side — find where it sends frames back to the companion). - Companion (phone) side handles
open-urlby doingwindow.open(url, '_blank')/location.href = urlso it opens in the phone's default browser.- If the companion is the Android APK (separate codebase, see
Android/+ memoryfeedback_companion_apk_not_in_update), add an intent-based handler there; if it's a mobile web client, handle in JS.
- If the companion is the Android APK (separate codebase, see
- Intercept
window.openon the kiosk dashboard globally (a small shim that, when remote-relay is active, forwards to the phone instead of opening).
Verify: phone + kiosk paired; tap an "open external" app from the companion; it opens in the phone browser.
Effort: medium; needs the companion device + possibly an APK change.
#50 — Integrate Meshroller into our mesh features
Decision made 2026-06-17: seam (a) — Rust-native lift. Full design with verified seam anchors (message types, dispatch, send API, event/trust gates, Ollama call) is in
docs/meshroller-integration-design.md. Summary below.
Source: https://gitea.l484.com/clasko/Meshroller
Phase 0 — review (DONE 2026-06-17)
- Reviewed. Meshroller is a single ~29KB Python script (
meshroller.py): a daemon that bridges a Meshtastic radio (via themeshtasticPython serial module,SerialInterface) to an Ollama LLM (qwen2.5-coder). It has trusted-node auth, scheduled/queued messaging, and command handling on mesh channels. It is a daemon, not firmware or a library. - License: in-house (our own developer) — no third-party license blocker.
- Hardware/transport reality: it rides Meshtastic serial + a local
Ollama. Our radio is Meshcore (Heltec V3) and our mesh stack targets
meshcore. The
meshtasticmodule does NOT speak meshcore, so the script cannot drive our radio unmodified. - Decision needed (architecture): per user, integration must work with
meshcore. Two seams:
- (a) Lift Meshroller's behaviors (LLM bridge, trusted-node auth, scheduled messaging, command parser) into our Rust mesh stack as typed message kinds — native to meshcore, no Python/Meshtastic dependency. Preferred for meshcore.
- (b) Package the Python daemon as a container app and add a meshcore serial
backend to it (keeps the script, but requires writing meshcore I/O the
meshtasticmodule doesn't provide). This choice is the remaining gate; the rest of Phase 1 below stands.
Phase 1 — choose the seam
- Our mesh stack:
core/archipelago/src/mesh/(mod.rsMeshService,listener/,protocol.rs,types.rs). Decide:- If Meshroller is a protocol/feature on the same radio → implement it as a
typed message kind in our
MeshMessageType+listener/dispatch.rs(mirrors how block headers / alerts are handled). - If it's a separate transport/daemon → wrap it behind our transport router
(
transport/) like FIPS/LAN/Tor.
- If Meshroller is a protocol/feature on the same radio → implement it as a
typed message kind in our
- Reuse the event seam (
MeshEvent) so the UI gets pushes (same path we just wired for #48).
Phase 2 — UX (ties into project_mesh_telegram_plan)
- A dead-simple onboarding + usage flow in the Mesh tab. Define the 1–2 killer actions and design the setup wizard.
Verify: 2 radios (the .116 Meshcore + a second).
Effort: multi-day; gated on the Phase 0 review + a license/architecture decision.
#15 — netbird app doesn't work (LOW PRIORITY)
Status (2026-06-17): DIAGNOSED LIVE on .198 + FIXED (option A shipped); login works. THE real blocker: the dashboard needs a secure context —
window.crypto.subtle is unavailableover plain http, so OIDC PKCE threw before login. Fix: proxy now serves HTTPS (self-signed cert at install,8087:443, all originshttps://); frontend opens netbird in a new tab (self-signed-HTTPS iframe is blocked). Layered fixes also instacks.rs: nginxresolver <gateway>+ variable upstreams (IP-cache 502;resolver local=on/${NGINX_LOCAL_RESOLVERS}FAIL on nginx:1.27-alpine), LAN-IP canonical origin + CORS + multi-origin redirect URIs,/nb-auth+/nb-silent-authSPA fallback (were 404), and a stale-store note (wipe to re-init). Also found:conmon diedzombie containers (recreate fixes; #53). Validated on .198, registration+login succeed. Trusted-cert/iframe (option B) = #56; registry-app migration = #52. Existing nodes need a clean reinstall.
Diagnose first (likely a container/config issue, like other app fixes):
- On a node:
podman logs <netbird container>— capture the actual failure. - Check the app manifest + install path (
container/install, env, ports, the four iframe-sync places per memoryfeedback_gitea_iframe_setupif it has a UI). - netbird needs a management URL / setup key — confirm whether the app expects config we don't provide, or a host capability (TUN device / NET_ADMIN) the rootless-podman setup lacks.
Likely fix: either supply the missing env/setup-key UI, or add the required container capability. Low priority — schedule after the above.
#43 — Install errors at DID-creation + password screens (.70); FIPS slow
.70 is unreachable, so we can't read its logs. Code-side hardening that helps
regardless:
Status (2026-06-17): hardening DONE & compiles. Root cause was a non-idempotent
seed.generatethat overwrote node keys under the client's retry storm on slow first boot. Fixed: idempotent generate + retry-safe verify (seed_rpc.rs), transient-vs-genuine error handling inOnboardingSeedGenerate/Verify.vue, and a non-blocking FIPS status onOnboardingDone.vue. Issue closed; full closure wants a fresh install on a reachable node + re-test on .70.
- Onboarding error surfacing — in the seed/DID + password onboarding views
(
OnboardingSeed*, the password step) and their RPC handlers (seed.generate/seed.verify/auth.setup), make a successful operation never show an error toast, and make genuinely-failed ops show the real message + a retry — so cosmetic errors (op actually succeeded) stop alarming users. Audit the promise/catch paths for races where a slow backend resolves after a timeout fires. - FIPS start delay — confirm
spawn_post_onboarding_fips_activate(api/rpc/seed_rpc.rs) isn't blocking onboarding; it already runs detached. Consider surfacing "FIPS starting…" status instead of letting it look stuck.
Verify: a fresh ISO install on a reachable node (.198 or a scratch box), watch the DID + password screens; then re-test on .70 once reachable.
Effort: small–medium (the hardening); full closure needs a repro node.