- Card click now dispatches through openItem(): owned -> cached viewer,
paid+playable -> 10% preview, paid non-playable -> pay modal (image is
never fetched pre-purchase), FREE image -> full-screen lightbox streaming
from /api/peer-content (fixes the click no-op where the old ternary fell
through to undefined for non-playable free items)
- Viewer footer caption is state-aware: green 'Owned · unlocked' only for
owned items, neutral 'Free · shared by peer' for free ones; Save streams
free files instead of calling content.owned-get
- Lightning / invoice-QR / on-chain payment successes now share
openPurchased() with the ecash flow: mark owned, autoplay audio in the
bottom bar or open image/video in the viewer (previously a browser
download that silently fails on the mobile companion)
- closeViewer only revokes blob: URLs (free items use plain stream URLs)
- Added a regression test: free image click opens the lightbox
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Two real Zazawowow tracks from Wavlake (metadata via catalog API, bytes +
artwork committed): WEBFIVEFOURTHREETWOONE is the showcase PAID track
(21 sats), Michael Michael Saylor is free
- Paid/owned downloads now return real file bytes with the correct mime_type
(was a text/plain placeholder) so buying a song autoplays in the bottom bar
- content.owned-list seeded per session with dated purchases matching the
session's federation onions; every purchase path appends to it so Owned
state survives the post-purchase refresh (Paid Files tab now populated)
- peerCatalogFor: deterministic one-peer-per-item assignment + 3 POPULAR
duplicates (was ~25% of items duplicated onto every third peer)
- demoFederationNodes memoised per session so catalogs/owned records/UI agree
- content.preview-peer serves a real audio slice for audio items (paid
preview button plays music, not artwork bytes)
- New GET /api/peer-content/:onion/:content_id Range-capable streaming route
(whitelist lookup, paid items 403) + /api dev proxy in vite.config.ts
- All ten photo-*.jpg picsum placeholders replaced with real Wikimedia
Commons photographs (credited in each description, >=1920px wide)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add an indeedhub entry to staticDevApps in mock-backend.js (state running,
lanPort 8190, existing marketplace title/description/icon). Per-visitor
demo state is structuredClone(staticDevApps), so every fresh session shows
IndeeHub installed in My Apps with no install step. The demo launch URL
bypasses /app/indeedhub/ entirely (iframe loads the :2101 whole-origin
proxy), so no DEMO_APP_PAGES placeholder is added; marketplace metadata
already lists indeedhub following the same pattern as the other static
apps, and uninstall is blocked for static demo apps as usual.
Verified: mock-backend.js boots with DEMO=1 and the /ws/db initial dump of
a fresh session contains indeedhub state=running.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- useDemoIntro: replace DEMO_EXTERNAL_URLS (external-tab workaround) with
DEMO_PROXY_PORTS; demoAppUrl('indeedhub') now resolves to
<protocol>//<current-hostname>:2101/ at runtime (no hardcoded host/IP);
isDemoExternal returns false (kept exported so call sites compile
unchanged); isDemoApp still true for indeedhub so the NEW_TAB bypass
keeps it in the in-app session
- useAppIdentity: suppress the identity-picker modal under IS_DEMO — the
embedded IndeeHub is already signed in via the seeded throwaway demo
account; real-node picker behavior untouched (IS_DEMO compile-time false)
Verified: 195 unit tests green (IS_DEMO=false path); VITE_DEMO=1 build
bundle contains the :2101 launch logic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- nginx-demo.conf: new :2101 server block reverse-proxying the live
indee.tx1138.com site with no path prefix (fixes the old sub_filter
path-rewrite breakage), X-Frame-Options/CSP stripped, WS upgrade
passthrough, and a demo sign-in script injected into <head>
- indee-demo-signin.js: PUBLIC-DEMO-ONLY seeder that writes a labelled
throwaway "nsec" account (freshly generated keypair, not a secret) into
the :2101 origin's indeedhub-accounts/indeedhub-active-account
localStorage keys, idempotently, so IndeeHub boots signed in
- Dockerfile.web: copy the seeder into the demo web image, EXPOSE 2101
- docker-compose.demo.yml + demo-deploy/docker-compose.yml: publish 2101
(DEMO_INDEE_PORT override documented in the thin deploy stack)
Verified: nginx -t clean in nginx:alpine; live proxy smoke shows 200 with
no framing headers, injected tag, seed script served, assets proxied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codebase-derived research for Phase 1 (FED-01..06): identifies an
unlocked concurrent read-modify-write race on federation/nodes.json
as the likely root cause of nodes reappearing after removal, flags
two stale CONCERNS.md claims already fixed on main (01cbec27), maps
remaining mesh demo-parity gaps (contacts-list/save, reaction/edit/
delete stubs) beyond the already-shipped attachment-send parity fix
(c2ce71c6), and scopes the greenfield Lightning-URI/channel-open
surface needed for FED-05.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Demo attach flow failed with 'Method not found: mesh.send-content-inline'
and force-opened the transport chooser modal real nodes don't show.
- mesh.transport-advice now mirrors the daemon's size-based tier logic
(typed_messages.rs): chooser only in the fits-both 1-2.3KB band
- implement mesh.send-content-inline / send-content / fetch-content and
POST /api/blob; bytes live in the per-visitor session store
- sent texts + attachments persist in mesh.messages so refresh-after-send
shows them, same as a real node
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Research findings and a concrete split of work: web-side items for this
repo (pre-warm camera, torch toggle, continuous focus, keep-stream-
alive) and a native handover list for the companion dev (pre-warmed
CameraX + ML Kit, QR-only format, 720p keep-latest analysis, torch,
zoom nudge, haptic dismiss) with acceptance criteria and how to
measure. Quick win landed now: live scan runs at 10 scans/sec when the
platform has a native BarcodeDetector, keeping 4/s only for the
JS-worker fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Framework-pt report: a paid invoice stalled the UI with no success
shown, and lightning/total balances disappeared until it settled.
Three compounding causes, three fixes:
- Backend payinvoice's synchronous wait drops 120s → 8s. Fast payments
(the majority) still settle in one round trip; slow multi-hop routes
return pending + payment_hash quickly and the caller's 3s poll takes
over — instead of the modal freezing for up to two minutes.
- payLightningInvoice gains an onPending hook: SendBitcoinModal and the
scan modal now flip to a visible "Settling…" success pane the moment
the payment goes pending (safe to close), and the ongoing poll
upgrades it to Paid — or replaces it with LND's real failure.
- One slow lnd.getinfo poll (5s budget) flipped the Home wallet card to
"disconnected", hiding balances the user already knew. Three
consecutive failures are now required (~30s) before the card gives up;
last-known balances keep rendering throughout.
rpc-client tests 75/75.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Another update operation is already running" surfaced as a scary
failure while the update was in fact applying fine (OptiPlex, v1.7.118
rollout). The apply path now joins the in-flight install — same
overlay, same wait-for-new-version polling — and a concurrent download
attempt shows a calm in-progress note (EN+ES strings added). The
backend's tarball extractions run under ionice -c3 nice -n10 so a
200MB update can't starve podman/status calls into multi-minute
timeouts on small disks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ~8s into a session a second "freshly plugged" RNode appears on
/dev/ttyACM0, so the global mesh setup modal (and its flash step)
demos itself shortly after opening the Mesh page. Fixed plugged_at
means "Not now" sticks for the whole browser session.
- Transport pills (LoRa/FIPS/Tor) now offered for every demo peer
except mountain-node, which stays radio-only for contrast.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- mesh.transport-advice: peer 1 federated (LoRa/FIPS/Tor pills in the
image modal), others radio-only.
- Scripted Flash LoRa job: flash-list-firmware + flash-device +
flash-status advancing download → erase → write → done over ~35s
with live log tail and percent, plus cancel.
- Demo messages carry per-message transport (meshcore/reticulum/fips/
tor variety for the pills + animated route modal) and
sender_pubkey/sender_seq so reactions/replies work.
- Chat-action acks: send-reaction/reply/read-receipt, edit/delete/
forward, send-channel, mesh.refresh, reboot-radio.
- Services classification demo: self-deployed "podsteadr" stack — main
app launchable, its MediaMTX backend (ui:null) files under Services
with no Launch button, mirroring ui_detection's verdicts.
All verified against the running mock: 11/11 runtime checks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unread badges came back on every visit (framework showed a phantom "2"
with nothing new): unreadCounts was memory-only, so each page load
replayed the entire message history as "new". Seen-state now persists
as a per-contact highest-seen-message-id watermark in localStorage
(ids are backend-monotonic across restarts); first run after this
ships seeds the watermark from history so nobody gets a wall of stale
badges. Opening a chat advances and persists the watermark for all
twins of the merged conversation.
The hop-route modal the user asked for is now reachable from a visible
per-message "⋯" button (the transport pill remains clickable too), has
a fallback title/branch for messages that predate transport tracking,
and animates: endpoints and link reveal in sequence, a pulse travels
the link, and relay dots blink in order — all disabled under
prefers-reduced-motion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1.7.117 broke Reticulum mesh on OTA-only fleet nodes two ways: the
update swaps only the backend binary and frontend tarball, so nodes
kept a stale archy-reticulum-daemon whose argparse exits on the new
--enable-transport flag (mesh session died on every spawn — confirmed
on framework-pt), and they never had archy-rnodeconf at all, so the
in-app Flash LoRa flow failed with a bare "No such file or directory".
Four-part fix:
- The Rust supervisor probes `daemon --help` and only passes
--enable-transport when the daemon advertises it; unsupported daemons
run edge-only exactly as pre-1.7.117 (tested against stub daemons
both ways + missing-binary fail-safe).
- Both PyInstaller tools ride the frontend tarball's runtime payload
(radio-tools/) and bootstrap.rs promotes them to /usr/local/bin on
startup when bytes differ — the first OTA path that ever updates
them. create-release.sh now rebuilds them every release and the
manifest script hard-fails if they're missing.
- The ISO bundles archy-rnodeconf alongside the daemon (it never did).
- Flash LoRa reports "tool not installed — update the node" instead of
the bare spawn error when rnodeconf is absent everywhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Reticulum daemon gains --enable-transport, which writes
enable_transport = yes into the RNS config it regenerates on every
start, and the Rust supervisor always passes it. Archy nodes now relay
RNS traffic and rebroadcast announces, so archy nodes (and Sideband/
NomadNet peers) beyond direct RF range discover and reach each other
through any archy node in between — edge-only operation left every
node limited to its own radio horizon. RNS's per-interface airtime
caps bound the extra announce overhead on LoRa.
Verified: config generation with the flag on/off, daemon --selftest
green with transport enabled, mesh test module 116/116.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrites the prepared 1.7.117 section to include the mesh flash flow,
first-class Reticulum fixes, radio-first routing, mesh chat polish, the
transactions-modal phone fixes, services-vs-apps classification, the
lightning slow-payment fix, cached-resource page loads, load-shedding,
FIPS uptime hardening, and companion 0.5.25. What's New modal block
regenerated from the new bullets (sync-whats-new --check passes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A published port no longer implies a web UI. The package scanner used to
synthesize interfaces.main.ui="true" for any container with a port or
onion address, so headless backends — including self-deployed compose
stacks like podsteadr — showed up as launchable apps. New ui_detection
module decides instead: a manifest interfaces declaration (catalog
overlay first, disk second) is definitive; undeclared apps get a short
HTTP probe of the launch port (HTML page, redirect, or browser auth
wall = UI; JSON APIs, raw TCP, dead ports = service), with cached
verdicts and probes gated on running containers. Frontend canLaunch
now refuses curated services outright and only treats a bare runtime
address as launchable for curated known apps.
Works identically for manifest apps and containers deployed by hand
outside the orchestrator. ui_detection tests 6/6, frontend suite
696/696.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chips become sticky inside the modal's scroll region with a dimmed
blurred band, so the rail filter stays reachable while rows scroll
underneath. Verified in headless chromium: pinned at scroll-region top
after deep scroll, touch swipes starting on the chips still scroll.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the external flasher.meshcore.co.uk link with a button that
opens the global device-setup modal directly at its flash step, via a
new manual entry point in the mesh store (flashFlowPath). Manual opens
target the connected radio (else the first detected stick), skip the
read-only probe — the port is held by the live session and a second tty
opener corrupts it; the backend flash job stops the listener itself —
and close the modal instead of stepping back to the detection screen.
Button is disabled with a hint when no radio is present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tx list kept a vestigial overflow-y-auto from before the modal
contract refactor made BaseModal's slot wrapper the scroller. With a
modal open, modal-scroll-locked applies overscroll-behavior:contain to
every .overflow-y-auto inside the overlay, so touch scrolls latched
onto the non-scrollable inner list and could not chain up to the real
scroller — the modal was unscrollable on any touch device. Wheel input
latches onto the scrollable ancestor directly, which is why desktop
never showed it. Verified with headless-chromium touch synthesis at
360x640/320x568: list scrolls to bottom, background stays contained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-merge regression from combining two individually-correct changes:
main's 2026-07-23 fix makes open_preferred_path bail WITHOUT touching the
port when no device_kind is pinned, while the hw-config branch's skip_path
dedup excludes the preferred path from the auto-detect fallback on the
assumption it was already probed this cycle. Together, on a single-radio
node with no pin (the common fleet state), the only candidate was never
probed at all and the mesh never came up — hit live on archi-dev-box
right after deploying merged main.
Fix: when device_kind is None, skip open_preferred_path entirely and go
straight to auto_detect_and_open with skip_path=None. The pinned path
keeps the existing probe-then-skip fallback.
Verified live on archi-dev-box: radio auto-detected, Reticulum daemon
ready, 5 persisted peers loaded. Mesh tests 116 passed / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>