Merge remote-tracking branch 'origin/main' into archy-hwconfig

This commit is contained in:
2026-07-23 21:40:06 +00:00
89 changed files with 3564 additions and 359 deletions
@@ -0,0 +1,56 @@
# HANDOFF — deploy companion APK 0.5.1 (vc21) to nodes
**For: the agent on archi-dev-box.** User-reported failure this evening:
pairing flow on Framework PT — downloaded the companion from the node's
QR, then the pairing scan didn't work. The APK the node serves predates
today's scanner fixes; the pipeline below gets the fixed build into the
user's hands.
## What changed on main today (all merged)
- **Pairing-scanner fix** (`QrScannerOverlay.kt`): ZXing decode attempts are
frame-gated (~7/s, was every frame — the CPU contention made the preview
stutter badly enough to never decode) and PreviewView uses TextureView (no
more black flash on open). This is the likely fix for "doesn't scan".
- Three-finger menu gesture (was two-finger, collided with scroll) + one-time
teaching overlay ~2 min after login.
- Native wallet QR scanner behind `window.ArchipelagoQr` + WebView file-chooser
support; web scan modal hands live scanning to it.
- npub-keyed saved servers (pairing contract item 1, PR #106).
- Served APK refreshed: `neode-ui/public/packages/archipelago-companion.apk`
is now **0.5.3 / versionCode 23**. On top of the 0.5.1 scanner fixes it
guarantees dual-path peering — the node's LAN endpoint (direct p2p, npub-
keyed dial hints) AND the Archipelago public anchor (vps2, baked into the
app so even an old node's QR can't leave the phone LAN-only) — and fixes
the two field failures from the user's 5G test (screenshots, 21:54):
- **Mesh VPN no longer kills the phone's internet** — the IPv6-only TUN
never called `allowFamily(AF_INET)`, so Android blocked all IPv4 while
the mesh was up. Now allowed (+ `allowBypass`).
- **Off-LAN connect works** — `connect()` no longer hard-fails when the
scanned LAN IP doesn't answer; it brings the mesh up and probes the
node's ULA (`meshIp`) with retries before reporting failure.
## What to do
1. Redeploy the web-ui bundle from current main to the active nodes —
web root `/opt/archipelago/web-ui/` (NOT a neode-ui/ subfolder), all
nodes the user pairs against, at minimum the one Framework PT scans.
2. Verify the served artifact really updated:
`curl -sI http://<node>/packages/archipelago-companion.apk` — size should
change (~27 MB build of 2026-07-23), or pull it and check
`aapt dump badging` shows `versionCode='21' versionName='0.5.1'`.
3. The demo stack gets its images from CI (run 100 pushed today with the new
web bundle) — confirm the Portainer stack re-pulled, or trigger its
redeploy, so the demo QR also serves vc21.
4. **Node side is half the 5G story**: away-from-home reachability needs the
NODE connected to the public anchor too. On Framework PT (and any test
node): deploy current main (node-side npub-first `fips.pair-info`), then
verify `sudo -n fipsctl show status` reports the anchor connected —
`fips.reconnect` RPC if not. A phone can dial the anchor perfectly and
still fail if the node never enrolled with it.
5. Re-test the user's exact flows with vc23 (updates any older install in
place): (a) pair ON the LAN, then switch the phone to 5G — the UI must
come up via the mesh ULA; (b) pair while ALREADY on 5G (never on the
node's LAN) — scan, VPN consent, and the connect must succeed through
the anchor. If either fails, capture `adb logcat` around the attempt and
`fipsctl show sessions` on the node, and report back.
+33 -1
View File
@@ -38,7 +38,7 @@ Query parameters:
| `fip` | no | Node's `fips0` ULA (IPv6). Once the phone is meshed, the node's UI stays reachable at `http://[<fip>]` from anywhere — this is the remote-access address (replaces the old WireGuard 10.44.0.1 flow). |
| `fhost` | no | Host the phone's embedded FIPS dials (same host `url` resolved to). |
| `fudp` / `ftcp` | no | Mesh transport ports on `fhost` (currently 2121/udp and 8443/tcp). |
| `fanchors` | no | Comma-joined `npub@host:port/transport` rendezvous anchors (the node's seed-anchor list, capped at 4). The phone peers with these too so it can route to the node via the public mesh when the direct endpoint is unreachable (away from home / NAT). |
| `fanchors` | no | Comma-joined `npub@host:port/transport` rendezvous anchors, capped at 4. **The FIRST entry is the paired node itself** (`fnpub` at its current LAN host — the addr is a dial *hint*, the npub is the identity). The remaining entries are the node's seed anchors, and the node guarantees the Archipelago public anchor (vps2, `146.59.87.168:8444/tcp`) is present even if the operator trimmed their own list — so the phone can always rendezvous through the public mesh when the LAN endpoint is unreachable (away from home / NAT). |
Examples the web UI actually emits:
@@ -87,6 +87,38 @@ Examples the web UI actually emits:
remote access = the node's fips0 ULA (`fip`), which the WebView falls back to
automatically when the LAN address stops answering.
## npub-first connectivity (2026-07-23 — REQUIRED app-side changes)
The QR used to be effectively IP-first: the app connected to `url`/`fhost` and
broke as soon as the LAN renumbered or the phone left home. FIPS peers on
**npubs**; IPs are only dial hints. The app must treat them that way:
1. **Identity = `fnpub`.** The saved server entry is keyed by the node's npub
(fall back to origin only when the QR has no FIPS params). Re-scanning the
same npub updates the entry even if every address changed.
2. **Peer with the node itself as the first anchor** (`fanchors[0]`): on LAN
this is direct p2p over FIPS (mDNS/known-endpoint dial via archy-fips-core
v0.4+, which discovers LAN peers without any IP pinning), so it keeps
working after DHCP renumbering.
3. **Peer with the public anchors too** (remaining `fanchors` entries — the
Archipelago vps2 anchor is always included by the node): away from LAN the
phone routes to the node's npub via the public mesh and reaches the UI at
`http://[<fip>]` (the fips0 ULA).
4. **Address selection order** for the WebView: LAN `url` when it answers →
ULA `fip` over the mesh otherwise. Never hard-fail because the scanned LAN
IP stopped existing.
(Node-side counterpart shipped 2026-07-23: `fips.pair-info` always includes
the vps2 public anchor, and the web UI prepends the node's self-anchor to
`fanchors`.)
App-side status: items 2/3 were already covered by `FipsPreferences.
upsertNodePeer` (npub-matched peers, self-anchor dedup) and item 4 by the
WebView's `meshFallbackUrl` retry. Item 1 shipped 2026-07-23: `ServerEntry`
carries `npub` (trailing serialization field, legacy entries still parse) and
`ServerPreferences` matches saved/active entries via `sameNode` — npub first,
address/port/scheme only as the LAN-only fallback.
## Testing checklist (app side)
- [ ] Scan demo QR from https://demo.archipelago-foundation.org → auto-connected demo session.
+84
View File
@@ -0,0 +1,84 @@
# TV input: keyboard/gamepad inside iframe apps — design
**Goal (2026-07-23, user requirement):** on a TV/kiosk node, keyboard and
gamepad control must work *inside iframe apps* (IndeeHub, Jellyfin, fedimint
UI, AIUI…), easily and globally — no per-app hacks.
## What already exists
- `neode-ui/src/composables/useControllerNav.ts` — a complete spatial-nav
system for the shell: reads gamepads (`navigator.getGamepads`), moves focus
between `data-controller-container` regions, plays nav sounds. It stops at
iframe boundaries: nothing is forwarded into frames.
- Keyboard focus DOES enter iframes natively (click/Tab into the frame), and a
focused iframe receives all keys — same- or cross-origin. The gap is
gamepad→app and deliberate focus handoff shell↔frame.
- Kiosk Chromium is our process (archipelago-kiosk-launcher), X11, and the ISO
already ships `xdotool`. Most app iframes are **cross-origin**
(`http://host:port`), so shell-side script injection is impossible for them;
only the nginx-proxied `/app/...` ones are same-origin.
## Recommended architecture — two layers, both global
### Layer 1 (OS, kiosk nodes): gamepad → virtual keyboard, kernel-level
A small host daemon (`archipelago-gamepad-keys`) on kiosk nodes:
- reads game controllers via evdev (`/dev/input/event*`, capability
BTN_GAMEPAD), hotplug-aware (udev monitor or 5s rescan — same pattern as the
audio router);
- emits a **uinput virtual keyboard**: D-pad/left-stick → arrow keys, A →
Enter, B → Escape, X → Space (play/pause), Y → `f` (fullscreen in most
players), shoulders → Tab / Shift+Tab, Start → Enter, Select → Escape;
- ships exactly like the audio router: `image-recipe/configs/` script + unit,
spliced into the ISO, `include_str!` self-heal in `bootstrap.rs`, gated on
the kiosk being installed. The `archipelago` user is in `input` group OR the
unit runs as root (uinput needs it anyway — run as root, it's ~100 lines of
evdev→uinput with no network).
Why this layer wins: the browser sees a real keyboard, so **every iframe —
any origin, any app — just works** the way it does for a physical keyboard
today. Video players, web games, AIUI: all of them already have keyboard
bindings. Zero app cooperation, zero web-platform security fights.
### Layer 2 (shell): deliberate focus handoff into/out of frames
Small extension to `useControllerNav`:
- When spatial nav selects an app-session container and the user presses
A/Enter: call `iframe.focus()` (works cross-origin) — keys (real or
virtual) now flow into the app.
- A dedicated **exit chord** the daemon maps from the gamepad (e.g. Home
button → F12 or a rarely-used key): the shell listens with a *capturing*
window listener; on seeing it, `iframe.blur()` + return focus to the shell
nav. Keyboard users get the same via a documented chord (e.g. long
Escape / Ctrl+Escape — plain Escape stays with the app, players use it).
- Same-origin frames (the `/app/...` proxied set) can additionally get the
full spatial-nav treatment by running the existing nav over
`iframe.contentDocument` — nice-to-have after the layers above land.
### Optional layer 3 (per-app polish): postMessage contract
For OUR app UIs only (AIUI, fedimint, launcher pages): a tiny
`archipelago:input` postMessage contract for semantic actions (back, home,
context-menu) where raw keys aren't expressive enough. Documented in the app
packaging docs; never required for an app to be usable.
## What NOT to do
- ❌ CDP (`--remote-debugging-port` + Input.dispatchKeyEvent): works but adds
a privileged debug port to the kiosk and a daemon↔browser coupling; the
uinput route gets the same result at kernel level with no attack surface.
- ❌ Per-app nav scripts injected into iframes: cross-origin makes this
impossible for most apps, and it's exactly the per-app hack the requirement
rules out.
## Implementation order
1. `archipelago-gamepad-keys` daemon (evdev→uinput, ~python3 stdlib or small
Rust bin) + unit + ISO splice + bootstrap self-heal. Test on Framework PT
with any USB/BT controller.
2. `useControllerNav`: A-button → `iframe.focus()` on the focused app session;
exit-chord capture listener to reclaim focus.
3. (Later) same-origin spatial nav inside `/app/...` frames; postMessage
contract for our own app UIs.