The kiosk image ships no color-emoji font, so the wallet card's 🥜 and ⚓
drew as empty squares on TVs. Swap them for inline stroke SVGs matching
the neighbouring rows (wallet card + scan modal), and add
fonts-noto-color-emoji to the ISO package set so remaining emoji across
the UI (peer files, chat, content) render on future installs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
App.vue skipped starting the browser-side remote relay on kiosks, citing a
system-level xdotool injector that no longer exists — the backend's
remote-input path is relay-only (validate + broadcast). With no consumer,
companion input was silently dropped on TVs. Start the relay on kiosks
like everywhere else; rides the next release + ISO via the web bundle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two gaps kept the wallet QR scanner camera-less outside a desktop browser:
- Companion app: the WebView's default WebChromeClient silently denies
getUserMedia. Both WebViews (main + in-app overlay) now implement
onPermissionRequest — granting video capture only, requesting the
app-level CAMERA permission on first use (manifest already declares it).
- Plain-http origins (mobile web/PWA on a LAN node): browsers hide
navigator.mediaDevices entirely, no permission can bring it back. New
"Take photo of QR" fallback uses <input capture=environment> — the
native camera needs no secure context — and decodes the shot locally
with qr-scanner's scanImage. Live preview still used when available.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All Zeus channel surfaces now read: "Open a channel with Zeus Olympus
node and start sending and receiving Lightning payments. Minimum
150,000 · maximum 1,500,000 on-chain sats required."
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AIUI persists aiui-theme in localStorage and a stored value beats its
prefers-color-scheme fallback, so kiosk profiles from before the launcher
forced a dark color scheme keep white panels forever. Kiosk boot now pins
the same-origin key to dark, healing existing profiles on next load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The onboarding-era :global(html.kiosk-mode .view-wrapper) transform:none
rule also matched the dashboard's route wrappers, freezing every list<->
detail depth transition into a jump cut on kiosk. Scope that rule to the
onboarding viewport and give the dashboard kiosk-safe 2D equivalents
(scale + opacity, no translateZ/blur/preserve-3d) that the software
compositor can animate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
system.kiosk-display.get/set RPCs write /etc/archipelago/kiosk-display.conf
(sourced by the kiosk launcher) and try-restart the kiosk so the choice
applies immediately. The Settings section only appears on nodes that have
the kiosk unit installed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zero-amount BOLT11 invoices need the payer to supply the amount; forward
it to LND REST as the amt field. The wallet scan modal unlocks its amount
field for such invoices and passes the user's entry through.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
html.kiosk-safe-area set background:#000 on the root element. With a root
background, body's background stops propagating to the canvas and paints
as a normal block background instead — which by CSS paint order covers
negative-z-index descendants. The dashboard/login art lives in
.bg-perspective-container at z-index:-10, so every kiosk display showed
solid black behind the UI while desktop browsers (no root background)
showed the art. Verified live on the Framework 4K TV via CDP: making the
root transparent restores the art instantly. Keep the black on body only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New WalletScanModal scans QR codes with the device camera (BarcodeDetector
with jsQR fallback) including animated/multi-frame QRs via qrloop, then
routes what it saw: BOLT11 invoices -> lightning pay (amount locked from
the invoice when present), bitcoin:/BIP21 URIs -> on-chain send, Cashu
tokens -> redeem, Fedimint invites -> join. Entry points: camera button on
the wallet card (far right), and a Scan button centered between the
Close/Send and Close/Receive buttons of both modals.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The kiosk window was sized in physical pixels, but Chromium interprets
--window-size in DIPs: with any device-scale > 1 the window painted
scale-times larger than the panel, and with no window manager in the
kiosk X session --kiosk/--start-fullscreen could not snap it back. On a
4K TV at scale 3 that meant an 11520x6480 window — 9x the panel area —
which blew the compositor tile-memory budget ("tile memory limits
exceeded, some content may not draw") and froze the screen on the last
painted frame; smaller scales cropped content off the right/bottom.
- Size the window in DIPs (panel / scale) so DIPs x scale = the panel.
- Panels >=2560 wide now target a 1920-wide layout (4K -> scale 2.0,
validated on the Framework's 72" 4K TV: spacious desktop layout at 2x
sharpness); smaller panels keep the 1280 target (1080p TV -> 1.5).
- Optional ARCHIPELAGO_KIOSK_MAX_WIDTH cap: drop to the best <=N-wide
xrandr mode for weak hardware (TV upscales instead of us rasterizing).
- Source /etc/archipelago/kiosk-display.conf so the upcoming Settings
display-resolution picker can manage these knobs per node.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The RF section shipped as four bare fields; restore a prepopulated plan
list (dropdown) with Custom unlocking free entry, per operator direction.
Preset labels carry the full values, so the number fields render only in
Custom mode — no duplicated display. Presets limited to verifiable
sources: the repo's MeshCore community plans (EU 868/433), the MeshCore
firmware's own build defaults (platformio.ini arduino_base 869.618/62.5/
SF8 CR5; companion example 915.0/250/SF10/CR5), and the operator-attested
Portugal plan (869.618/62.5/SF8/CR8). Stored params are matched back to a
named preset on load; hand-editing flips the dropdown to Custom.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The launcher must be a secure context for Web Bluetooth, but the UI opens local
apps as http://host:port (resolveAppUrl), so an https-only launcher (v1.1.0)
would break the Open button with a TLS mismatch. Serve http on 10380 (the Open
target) that 301-redirects to the https listener on 10381, so the new tab lands
on https where navigator.bluetooth is available. Bump pine 1.1.0 -> 1.1.1;
re-sign the catalog (only the pine entry changes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the launcher's text instructions with the real Improv-over-BLE
Web-Bluetooth provisioner (a "Connect Pine to WiFi" button, SSID/password
fields, live log) ported from the pine repo. Web Bluetooth needs a secure
context, so the launcher now serves HTTPS (self-signed): :80 (published 10380,
the Open target) 301-redirects to :10381, and open_in_new_tab keeps BLE out of
an iframe. Bump pine 1.0.0 -> 1.1.0.
Regenerate + re-sign releases/app-catalog.json (only the pine entry changes vs
v1.7.109) so nodes reconcile the button page live. Signed by the release-root
key did🔑z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersedes the v1.7.108 hold now that the manifest is signed and the
release assets are being published.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A prep commit accidentally carried the in-progress v1.7.109 manifest onto
main, so nodes saw an unsigned 1.7.109 with download URLs that 404. Restore
the signed v1.7.108 manifest until the v1.7.109 publish flow completes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The release gate requires ≥3 curated changelog bullets; the Pine app is
user-facing and deserved one anyway. Carries the version bumps and manifest
from the aborted cut so the tree is clean for the re-run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pine catalog regeneration (c1dfc667) refreshed generatedAppSessionConfig
from the manifests, where did-wallet publishes host port 8088 — the test's
8083 expectation was stale. The test asserts manifest-generated ports are
used, so it must track the manifest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Device panel gets MeshCore RF fields (frequency MHz, bandwidth kHz, SF, CR)
wired to mesh.configure's validated lora_radio_params; shown only for
MeshCore radios, all-empty leaves the radio's flashed settings untouched.
Human units in the UI, firmware field units on the wire. Per operator
direction the panel's static stat rows duplicating editable fields (name,
region, channel) are dropped — the editable control is the display. Stale
'adjust via a MeshCore client' hints updated: the node programs the radio now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regenerate + sign releases/app-catalog.json (65 apps) with the pine,
pine-whisper and pine-piper manifests embedded, so nodes can install the Pine
stack straight from the signed registry (raw URL on main). Signed by the
release-root key did🔑z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Archy set no radio params on Meshcore devices — SF/CR/BW/freq lived only in
device firmware, so a radio on the wrong preset could not be fixed from the
node (it hears RF energy but demodulates nothing; the fleet hit exactly this
on an RNode: docs/RETICULUM-TRANSPORT-PROGRESS.md item 4).
- protocol.rs: build_set_radio_params — wire format verified against the
MeshCore companion firmware handler (CMD_SET_RADIO_PARAMS=11:
[11][freq:u32 LE MHz*1000][bw:u32 LE kHz*1000][sf][cr]); byte-layout test
pinned to the Portugal preset 869.618 MHz / 62.5 kHz / SF8 / CR8.
- serial.rs: MeshcoreDevice::set_radio_params (device reboots on OK).
- MeshConfig.lora_radio_params: Option — None (default) leaves the radio
untouched, so only explicitly-configured deployments are affected.
- session.rs: provision on connect, gated on a persisted last-applied marker
(SELF_INFO offsets shift across firmware versions) + the same attempt cap
as region/channel so a refusing radio never reboot-loops.
- mesh.configure RPC: lora_radio_params arm with firmware-range validation.
mesh tests: 114/114 pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- catalog.json (app-catalog + neode-ui/public): one "pine" entry, home
category; engines stay out of the catalog
- appsConfig.ts: pine → home category, pine-whisper/pine-piper → SERVICE_NAMES
(render in Services, not as extra cards), pine-* icon fallback
- generatedAppSessionConfig.ts: regenerated (pine port 10380 + titles +
open-in-new-tab), via scripts/generate-app-catalog.py
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register "pine" as a manifest-driven stack (like netbird) across every
lifecycle site so install/stop/start/restart/uninstall/crash-recovery treat
the launcher + two Wyoming engines as one app:
- stacks.rs: pine_stack_app_ids() + install_pine_stack() (orchestrator-first,
adopt existing, else bail — no hardcoded fallback)
- install.rs: dispatch package_id == "pine" to the stack installer
- app_ops.rs: canonical stack-member table + owning_package STACKS
- dependencies.rs: startup_order + needs_archy_net
- config.rs: all_container_names
- crash_recovery.rs: auto-start members + a StackRecoverySpec (anchor
pine-whisper) so a genuinely-installed stack self-heals but orphan debris
does not crash-loop
- docker_packages.rs: exclude the engines from the apps list
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Package the PineVoice/Wyoming local voice assistant as ONE store app "Pine"
that runs three rootless containers:
- pine-whisper — Wyoming faster-whisper STT (docker.io/rhasspy/wyoming-whisper
3.4.1, model base-int8/en), publishes :10300
- pine-piper — Wyoming Piper TTS (2.2.2, voice en_GB-alba-medium),
publishes :10200
- pine — nginx launcher serving a self-contained setup/status page
(WiFi provisioning + Home Assistant wiring instructions), the Open target
The two engines publish their Wyoming ports so Home Assistant reaches them via
host.containers.internal. All rootless (cap-drop=ALL, no_new_privileges),
manifest-driven — no hardcoded podman/installer. The engines are internal stack
members (added to the drift-check INTERNAL_MANIFEST_IDS allowlist); only "pine"
carries a catalog entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersedes the unpublished v1.7.107. Covers the multi-anchor FIPS fix, WiFi
self-heal, mesh fast-rejoin, kiosk TV-scaling, kiosk dark theme / VT hints /
UTF-8 logo / power-button hardening, and the ISO build fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three fresh-install kiosk issues found on a Framework node:
- MOTD said 'Ctrl+Alt+F7 for kiosk' / 'F1 for terminal', but the kiosk's
Xorg runs on vt1 — so F7 was an empty black VT (the reported black screen
and the failed 'return to kiosk'). Corrected: kiosk=F1, terminal=F2.
- The UTF-8 block-glyph logo corrupted intermittently because returning from
the kiosk can leave the console VT out of UTF-8 mode; force it with ESC%G.
- A short power-button press shut the node down instantly. Ignore the short
press, keep long-press poweroff, so accidental brushes don't kill the node.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nodes depended on a single public anchor (185.18.221.160) that is
unreachable from most real networks — confirmed on the thinkpad WiFi and a
fresh Framework node, both of which islanded (is_root=true, depth=0). Its
DNS even resolves IPv6-first while the daemon is IPv4-only.
Stand up an Archipelago-operated FIPS anchor on vps2 (146.59.87.168, the
OTA host every node already reaches) and ship it as an additional default
anchor. default_public_anchor() -> default_public_anchors() -> Vec; load()
returns the set; the apply loop already dials each, so whichever the node's
network can reach wins. Verified end-to-end: a fresh node joined the tree
via vps2 in 3s (depth 1). anchors tests: 7/7 pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AIUI themes via @media (prefers-color-scheme) and falls back to its light
variant (white panels) when the browser reports no preference — which the
minimal kiosk X session does, so AIUI came up light instead of its designed
dark. The main UI hardcodes dark and is unaffected. Push two independent
dark signals on the kiosk Chromium: GTK_THEME=Adwaita:dark (version-
independent, can never force light) and --blink-settings=preferredColorScheme=0
(0 = kDark in modern Chromium). Needs on-device kiosk verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two fleet fixes that OTA can deliver (unlike the ISO-only polkit rule):
- WiFi (issue #99): nodes drive NetworkManager from a system-level service
with no logind seat, so the stock polkit rule denies them and Wi-Fi setup
fails with 'Insufficient privileges'. Fresh ISOs since 2026-05 ship the
scoped rule, but OTA'd nodes never got it (OTA replaces binary + web UI,
not host system config). Add a startup self-heal that installs the rule if
missing and best-effort ensures polkitd — both wrapped so an offline/locked
apt can never fail startup; the rule is written regardless so it activates
once polkitd lands. Idempotent on the rule's unique subject.user marker.
- FIPS: regenerating fips.yaml (this build does, once, on first boot after
the OTA) restarts the fips daemon; for a few seconds /run/fips/control.sock
is gone and every seed-anchor dial fails, islanding the node until the next
5-min tick. Detect that exact failure (all dials fail on control.sock) and
retry in 15s instead — bounded to 8 fast retries/episode so a node with no
fips daemon falls back to the steady cadence rather than busy-looping.
FIPS unit suite: 33/33 pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The relay image was rebuilt to the standard nostr-rs-relay layout — WORKDIR
/usr/src/app, execs ./nostr-rs-relay — but the build still cp'd from the old
/usr/local/bin path, so extraction silently produced nothing. Once the
missing-binary guard was added, that silent miss became a hard build
failure. Verified the binary is a valid amd64 ELF (GLIBC 2.34, under Debian
13's 2.40) at /usr/src/app/nostr-rs-relay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ISO build hard-failed at 'Required binaries not extracted: nvpn'
because it still pulled 146.59.87.168:3000/lfg2025/nostr-vpn:v0.3.7 — an
image that was deleted from the registry when NostrVPN was removed from
the product. The native nvpn daemon is already masked here (ln -sf
/dev/null nostr-vpn.service) and is never spawned at runtime (core vpn.rs
manages WireGuard/Tailscale, only reading a legacy nvpn config as
fallback), so extracting its binary was dead weight that bricked every
build once the image went away.
Drop the nvpn extraction and remove it from the missing-binary guard.
nostr-rs-relay stays required — its nostr-relay unit is still enabled and
the image still pulls. Change is in STEP 3, so the cached rootfs is
unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ISO built FIPS from an unpinned --depth 1 clone of upstream main, so
every build shipped whatever happened to be on main that day. Pin to
v0.4.1 via a FIPS_VERSION build arg — the version fips/config.rs renders
its typed config against, and the one validated in the field on .198 and
the thinkpad. The pin lands inside the STEP 1 recipe-hash range, so the
cached rootfs correctly invalidates on the next build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captures the three commits this release carries, the FIPS 0.4.1 upgrade
state (2 nodes done, fleet not rolled), the peer-files diagnosis including
what is explicitly NOT explained, the open decisions not taken, and the
exact release + ISO ritual.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon config was built by format!-ing a YAML string literal. Upstream's
config structs are #[serde(deny_unknown_fields)], so a key we get wrong does
not degrade — the daemon refuses to start and the node drops off the mesh.
String-built config made that a runtime discovery on a live node.
Replace it with a typed struct tree serialised via serde_yaml, verified
field-by-field against jmcorgan/fips v0.4.1, plus an exact-output snapshot
test so schema drift fails in CI rather than at boot. Also adds tests for
determinism (server.rs compares the render against disk to detect drift, so
instability would cause a reinstall+restart loop) and for the mDNS key path.
Enables node.discovery.lan.enabled (mDNS/DNS-SD, added upstream in v0.4.0)
so co-located nodes peer directly instead of depending on the public anchor
being reachable — an anchor blackhole on one segment currently islands a node
completely. Emitted unconditionally rather than version-gated: v0.3.0's
DiscoveryConfig has no `lan` field and no deny_unknown_fields, so a v0.3.0
daemon ignores it harmlessly and it activates on upgrade with no second
config migration.
Note: the first startup after this lands renders a config that differs from
disk, so the existing drift check reinstalls it and restarts the daemon once.
That is the intended self-healing path and settles immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RPC failures logged only the top-level anyhow context, so a peer-files
failure produced exactly "RPC error on content.browse-peer: Failed to
connect to peer" with the real cause discarded. That made it impossible
to tell a dead peer from a slow Tor circuit from a FIPS resolve failure
without reproducing by hand.
Switch the log line to `{:#}` so the whole context chain is rendered.
The client-facing message still uses `{}` through sanitize_error_message,
so no internal detail is leaked to callers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The peer-files header title block is `hidden md:block` (the global header
carries the peer name on mobile), which also hid the transport pill nested
inside it — so mobile users browsing a peer's files had no indication of
whether they were on FIPS or Tor.
Render a `md:hidden` pill alongside the peer icon so the transport is
visible at every width, without duplicating the peer name on desktop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>