- SendBitcoinModal crashed AT MOUNT on device ('Cannot access R before
initialization'): the invoice-first block's watch() evaluates its
computed source at setup, which read effectiveMethod while still in
its temporal dead zone (declared 20 lines later). Send button did
nothing and the failure poisoned sibling modal data. Block moved below
effectiveMethod, with a comment so it can't drift back.
- Cold start injected 0px safe-area vars (onPageFinished can beat window
attachment; rootWindowInsets null) — UI lost top/bottom margins and
the tab bar sat in the gesture zone eating taps. Injection extracted
to injectSafeAreaVars(), re-fired from an OnApplyWindowInsetsListener
when real insets arrive, on page finish, and on reattach.
- MeshLoadingScreen wears the proper brand: pixel 'a' in the black
circle-container + F*CK IPS MESH wordmark.
Served APK: 0.5.10 (vc30). Reproduced+diagnosed live on-device.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything from tonight's remote field testing:
- Back-to-dashboard fixed: JS bridges on the retained WebView delegated
through live-composition callbacks (stale closures made apps refuse to
launch after remote ⇄ dashboard), and reattach forces a layout pass
(top/bottom UI was wrong until a tap).
- F*CK IPs MESH branded loader: full-screen on relaunch (startup race)
and during the first connect after scanning a node QR.
- Party 'Share this app' is now a QR of the public vps2 download link
(scan with any camera → install over any internet); direct APK
file-share kept as a secondary action.
- Mesh party pairing is MUTUAL: scanner announces itself to the scanned
phone's Flare /hello — both sides get the peer, a chat entry and a
'👋 joined the party' message; scanner auto-opens the chat.
- Party scanner asks for CAMERA permission (fresh installs/reinstalls
landed on a black preview).
- Node: device tokens mint unique companion-<id> names — pairing a
second phone no longer revokes the first phone's login (the source of
'reconnecting a lot' and the second phone's failure).
Served APK: 0.5.9 (vc29).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WEB_VIEW route recreated the WebView on every return from the
remote screen — full reload + re-login every time. The kiosk WebView is
now retained in a holder and reattached live (no race, no reload);
clients/bridges/listeners are re-bound to the new composition on
reattach, and the instance is dropped on retry, disconnect, or server
change so errored/stale sessions still reload for real.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Party screen 'Share this app': shares this install's own APK through
the system sheet (FileProvider over cache/share/), so a nearby friend
gets the companion via Quick Share/Bluetooth with zero internet — the
party-mode premise end to end.
- Intro screen: Mesh Party button under Get Started — party works with
no node at all, so it belongs on the first screen a fresh install
shows.
- Served APK: 0.5.8 (vc28), ready for the QR/OTA/ISO cut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Combined session state from both agents (shared checkout):
- Mesh Party / Flare (experimental): phone-to-phone FIPS via party QR,
Flare chat/beam on :5680, optional fixed UDP listen (2121); party-only
phones get the baked-in Archipelago anchor so two bare phones reach
each other across the internet, not just on a hotspot.
- Keep-warm mesh: a running healthy node survives app opens (restart
only when dead or peers changed — peersDirty); requestMeshRestart for
config changes. Cold start measured on-device: session in ~3.1s.
- Restored after regression: startup LAN-vs-mesh race (the party work
was based on a pre-#114 file copy) and same-node routing (BTCPay/
Pine/HA had returned to opening in the external browser).
- UI: top status-bar scrim removed (bottom-only bars); web app-session
mobile bar is solid black — app theme colour no longer bleeds through
the bar and its safe-area strip.
Served APK: 0.5.7 (vc27).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the app-side recommendations from the node diagnosis
(HANDOFF-2026-07-23): first session through the public tree takes 15s+
when it works at all, far beyond the old ~8s probe window.
- connect(): mesh ULA probed inside a 60s budget, 15s per-phase timeouts.
- ArchyVpnService: session warmer probes every saved node ULA as soon as
the tunnel is up (5s cadence first minute, then 60s keep-warm) so the
UI hits a warm session and it never idles out.
- Served APK: 0.5.5 (vc25); handoff updated with 25-ping evidence that
public-tree discovery currently fails outright (supports the
private-tree infra decision).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On-device diagnosis (phone on adb, 5G): mesh sessions established but no
packet ever entered the tunnel — Android hands the VpnService TUN fd over
non-blocking, and the fips fork's dedicated blocking-read thread treats
EAGAIN as fatal, dying at startup. archy-fips-core now forces the fd into
blocking mode before Node::start_with_tun_fd. Verified on-device: reader
survives, packets flow into the mesh, and the 30s anchor-link flap is
gone (stable 8+ min on 5G).
Also setMetered(false): Android 10+ defaults VPNs to metered, flipping
the phone into data-restricted behaviour whenever the mesh is up.
Served APK: 0.5.4 (vc24). Handoff updated with the remaining node-side
blocker: phone->anchor works, phone->node ULA gets no reply — needs
fipsctl introspection on Framework PT (suspect fork/daemon session or
routing mismatch).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whole point of FIPS: the phone peers with the node's npub using the
LAN endpoint as a direct p2p dial hint AND rendezvouses via a public
anchor when away — neither LAN nor 5G location may matter. The anchor
half only existed when the scanned QR carried fanchors, so pairing
against an older node left the phone LAN-only and everything died on 5G.
The Archipelago vps2 anchor (npub + 146.59.87.168:8444/tcp, lockstep
with core fips/anchors.rs) is now baked into upsertNodePeer as a
guaranteed peer. Mesh connect retry window widened so a first-ever
pairing survives the VPN consent dialog. Served APK: 0.5.3 (vc23).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field test on 5G (Framework PT): pairing scanned fine but connect
hard-failed on the scanned LAN IP, and enabling the mesh VPN killed the
phone's internet.
- ArchyVpnService: the TUN is IPv6-only, and Android blocks every
address family the VPN holds no address for — allowFamily(AF_INET)
(+ allowBypass) so normal traffic flows while only fd00::/8 is routed.
- ServerConnectScreen.connect(): when the LAN address doesn't answer and
the entry carries a mesh ULA, bring the tunnel up (autoStartIfReady)
and probe the ULA with retries before reporting failure — the scanned
IP is a dial hint, the npub/ULA is the identity (npub-first contract).
- Served companion APK refreshed as 0.5.2 (vc22); handoff doc updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The APK served from the pairing QR predated today's scanner fixes, so
the download-then-scan flow still hit the laggy scanner. Bumps the
version so phones update in place, refreshes the served artifact
(signed v1+v2+v3, verified), and adds the archi-dev-box deploy handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pine-openwakeword manifest (wyoming-openwakeword 2.1.0, :10400, /custom model
dir for the future Yo Archy model). Pine 1.3.0: launcher page gains a live
node-status card fed by /api/pine/status via a same-origin nginx proxy, and
copy for the new intents / Claude fallback / mesh announcements. Catalogs,
app-session config, drift ids regenerated; release catalog embeds 56
manifests (unsigned until the ceremony). Framework PT test plan in docs/.
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>
- 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>
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>
Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node)
gains a guided path to a working channel:
- New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing
its initial sync: while syncing it shows a live progress bar with a
counting-down time-remaining estimate; once synced it shows the on-chain
balance and a "Fund Wallet" button that opens the receive modal with a
fresh address, QR, and the Zeus channel limits (min 150,000 / max
1,500,000 sats) noted.
- When the sync finishes while a Lightning setup is mid-flight, a toast
pops with a "Finish setup" link straight back to the wizard (toasts now
support action links). If several setups are in flight, one is chosen —
the shared steps complete the lightning part of any of them.
- Channel steps are Zeus-branded (logo + copy) and land on the Lightning
Channels screen, which now carries an "Open a channel with Zeus" card
that prefills the open-channel modal with the Olympus peer URI, 150k
sats, and private-channel checked. "Get Zeus" links to zeusln.com.
- Setup completion now actually requires walking the manual steps (fund,
open channel, configure) — previously any step whose app was installed
was silently auto-ticked and running apps marked the whole goal done.
- Completion CTAs now go to the app you just set up ("Go to my shop
(BTCPay)" etc.) instead of the generic services list; iframe apps open
in the on-top app overlay.
- On-chain send modal gains a "Send all funds" sweep toggle, backed by
LND's send_all on the backend (amount no longer required when sweeping).
- Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress
contract the UI reads and simulates a ~90s IBD ramp per visitor so the
timer, toast, and fund flow can all be demoed live; channel list data
fixed (status/channel_point/liquidity totals — the panel previously
crashed on the missing status field); sendcoins supports send_all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Identities render as profile cards (banner, overlapping avatar, bio)
in narrow containers via container query; wide/desktop row untouched
- Demo identities get purpose-tinted SVG avatars (public/demo-avatars)
in both mock identity.list/get branches
- Node Visibility: description stacks under icon+title on mobile,
Refresh is a full-width bottom action on all sizes, discoverable
warning line removed, and the demo no longer exposes an onion address
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The App Store gains a "Your Node. In Your Pocket." hero banner for the
mobile companion app, in the same format as the featured-app banner, with
the app running on a phone mockup rising out of the right edge. Install
(or clicking anywhere on it) opens the Remote Companion download/pairing
modal — now openable on demand via a shared trigger instead of only
auto-showing once.
Also fixes the companion APK download on PWA installs: /packages/ now
bypasses the service worker's SPA fallback, which was answering the
download click with index.html instead of the file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SplashScreen no longer re-checks neode_intro_seen itself: it silently
skipped the whole logo-tap/typing sequence for any browser that had
seen the intro before, even when App.vue explicitly requested it (demo
fresh visits, Replay Intro). App.vue is the single decision point.
- Intro video re-encoded 12.7MB/12.6Mbps → 4.3MB/4.2Mbps (1920x1080 CRF23
faststart, muted track dropped) — visually identical on this
illustrated content; cache-buster bumped to v=8.
- Video is <link rel=preload>ed the moment the splash is scheduled (the
<video> element only mounts ~20s in, so cold caches used to start the
download mid-sequence and stutter).
- nginx demo cache regex now covers mp4/webm/mp3/webp/woff2 so repeat
visits play from browser cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ark tab in Wallet Settings (status, balances, receive address,
board/offboard, server config via wallet.ark-*), teal Ark badge in the
transactions modal, Ark row on the home wallet card (hidden until barkd
reports a balance), official bark icon, and wallet.ark-* mocks so the
public demo renders the tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plugging in a LoRa radio now pops a global two-step setup modal on any
page: step 1 shows the ACTUAL detected board (25 board SVGs vendored
from the Meshtastic web-flasher, GPL-3.0; matched by USB product string
on native-USB boards, vid:pid heuristics for bridge chips, animated
fallback otherwise); step 2 configures with Archipelago presets — the
LoRa region pre-selected from the node's location (new lat/lon→region
mapping), channel 'archipelago', node name, and an advanced firmware
pin. Options adapt per protocol: region+channel program Meshtastic;
MeshCore shows its community frequency plan (firmware owns RF); RNode
defers to the Reticulum daemon config.
Backend: mesh.configure now accepts lora_region (validated against the
driver's region table) and device_kind (probe pin); mesh.status returns
the persisted values plus per-port USB identity (sysfs vid/pid/product)
for board identification. The Mesh Device tab gains a full settings
form (region with duty-cycle/legality hints, firmware pin, channel,
name, identity broadcast) replacing the read-only panel; the old
Mesh-page-only onboarding modal is superseded by the global flow.
Mock backend: stateful mesh config so the dev UI demos the full
detect→configure round-trip (disable mesh → modal fires with Heltec V3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
484.kitchen, Arch Presentation, Call the Operator, Next Web News
Network, Syntropy Institute, and T-0 were hardcoded web-only bookmark
apps (no manifests/containers). Removed across the frontend app grid /
curated / marketplace / session / launcher data, the backend
AppMetadata table, their icons, and the nginx /ext/ proxy blocks
(external-app-proxies.conf deleted — nothing installed it anymore).
botfights/nostrudel/gitea keep the shared external-app code paths.
Frontend: vue-tsc clean, 672 tests pass, production build verified
clean of the removed slugs. Backend: cargo check clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Jul 10 asset swap (75ed3041) replaced the progressive JPEG with a
baseline re-encode that trips the known Chromium compositor black-layer
bug in the 3D perspective background stack — the image paints for a
second, then the layer drops and the #000 container fill shows through.
Restore the pre-swap progressive encoding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
00a70c6 fixed app-catalog/catalog.json but the served copy in
neode-ui/public/catalog.json still pointed at the nonexistent lnd.svg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tx1138 Gitea was retired as a release server 2026-06-13 and its registry
frontend is fully dead (500 on every /v2 manifest read, observed 2026-07-10).
Nothing may reference it anymore:
- registry.rs: no longer a default registry, no longer force-enabled on
load; saved configs are stripped of it on load (same one-time migration
treatment as the decommissioned Hetzner mirror), with a regression test.
- image_policy.rs: removed from TRUSTED_REGISTRIES — refs through the dead
host are now refused at the pull site (rejection test added).
- api/handler: dropped the legacy catalog-proxy fallback URL.
- .gitmodules: indeedhub submodule repointed to the OVH Gitea.
- scripts, image-recipe, app-catalog data, neode-ui strings, docs, and all
test fixtures repointed to 146.59.87.168:3000 (or neutral example hosts).
- image-versions.sh: ARCHY_REGISTRY_FALLBACK emptied (guarded consumers
skip it); reconcile-containers.sh candidate guard hardened.
The only remaining occurrences of the host string are the strip/reject
enforcement paths and their regression tests — the code that guarantees it
is never used again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Channel open fee control, channels tab in wallet settings, mempool tx
link, and the channel-open async peer-connect race fix. Conflict in
LightningChannels.vue resolved to the branch's extracted
LightningChannelsPanel component, with main's Teleport-to-body modal
fixes and glass-button-warning class (8256fde1) ported into the panel
so they aren't lost. lnd tests 16/16, LightningChannels vitest green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes from real fresh-install feedback (Framework node .81) + its log bundle:
Backend:
- websocket: subscribe before initial snapshot — broadcasts in the gap were
silently lost, stranding clients on stale state until a hard refresh
(the "everything needs ctrl-r" bug: My Apps stuck Loading, App Store
stuck Checking, containers-scanned never arriving)
- crash recovery: check the crash marker BEFORE writing our own PID —
recovery had never run on any node (always saw its own PID and skipped);
PID-reuse guard via /proc cmdline
- boot status: pending-boot-starts registry (recovery, stack recovery,
reconciler, adoption) — scanner overlays queued-but-down apps as
Restarting instead of Stopped after a reboot; scanner-authored
Restarting resolves immediately on a settled scan (no transitional wedge)
- install deps: bounded wait (36x5s) when a dependency is installed but
still starting ("Waiting for Bitcoin to start…") instead of instant
rejection; dependency-gate rejections remove the optimistic entry (no
phantom Stopped tile) and surface as a notification
- seed backup: auth.setup persists the onboarding mnemonic as the
encrypted seed backup (reveal previously failed on EVERY node — nothing
ever wrote master_seed.enc); seed.restore stashes too; error sanitizer
lets seed/2FA errors through instead of "Check server logs"
- lnd: bitcoind.rpchost resolved from the running Bitcoin variant
(hardcoded bitcoin-knots broke Core nodes); manifest uses derived_env
- bitcoin status: clean human message for connection-reset/startup; raw
URLs + os-error chains no longer reach the app card
- fedimint-clientd: chown /var/lib/archipelago/fmcd to 1000:1000 (root-
created dir crash-looped the rootless container, EACCES) — first-boot
script + pre-start self-heal
- log volume (>1GB/day on a day-old node): journald caps drop-in (ISO +
bootstrap self-heal), bitcoind -printtoconsole=0 everywhere (90% of the
journal was IBD UpdateTip spam), tracing default debug→info
Frontend:
- Login: Enter advances to confirm field then submits; submit always
clickable with inline errors (was silently disabled on mismatch);
Restart Onboarding needs a confirming second click (the mismatch →
"onboarding restarted" trap)
- sync store: 30s state reconciliation + refetch on re-entrant connect;
20s containers-scanned escape hatch so Checking can never show forever;
fresh empty node reaches the real "no apps yet" state
- intro video: CRF20 re-encode (SSIM 0.988) + faststart — moov was at EOF
so playback needed the full 15MB first (the intro lag)
- backgrounds: 10 heaviest JPEGs → WebP q90 (9.4MB→6.6MB); 7 stayed JPEG
(WebP larger on noisy sources)
- Web5ConnectedNodes: drop unused template ref that failed vue-tsc -b
ISO/kiosk:
- nginx: /assets/ 404s no longer cached immutable for a year; HTTPS block
gained the missing /assets/ location (served index.html as images)
- kiosk: launcher/service spliced from configs/ at ISO build (stale
heredoc force-disabled GPU); MemoryHigh/Max 1200/1500→2200/2800M (kiosk
rode the reclaim throttle = the lag); firmware-intel-graphics +
firmware-amd-graphics (trixie split DMC blobs out of misc-nonfree)
Verified: cargo test 898/898 green, npm run build green with dist
contents confirmed (webp refs, lnd.png, faststart video, new strings).
Handover for ISO build + deploy: docs/HANDOVER-2026-07-02-iso-feedback.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GoalDetail.vue, EasyHome.vue, and the backend's docker_packages.rs
metadata still pointed electrs-family app ids at the old electrs
icon (svg). Point them at electrumx.png like every other reference,
and delete the now-unused electrs.svg asset.
On NAT'd nodes that can reach the iroh federation neither directly nor
via iroh's public relays, fmcd's embedded iroh networking enters a
relay/hole-punch reconnect hot-loop that pegs its entire CPU allotment
indefinitely (observed ~1 core sustained for 4 days on a Tailscale node,
while LAN nodes that reach the guardian directly stay <3%). fmcd 0.8.0
exposes no iroh/relay knobs, so:
- fmcd-run now samples fmcd's own CPU and restarts it when it stays near
its allotment for ~15 min (a restart demonstrably clears the stuck iroh
state; real work is bursty and never flat-pegs a core for minutes).
- Lower cpu_limit 1 -> 0.25 core so a stuck instance can't starve the
node (steady-state is <3% of a core; joins are brief).
Ships as fmcd:0.8.1 (launcher-only rebuild, same fmcd binary). Bumped the
image pin + cpu_limit in the manifest, image-versions.sh, the embedded
catalog manifest (releases/app-catalog.json), and the UI catalogs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fresh Meshtastic radios ship region-UNSET (RF-silent) and on mismatched
channels, so nodes only ever saw themselves. Bring them to MeshCore parity
using the official Meshtastic admin API:
- Auto-provision LoRa region (set_config, AdminMessage field 34) from a new
mesh-config `lora_region` (e.g. EU_868) when the radio's region differs.
- Auto-provision a shared primary channel (set_channel, field 33) with a
PSK derived deterministically from channel_name, so every node converges on
one mesh — the parity equivalent of MeshCore's named "archipelago" channel.
- Read current region/channel from want_config; only write when different
(no reboot loop); cap attempts so a radio that won't persist can't loop.
- Active NodeInfo advert scaffolding + aggressive serial drain.
Verified on .116+.228: region+channel persist, discovery works (both see each
other as named reachable contacts), bidirectional RF + sending confirmed.
Receiving in the running driver is still under diagnosis (instrumentation added).
Also removes the unwanted `meshtastic` daemon app from the registry (it was
never meant to be a container — native driver provides system-level support):
deletes apps/meshtastic + catalog entries (app-catalog, neode-ui, releases) +
test refs. Meshtastic stays native, like MeshCore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 0.4.11 edit affordance only lived on ServerConnectScreen, which a
connected user never sees. Add edit to NESMenu — the settings modal
reached via two-finger hold while connected: a ✎ pencil on each saved
server opens the form pre-populated (Edit Server header + Cancel),
persists via ServerPreferences.updateSavedServer(), and reconnects when
the edited server is the live one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an edit affordance to each saved server in ServerConnectScreen: a
pencil button loads the entry into the form (Edit Server mode) with
Save Changes / Cancel actions. Persisted via a new
ServerPreferences.updateSavedServer() that replaces by connection
identity (address/port/scheme) and keeps the active record in sync when
the edited server is the active one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The published companion APK was v2-only (AGP silently ignores
enableV1Signing for minSdk>=24) and clean builds broke on stray
space-named resource dirs. Harden scripts/publish-companion-apk.sh:
clean build, remove/ýreject space-named res dirs, force v1+v2+v3 via
zipalign+apksigner, and abort unless all three schemes verify. Wire
ship-companion.sh to the shared script. Re-sign the served 0.4.10 APK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>