From 8b930fcd004008b2cc18baeed231e04a11fa8ad9 Mon Sep 17 00:00:00 2001 From: Joey Stanford Date: Sun, 2 Aug 2026 13:48:16 -0600 Subject: [PATCH 1/5] feat(reticulum): float Ratspeak stack and add LXMFace avatars Checkout rsReticulum/rsLXMF/rsNomad to origin/main by default with fail-loud overlays, tip-compatible propagation sticky errors, and LXMFace default peer faces. pnpm run update also watches Ratspeak upstream releases and new org repos. --- .github/workflows/flatpak.yaml | 2 - .github/workflows/reticulum-sidecar.yaml | 4 - AGENTS.md | 2 +- docs/ci-cd.md | 6 +- docs/development-environment.md | 4 +- docs/reticulum-sidecar-ipc.md | 42 ++-- docs/reticulum.md | 4 +- docs/troubleshooting.md | 21 +- reticulum-sidecar/Cargo.lock | 6 +- reticulum-sidecar/README.md | 16 +- reticulum-sidecar/patches/README.md | 101 +++++---- .../rsLXMF-link-delivery-has-pending-to.patch | 14 +- ...LXMF-propagation-node-policy-setters.patch | 18 +- reticulum-sidecar/src/stack/live.rs | 5 +- reticulum-sidecar/src/stack/lxmf_delivery.rs | 4 +- .../src/stack/pn_hosting_apply.rs | 9 +- .../src/stack/propagation_bridge.rs | 198 +++++++++++++++--- ...ply-rsLXMF-link-delivery-has-pending-to.sh | 41 +--- ...-rsLXMF-propagation-node-policy-setters.sh | 38 +--- .../apply-rsLXMF-propagation-sync-peering.sh | 38 ++-- scripts/apply-rsReticulum-auto-beacon-utun.sh | 38 +--- ...m-ble-rnode-pairing-transition-debounce.sh | 33 +-- ...y-rsReticulum-discovery-announce-egress.sh | 38 +--- .../apply-rsReticulum-link-client-nomad.sh | 38 +--- scripts/apply-rsReticulum-packet-tap.sh | 28 +-- .../apply-rsReticulum-path-medium-slots.sh | 32 +-- scripts/clone-ratspeak-stack.sh | 116 +++++++--- scripts/clone-ratspeak-stack.test.mjs | 53 +++++ scripts/ensure-rsReticulum-patches.sh | 14 +- scripts/lib/apply-ratspeak-overlay.sh | 33 +++ scripts/lib/ratspeak-overlay-apply-list.sh | 36 ++++ .../lib/ratspeak-overlay-apply-list.test.mjs | 39 ++++ scripts/update.sh | 189 ++++++++++++++++- scripts/update.test.mjs | 25 +++ .../components/ReticulumPeerDetailModal.tsx | 7 +- .../components/ReticulumPeerListPanel.tsx | 7 +- .../ReticulumPnHostingDangerZone.tsx | 27 +-- .../components/ReticulumProfileIcon.test.tsx | 42 ++++ .../components/ReticulumProfileIcon.tsx | 47 ++++- src/renderer/lib/reticulum/lxmface.test.ts | 53 +++++ src/renderer/lib/reticulum/lxmface.ts | 91 ++++++++ src/renderer/locales/cs/translation.json | 7 +- src/renderer/locales/de/translation.json | 7 +- src/renderer/locales/en/translation.json | 5 +- src/renderer/locales/es/translation.json | 7 +- src/renderer/locales/fr/translation.json | 7 +- src/renderer/locales/id/translation.json | 7 +- src/renderer/locales/it/translation.json | 7 +- src/renderer/locales/ja/translation.json | 7 +- src/renderer/locales/ko/translation.json | 7 +- src/renderer/locales/nl/translation.json | 7 +- src/renderer/locales/pl/translation.json | 7 +- src/renderer/locales/pt-BR/translation.json | 7 +- src/renderer/locales/ru/translation.json | 7 +- src/renderer/locales/tr/translation.json | 7 +- src/renderer/locales/uk/translation.json | 7 +- src/renderer/locales/zh/translation.json | 7 +- 57 files changed, 1127 insertions(+), 542 deletions(-) create mode 100644 scripts/clone-ratspeak-stack.test.mjs create mode 100644 scripts/lib/apply-ratspeak-overlay.sh create mode 100644 scripts/lib/ratspeak-overlay-apply-list.sh create mode 100644 scripts/lib/ratspeak-overlay-apply-list.test.mjs create mode 100644 src/renderer/components/ReticulumProfileIcon.test.tsx create mode 100644 src/renderer/lib/reticulum/lxmface.test.ts create mode 100644 src/renderer/lib/reticulum/lxmface.ts diff --git a/.github/workflows/flatpak.yaml b/.github/workflows/flatpak.yaml index a8839a27c..29192fbc9 100644 --- a/.github/workflows/flatpak.yaml +++ b/.github/workflows/flatpak.yaml @@ -12,8 +12,6 @@ jobs: reticulum-sidecar: name: Reticulum sidecar (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} - env: - RS_RETICULUM_REF: 9928abed269a83ec5a7ef165ff1142d938cad706 permissions: contents: read strategy: diff --git a/.github/workflows/reticulum-sidecar.yaml b/.github/workflows/reticulum-sidecar.yaml index 0cbf33e6a..cabe34831 100644 --- a/.github/workflows/reticulum-sidecar.yaml +++ b/.github/workflows/reticulum-sidecar.yaml @@ -97,8 +97,6 @@ jobs: retention-days: 14 build-rns-stack: - env: - RS_RETICULUM_REF: 9928abed269a83ec5a7ef165ff1142d938cad706 strategy: fail-fast: false matrix: @@ -166,8 +164,6 @@ jobs: retention-days: 14 build-windows-arm64-rns-stack: - env: - RS_RETICULUM_REF: 9928abed269a83ec5a7ef165ff1142d938cad706 permissions: contents: read runs-on: windows-latest diff --git a/AGENTS.md b/AGENTS.md index ae1898b56..eae23c506 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,7 +118,7 @@ Adding a cross-boundary feature: **Local Linux CI (optional):** Container mode — `act:ci`, `act:tests`, `act:pr`, … (needs a Docker-compatible engine + act; Podman preferred). Host mode — `act:ci:native`, `act:tests:native`, … (no container engine). See [docs/ci-cd.md](docs/ci-cd.md). macOS/Windows packaging uses native `dist:mac` / `dist:win`. **`dist:mac`** / **`dist:mac:publish`** always run **`scripts/verify-mac-packaging.mjs`** (ZIP + DMG symlink asserts, no raw `.app` CI uploads). macOS signing env (`CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`, `CSC_IDENTITY_AUTO_DISCOVERY`) is scoped to **`macos-latest`** jobs in `release.yaml` / `build.yaml`; partial-secret validation fails the release job when `CSC_LINK` is set but notarization secrets are missing. -> **Update script sync:** When adding or removing packages from `patchedDependencies` in `pnpm-workspace.yaml`, keep `WATCH_ENTRIES` in `scripts/update.sh` in sync so the script warns on version changes to every patched dependency. When adding or removing Ratspeak overlays under `reticulum-sidecar/patches/`, keep `RATSPEAK_PATCH_ENTRIES` in `scripts/update.sh` (`check_ratspeak_patches`) in sync — `pnpm run update` queries upstream PRs (rsReticulum / rsLXMF / rsNomad) and warns when a local overlay can be removed. `pnpm run update` also runs `rustup update` (or Homebrew `rust` on macOS without rustup) and `cargo build` in `reticulum-sidecar/` when `cargo` is on `PATH` (full-feature build includes `nomad-core` / rsNomad). +> **Update script sync:** When adding or removing packages from `patchedDependencies` in `pnpm-workspace.yaml`, keep `WATCH_ENTRIES` in `scripts/update.sh` in sync so the script warns on version changes to every patched dependency. When adding or removing Ratspeak overlays under `reticulum-sidecar/patches/`, keep `RATSPEAK_PATCH_ENTRIES` in `scripts/update.sh` (`check_ratspeak_patches`) in sync — `pnpm run update` queries upstream PRs (rsReticulum / rsLXMF) and warns when a local overlay can be removed. It also runs `check_ratspeak_upstream` (watched releases for rsLXST / lrgp-rs / Ratspeak / LXMFace, plus new `ratspeak` org repos) — keep `RATSPEAK_RELEASE_WATCH_ENTRIES` / `RATSPEAK_KNOWN_ORG_REPOS` in sync when adopting libs. `scripts/clone-ratspeak-stack.sh` floats **rsReticulum** / **rsLXMF** / **rsNomad** to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF`); overlays must apply or the clone fails. Peer default avatars use vendored **LXMFace** (`src/renderer/lib/reticulum/lxmface.ts`). `pnpm run update` also runs `rustup update` (or Homebrew `rust` on macOS without rustup) and `cargo build` in `reticulum-sidecar/` when `cargo` is on `PATH` (full-feature build includes `nomad-core` / rsNomad). **Pre-commit hook order:** diff --git a/docs/ci-cd.md b/docs/ci-cd.md index c7c6d2940..2d04188ef 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -76,6 +76,8 @@ Path-filtered on `reticulum-sidecar/**` and related scripts: 1. **`lint` job (ubuntu-latest)** — `cargo fmt --check` + `cargo clippy` with `rns-stack,rns-ble,rns-rnode-tcp` (`-D warnings`) 2. **Build matrix** — stub + full-stack `cargo test` and release builds on Linux, macOS, and Windows (including WoA arm64 jobs) +CI clones Ratspeak siblings via `scripts/clone-ratspeak-stack.sh` and **no longer hardcodes `RS_RETICULUM_REF`** — rsReticulum / rsLXMF / rsNomad float to `origin/main` (overlays must apply). Override with `RS_*_REF` only for local bisect. + Local parity: `pnpm run reticulum:sidecar:clippy:full`, `pnpm run check:reticulum-sidecar` (pre-commit full-feature). See [development-environment.md](development-environment.md#reticulum-sidecar-optional). --- @@ -150,7 +152,9 @@ Automated dependency updates are configured in `.github/dependabot.yml`: - **GitHub Actions:** Grouped into one PR - **Open PRs:** `open-pull-requests-limit: 0` — Dependabot scans but does **not** open PRs. Dependency bumps are applied manually via `pnpm run update` (`scripts/update.sh`), which - also runs dedupe and Ratspeak/rsReticulum patch checks. See AGENTS.md §6. + also runs dedupe, Ratspeak overlay PR checks, and an upstream release / new-org-repo watch + (rsLXST, lrgp-rs, Ratspeak, LXMFace). Sibling **rsReticulum** / **rsLXMF** / **rsNomad** + float to `origin/main` via `clone-ratspeak-stack.sh` (overlays must apply). See AGENTS.md §6. ### Testing Dependabot PRs locally diff --git a/docs/development-environment.md b/docs/development-environment.md index c40a53cbd..9bcbc6560 100644 --- a/docs/development-environment.md +++ b/docs/development-environment.md @@ -119,7 +119,9 @@ pnpm run reticulum:sidecar:build This writes `reticulum-sidecar/target/debug/mesh-client-reticulum` (macOS/Linux) or `.exe` on Windows. -When sibling checkouts `../rsReticulum`, `../rsLXMF`, and `../rsNomad` exist, the build script applies required rsReticulum overlays automatically (`scripts/ensure-rsReticulum-patches.sh`) before compiling with `rns-stack,rns-ble,rns-rnode-tcp`. See [`reticulum-sidecar/patches/README.md`](../reticulum-sidecar/patches/README.md) for overlay details. +**First-time / recover siblings:** from the mesh-client repo root, run `./scripts/clone-ratspeak-stack.sh`. That script clones (or updates) sibling checkouts `../rsReticulum`, `../rsLXMF`, and `../rsNomad`, floats each to **`origin/main`** by default, and applies mesh-client overlays (fails if a patch will not apply). For bisect or a known-good pin, set `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` to a SHA or ref before running the clone script. + +When those siblings already exist, `pnpm run reticulum:sidecar:build` applies required overlays via `scripts/ensure-rsReticulum-patches.sh` before compiling with `rns-stack,rns-ble,rns-rnode-tcp`. See [`reticulum-sidecar/patches/README.md`](../reticulum-sidecar/patches/README.md) for overlay details. **First run in Electron dev:** **Reticulum** → **Connection** → **Start stack** will run `cargo build` automatically if that binary is missing (first compile can take a few minutes). Pre-build with the command above to avoid waiting on the first click. diff --git a/docs/reticulum-sidecar-ipc.md b/docs/reticulum-sidecar-ipc.md index 3419bc428..01f732645 100644 --- a/docs/reticulum-sidecar-ipc.md +++ b/docs/reticulum-sidecar-ipc.md @@ -126,27 +126,27 @@ Routing bias between **RF** (LoRa / RNode) and **network** (TCP/UDP/I2P/gateway/ **`PnHostingPolicy`** (mirrored in `src/shared/pnHostingPolicy.ts` / sidecar `pn_hosting_policy.rs`): -| Field | Default | Notes | -| -------------------------- | ------- | ----------------------------------------------- | -| `peering_cost` | `18` | Must be ≤ `max_peering_cost` | -| `max_peering_cost` | `26` | | -| `autopeer` | `true` | | -| `autopeer_maxdepth` | `4` | Cap 64 | -| `max_peers` | `20` | 1–256 | -| `propagation_stamp_cost` | `16` | | -| `propagation_stamp_flex` | `3` | Must be ≤ stamp cost | -| `message_storage_limit_mb` | `256` | 1–10240 | -| `propagation_limit_kb` | `256` | 1–102400 | -| `sync_limit_kb` | `10240` | 1–102400 | -| `delivery_limit_kb` | `1000` | 1–102400 | -| `from_static_only` | `false` | | -| `auth_required` | `false` | | -| `enforce_stamps` | `false` | | -| `enforce_ratchets` | `false` | | -| `static_peers` | `[]` | Lowercase 32-hex hashes (max 256) | -| `node_name` | `null` | Trimmed; max 128 scalar chars; no control chars | -| `pn_announce_interval_sec` | `360` | Cap 86400 | -| `announce_at_start` | `true` | | +| Field | Default | Notes | +| -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `peering_cost` | `18` | Must be ≤ `max_peering_cost` | +| `max_peering_cost` | `26` | | +| `autopeer` | `true` | | +| `autopeer_maxdepth` | `4` | Cap 64 | +| `max_peers` | `20` | 1–256 | +| `propagation_stamp_cost` | `16` | | +| `propagation_stamp_flex` | `3` | Must be ≤ stamp cost | +| `message_storage_limit_mb` | `256` | 1–10240 | +| `propagation_limit_kb` | `256` | 1–102400 | +| `sync_limit_kb` | `10240` | 1–102400 | +| `delivery_limit_kb` | `1000` | 1–102400 | +| `from_static_only` | `false` | | +| `auth_required` | `false` | | +| `enforce_stamps` | `false` | Persisted for UI/compat; **not applied** on current rsLXMF tip (`set_enforce_stamps` removed upstream; stamp gating uses `set_stamp_requirements`) | +| `enforce_ratchets` | `false` | Persisted for UI/compat; **not applied** on current rsLXMF tip (`set_enforce_ratchets` removed upstream) | +| `static_peers` | `[]` | Lowercase 32-hex hashes (max 256) | +| `node_name` | `null` | Trimmed; max 128 scalar chars; no control chars | +| `pn_announce_interval_sec` | `360` | Cap 86400 | +| `announce_at_start` | `true` | | ### Nomad Network diff --git a/docs/reticulum.md b/docs/reticulum.md index df2025863..53929d115 100644 --- a/docs/reticulum.md +++ b/docs/reticulum.md @@ -310,7 +310,7 @@ When a destination is reachable over more than one next hop, the sidecar keeps u - **Peers tab UX:** keep-alive after first visit; opening the tab uses soft/cached path-table data (skips refresh when peers are already in the store). Manual **Refresh** forces a live dump (`?refresh=1`). Row prepare/sort for large lists is deferred so chrome paints immediately. - **After a DB wipe:** peer rows refill only as destinations announce again (or path responses arrive). Connecting to the same hub does **not** dump every known destination instantly. mesh-client applies announces / `peers_updated` patches incrementally (batched), with a full peer dump on connect, manual Refresh, stack restart, and a 30s safety poll (60s when the path table is large). - **Your node** does not appear as a peer row; identity hash is under **Network → Identity**; topology uses a synthetic **You** center node -- **Avatar:** peers without a custom icon show an empty outline; **People** maps wire `people`/`person`/`user` → Lucide `user` (`reticulumIconAppearance.ts`). Legacy `circle` is treated as unset (not a real avatar choice). +- **Avatar:** peers without a custom Lucide icon show an [LXMFace](https://github.com/ratspeak/LXMFace) default identicon (`src/renderer/lib/reticulum/lxmface.ts`); a custom icon still overrides. **People** maps wire `people`/`person`/`user` → Lucide `user` (`reticulumIconAppearance.ts`). Legacy `circle` is treated as unset (not a real avatar choice). - **`interface` column:** path learned via that interface, not “devices on this serial port” - **Display names / aliases:** sidecar peers may ship without labels; mesh-client enriches from (in order) sidecar `display_name`, **LXMF / Nomad announce** `app_data` (msgpack, JSON `server_name`, or UTF-8 — parsed in the sidecar; RMAP/geo JSON blobs are rejected), SQLite `reticulum_destinations.display_name`, and Nomad Network node list during `refreshReticulumPeersFromSidecar`. Sidecar `list_contacts` / contact upsert also fills nameless or hash-prefix contact labels from that announce/peer/Nomad cache (does not overwrite a real stored name; may persist fills; retries persist after save failure). Renderer refresh preserves peer announce aliases when contact dumps omit names after path/probe, keeps in-memory **icon/appearance** when the DB row lacks icons, and Chat/`nodeStore` sync via `reticulumContactToNodeRecordPreservingLabel` refuses hash-prefix `longName` overwrites. Renderer display (`sanitizeReticulumDisplayName` / `reticulumRealDisplayName`) mirrors sidecar rules for already-stored bad values. Inbound LXMF ingest (`reticulumIngest.ts`) treats a `sender_name` equal to the destination hash prefix as a **placeholder**, not a real alias — contact upserts omit it. SQLite upsert (`db:upsertReticulumDestination`) requires an exact **32-hex** destination hash (lowercased; no separator stripping), **refuses to overwrite** an existing name with a hash-prefix alias (case-insensitive guard on the first 12 hex chars), and leaves **`favorited` alone** when the payload omits it (icon-only patches). Schema upgrade collapses legacy case-variant destination rows onto one lowercase PK. - **Topology:** one next hop per destination (`via_hash`); sidecar infers `self → relay` when needed; force layout with hop fallback; auto-refresh debounced and paused under large path tables (manual Refresh always available) @@ -374,7 +374,7 @@ Firmware `.zip` files are selected locally (no in-app GitHub download). Disconne ## Building the sidecar (development) -`rns-stack` builds need siblings `rsReticulum`, `rsLXMF`, and `rsNomad` (see `scripts/clone-ratspeak-stack.sh`). +`rns-stack` builds need siblings `rsReticulum`, `rsLXMF`, and `rsNomad` (see `scripts/clone-ratspeak-stack.sh`). That script floats **rsReticulum** / **rsLXMF** / **rsNomad** to `origin/main` by default (bisect with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF`) and applies mesh-client overlays (fails if a patch will not apply). Peer list / detail default avatars use [LXMFace](https://github.com/ratspeak/LXMFace) (`src/renderer/lib/reticulum/lxmface.ts`) when no custom Lucide icon is set. End users of **GitHub Releases** or **Flatpak** do not need Rust. Developers and contributors do. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f0127daac..7c62cdf51 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -946,21 +946,23 @@ Keep Rust current with `pnpm run update` (runs `rustup update` and rebuilds the **Symptoms**: **Start stack** fails; logs show `RETICULUM_CARGO_BUILD_FAILED` or Rust errors such as `method not found in ReticulumHandle`, `register_packet_tap`, or `PacketTapEvent`. Electron may surface `RETICULUM_RNS_PATCH_MISSING` after upgrading mesh-client. -**Cause**: Full-stack (`rns-stack`) dev builds call `register_packet_tap` in the sidecar, but that API lives in a local rsReticulum overlay ([`reticulum-sidecar/patches/rsReticulum-packet-tap.patch`](../reticulum-sidecar/patches/rsReticulum-packet-tap.patch)) until [ratspeak/rsReticulum#10](https://github.com/ratspeak/rsReticulum/pull/10) merges. CI applies it automatically; a sibling `../rsReticulum` checkout without the overlay fails to compile. +**Cause**: Full-stack (`rns-stack`) dev builds call `register_packet_tap` in the sidecar, but that API lives in a local rsReticulum overlay ([`reticulum-sidecar/patches/rsReticulum-packet-tap.patch`](../reticulum-sidecar/patches/rsReticulum-packet-tap.patch)) until [ratspeak/rsReticulum#10](https://github.com/ratspeak/rsReticulum/pull/10) merges. CI applies overlays via `clone-ratspeak-stack.sh`; a sibling `../rsReticulum` checkout without the overlay fails to compile. -**Fix**: +**Fix** (canonical recover path): -1. From mesh-client repo root (requires sibling `../rsReticulum` and `../rsLXMF`): +1. From mesh-client repo root, re-float siblings and re-apply overlays: ```bash + ./scripts/clone-ratspeak-stack.sh pnpm run reticulum:sidecar:build ``` - This runs `scripts/ensure-rsReticulum-patches.sh` before `cargo build`. -2. **Manual apply** (when you prefer not to use the npm script): + `clone-ratspeak-stack.sh` floats `rsReticulum` / `rsLXMF` / `rsNomad` to `origin/main` (override with `RS_*_REF` for bisect) and fails if an overlay will not apply. +2. If siblings already exist and you only need overlays: `./scripts/ensure-rsReticulum-patches.sh` then `pnpm run reticulum:sidecar:build`. +3. **Manual apply** (single overlay): ```bash git -C ../rsReticulum apply reticulum-sidecar/patches/rsReticulum-packet-tap.patch pnpm run reticulum:sidecar:build ``` -3. On **newer rsReticulum** checkouts that already include the auto-beacon utun fix upstream, only the packet-tap patch is required — `apply-rsReticulum-auto-beacon-utun.sh` is a no-op. +4. On **newer rsReticulum** checkouts that already include the auto-beacon utun fix upstream, only the packet-tap patch is required — `apply-rsReticulum-auto-beacon-utun.sh` is a no-op. Quit mesh-client fully, reopen, and click **Start stack** again. @@ -973,13 +975,12 @@ Quit mesh-client fully, reopen, and click **Start stack** again. **Fix**: 1. **Update mesh-client** to a build that includes the rsReticulum overlay `rsReticulum-auto-beacon-utun.patch` (skips `utun*` during enumeration and backs off repeated TX failures). -2. **Dev rebuild**: from repo root, apply overlays then rebuild: +2. **Dev rebuild**: from repo root, prefer the canonical recover path, then rebuild: ```bash - ./scripts/apply-rsReticulum-packet-tap.sh - ./scripts/apply-rsReticulum-auto-beacon-utun.sh - ./scripts/apply-rsReticulum-link-client-nomad.sh + ./scripts/clone-ratspeak-stack.sh pnpm run reticulum:sidecar:build ``` + Or apply individual overlays (`./scripts/apply-rsReticulum-packet-tap.sh`, `auto-beacon-utun`, `link-client-nomad`, …) then `pnpm run reticulum:sidecar:build`. 3. **Workaround on old builds**: disable **AutoInterface** under Connection → Interfaces if LAN discovery is not needed (TCP/RNode paths still work). 4. **Physical NIC failures** (`en0`, `wlan0`, …): restart the stack; check firewall/multicast permissions — that indicates real LAN discovery failure, not VPN noise. diff --git a/reticulum-sidecar/Cargo.lock b/reticulum-sidecar/Cargo.lock index d2c93dc40..3b03aed2f 100644 --- a/reticulum-sidecar/Cargo.lock +++ b/reticulum-sidecar/Cargo.lock @@ -1329,12 +1329,13 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lxmf-core" -version = "1.0.1" +version = "1.1.0" dependencies = [ "base64", "bytes", "hex", "rand 0.8.6", + "rmp", "rmp-serde", "rmpv", "rns-crypto", @@ -1348,6 +1349,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", + "unicode-normalization", ] [[package]] @@ -1498,7 +1500,7 @@ dependencies = [ "rns-transport", "rns-wire", "serde", - "serde_json", + "tempfile", "thiserror 2.0.18", "tokio", "tracing", diff --git a/reticulum-sidecar/README.md b/reticulum-sidecar/README.md index d9808ebdd..9365059f9 100644 --- a/reticulum-sidecar/README.md +++ b/reticulum-sidecar/README.md @@ -8,7 +8,15 @@ Install Rust (**1.85+**, edition 2024). Prefer [rustup](https://rustup.rs/). See ## Build -**Default (stub stack)** — builds without `--features rns-stack`; Cargo still requires sibling `rsReticulum`, `rsLXMF`, and `rsNomad` directories on disk (CI checkouts them automatically; locally clone next to `mesh-client`): +**First-time setup** — from the mesh-client repo root, clone/float siblings and apply overlays: + +```bash +./scripts/clone-ratspeak-stack.sh +``` + +That floats `rsReticulum` / `rsLXMF` / `rsNomad` to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` for bisect). Peer default avatars use [LXMFace](https://github.com/ratspeak/LXMFace) in the **renderer** (`src/renderer/lib/reticulum/lxmface.ts`), not this sidecar. + +**Default (stub stack)** — builds without `--features rns-stack`; Cargo still requires sibling `rsReticulum`, `rsLXMF`, and `rsNomad` directories on disk (CI runs `clone-ratspeak-stack.sh`; locally use the script above): ```bash pnpm run reticulum:sidecar:build @@ -24,7 +32,7 @@ parent/ mesh-client/reticulum-sidecar/ ``` -Apply overlays (required for `rns-stack` until upstream merges): +Prefer `./scripts/clone-ratspeak-stack.sh` (or `./scripts/ensure-rsReticulum-patches.sh` on an existing tree). Individual apply scripts remain for single-overlay work: ```bash ./scripts/apply-rsReticulum-packet-tap.sh @@ -36,7 +44,7 @@ Apply overlays (required for `rns-stack` until upstream merges): ./scripts/apply-rsLXMF-propagation-node-policy-setters.sh ``` -See [patches/README.md](patches/README.md) for base SHA and regen steps. +See [patches/README.md](patches/README.md) for overlay regen against floated `origin/main` (record the short SHA in the PR). ```bash cd reticulum-sidecar @@ -74,7 +82,7 @@ Install coverage tooling once: `cargo install cargo-llvm-cov`. - **Pre-commit** runs sibling `rsNomad` fmt/clippy plus sidecar stub fmt/clippy/test when `cargo` is on `PATH` (no coverage). - **CI lint** (`reticulum-sidecar.yaml`): `rsNomad` fmt/clippy, then full-feature sidecar `fmt --check` + Clippy. - **CI coverage** (`tests.yaml`): `cargo llvm-cov --fail-under-lines 45` when sidecar paths change (ratchet toward ~52%; ignores `rsReticulum`/`rsLXMF`/`rsNomad` path deps). -- **`rsNomad` pin:** `scripts/clone-ratspeak-stack.sh` checks out `RS_NOMAD_REF` (override or `RS_NOMAD_SKIP_PIN=1` for local work). +- **Ratspeak / Nomad siblings:** `scripts/clone-ratspeak-stack.sh` floats `rsReticulum` / `rsLXMF` / `rsNomad` to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF`); overlays must apply. ## API diff --git a/reticulum-sidecar/patches/README.md b/reticulum-sidecar/patches/README.md index dea6ea6b0..4b1187b44 100644 --- a/reticulum-sidecar/patches/README.md +++ b/reticulum-sidecar/patches/README.md @@ -1,18 +1,21 @@ -# rsReticulum overlays +# rsReticulum / rsLXMF overlays -Patches applied on top of pinned [ratspeak/rsReticulum](https://github.com/ratspeak/rsReticulum) checkouts for mesh-client `rns-stack` builds. +Patches applied on top of [ratspeak/rsReticulum](https://github.com/ratspeak/rsReticulum) / [ratspeak/rsLXMF](https://github.com/ratspeak/rsLXMF) checkouts for mesh-client `rns-stack` builds (sibling [Colorado-Mesh/rsNomad](https://github.com/Colorado-Mesh/rsNomad) is also required for Nomad hosting; no mesh-client overlay today). + +By default `scripts/clone-ratspeak-stack.sh` floats siblings to **`origin/main`** and applies these overlays (fails loud if a patch will not apply). Use `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` to pin a known-good SHA for bisect. Per-overlay **Base commit** tables below record the last regeneration baseline, not a permanent pin — when regenerating, prefer floated `origin/main` and record the short SHA in the PR. ## Development — overlays/patches Overlays require **git checkouts** of sibling repos next to this clone (not a bare Cargo cache path): -- `../rsReticulum` — rsReticulum source at the pinned commit used by `clone-ratspeak-stack.sh` -- `../rsLXMF` — when applying LXMF overlays +- `../rsReticulum` — floated to `origin/main` unless `RS_RETICULUM_REF` is set +- `../rsLXMF` — floated to `origin/main` unless `RS_LXMF_REF` is set +- `../rsNomad` — floated to `origin/main` unless `RS_NOMAD_REF` is set **First-time setup:** ```bash -# From mesh-client repo root — clones/pins siblings and applies known overlays +# From mesh-client repo root — clones/floats siblings and applies known overlays ./scripts/clone-ratspeak-stack.sh # Or ensure patches on an existing sibling tree: ./scripts/ensure-rsReticulum-patches.sh @@ -53,22 +56,27 @@ From mesh-client repo root (sibling `../rsReticulum` required): ### Regenerate +Regenerate against floated `origin/main` (record the short SHA in the PR): + ```bash cd ../rsReticulum -git fetch origin -git diff 9928abed269a83ec5a7ef165ff1142d938cad706 -- \ +git fetch origin && git checkout --detach origin/main +# apply local packet-tap edits, then: +git diff -- \ crates/rns-runtime/src/reticulum.rs \ crates/rns-transport/src/messages.rs \ crates/rns-transport/src/actor/mod.rs \ crates/rns-transport/src/actor/inbound.rs \ > ../mesh-client/reticulum-sidecar/patches/rsReticulum-packet-tap.patch -git -C /tmp/rsReticulum-patch-test checkout 9928abed269a83ec5a7ef165ff1142d938cad706 +# smoke-check on a clean tip clone: +git -C /tmp/rsReticulum-patch-test fetch origin +git -C /tmp/rsReticulum-patch-test checkout --detach origin/main git -C /tmp/rsReticulum-patch-test apply --check ../mesh-client/reticulum-sidecar/patches/rsReticulum-packet-tap.patch ``` ### Sunset -When the upstream PR merges, remove this patch, drop the CI apply step, and clone `ratspeak/rsReticulum` `main` directly in `build-rns-stack` jobs. +When the upstream PR merges and floated `origin/main` includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. ## rsReticulum-auto-beacon-utun.patch @@ -100,18 +108,22 @@ Apply after the packet-tap patch when both overlays are needed: ### Regenerate +Regenerate against floated `origin/main` (record the short SHA in the PR): + ```bash cd ../rsReticulum -# after implementing on top of RS_RETICULUM_REF -git diff 9928abed269a83ec5a7ef165ff1142d938cad706 -- crates/rns-interface/src/auto.rs \ +git fetch origin && git checkout --detach origin/main +# after implementing the utun filter/backoff, then: +git diff -- crates/rns-interface/src/auto.rs \ > ../mesh-client/reticulum-sidecar/patches/rsReticulum-auto-beacon-utun.patch -git -C /tmp/rsReticulum-patch-test checkout 9928abed269a83ec5a7ef165ff1142d938cad706 +git -C /tmp/rsReticulum-patch-test fetch origin +git -C /tmp/rsReticulum-patch-test checkout --detach origin/main git -C /tmp/rsReticulum-patch-test apply --check ../mesh-client/reticulum-sidecar/patches/rsReticulum-auto-beacon-utun.patch ``` ### Sunset -When [ratspeak/rsReticulum#11](https://github.com/ratspeak/rsReticulum/pull/11) merges, remove this patch and drop the CI apply step (same as packet-tap). +When [ratspeak/rsReticulum#11](https://github.com/ratspeak/rsReticulum/pull/11) merges and floated `origin/main` includes it, remove this patch and drop the apply step (same as packet-tap). ## rsReticulum-link-client-nomad.patch @@ -137,7 +149,7 @@ From mesh-client repo root (sibling `../rsReticulum` required): ./scripts/apply-rsReticulum-link-client-nomad.sh ``` -Apply after packet-tap + auto-beacon when rebuilding a pinned checkout: +Apply after packet-tap + auto-beacon when rebuilding against floated `origin/main`: ```bash ./scripts/apply-rsReticulum-packet-tap.sh @@ -147,9 +159,12 @@ Apply after packet-tap + auto-beacon when rebuilding a pinned checkout: ### Regenerate +Regenerate against floated `origin/main` (record the short SHA in the PR): + ```bash -# From a clean pin with the other overlays applied, then the upstream commit: -git -C /tmp/rsReticulum-patch-test checkout 9928abed269a83ec5a7ef165ff1142d938cad706 +# From a clean tip with the other overlays applied, then the LinkClient edits: +git -C /tmp/rsReticulum-patch-test fetch origin +git -C /tmp/rsReticulum-patch-test checkout --detach origin/main git -C /tmp/rsReticulum-patch-test apply reticulum-sidecar/patches/rsReticulum-packet-tap.patch git -C /tmp/rsReticulum-patch-test apply reticulum-sidecar/patches/rsReticulum-auto-beacon-utun.patch git -C /tmp/rsReticulum-linkclient-nomad format-patch -1 --stdout \ @@ -160,7 +175,7 @@ git -C /tmp/rsReticulum-patch-test diff \ ### Sunset -When [ratspeak/rsReticulum#14](https://github.com/ratspeak/rsReticulum/pull/14) merges, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. +When [ratspeak/rsReticulum#14](https://github.com/ratspeak/rsReticulum/pull/14) merges and floated `origin/main` includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. ## rsReticulum-link-client-proof-budget.patch @@ -179,7 +194,7 @@ Keep `LinkClient::query` proof wait on the **remaining overall deadline** (v5.25 ## Removed: rsReticulum-rnode-tcp-activity-keepalive.patch -Sunset when upstream landed `RNodeIdleProbe` (`88d3d38` — *rnode: restore TCP application idle probes*). [ratspeak/rsReticulum#15](https://github.com/ratspeak/rsReticulum/pull/15) was closed as superseded; mesh-client no longer carries that overlay (pin `9928abed269a83ec5a7ef165ff1142d938cad706` or later already includes idle probes). Tracked entry removed from `RATSPEAK_PATCH_ENTRIES` in `scripts/update.sh` after sunset confirmation. +Sunset when upstream landed `RNodeIdleProbe` (`88d3d38` — *rnode: restore TCP application idle probes*). [ratspeak/rsReticulum#15](https://github.com/ratspeak/rsReticulum/pull/15) was closed as superseded; mesh-client no longer carries that overlay (floated `origin/main` already includes idle probes). Tracked entry removed from `RATSPEAK_PATCH_ENTRIES` in `scripts/update.sh` after sunset confirmation. ## rsReticulum-ble-rnode-pairing-transition-debounce.patch @@ -202,7 +217,7 @@ From mesh-client repo root (sibling `../rsReticulum` required): ./scripts/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh ``` -Apply after the other rsReticulum overlays when rebuilding a pinned checkout: +Apply after the other rsReticulum overlays when rebuilding against floated `origin/main`: ```bash ./scripts/apply-rsReticulum-packet-tap.sh @@ -214,15 +229,19 @@ Apply after the other rsReticulum overlays when rebuilding a pinned checkout: ### Regenerate +Regenerate against floated `origin/main` (record the short SHA in the PR): + ```bash cd ../rsReticulum +git fetch origin && git checkout --detach origin/main +# apply local debounce edit, then: git diff -- crates/rns-interface/src/ble_rnode.rs \ > ../mesh-client/reticulum-sidecar/patches/rsReticulum-ble-rnode-pairing-transition-debounce.patch ``` ### Sunset -When [ratspeak/rsReticulum#20](https://github.com/ratspeak/rsReticulum/pull/20) merges and the clone pin includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. +When [ratspeak/rsReticulum#20](https://github.com/ratspeak/rsReticulum/pull/20) merges and floated `origin/main` includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. ## rsReticulum-discovery-announce-egress.patch @@ -259,9 +278,12 @@ Apply **after** the other rsReticulum overlays (packet-tap also touches `reticul ### Regenerate +Regenerate against floated `origin/main` (record the short SHA in the PR): + ```bash -# After applying prior overlays on the pin, implement the discovery fix, then: +# After applying prior overlays on tip, implement the discovery fix, then: cd ../rsReticulum +git fetch origin && git checkout --detach origin/main git diff -- \ crates/rns-runtime/src/reticulum.rs \ crates/rns-transport/src/actor/mod.rs \ @@ -271,7 +293,7 @@ git diff -- \ ### Sunset -When [ratspeak/rsReticulum#19](https://github.com/ratspeak/rsReticulum/pull/19) merges and the clone pin includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. +When [ratspeak/rsReticulum#19](https://github.com/ratspeak/rsReticulum/pull/19) merges and floated `origin/main` includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. ## rsLXMF-propagation-sync-peering.patch @@ -279,12 +301,12 @@ LinkIdentify + peering stamp before LXMF `/offer`, sticky offer/finish fields, p | Field | Value | | ----- | ----- | -| **Base commit** | `68ad7c835187c052c763bb28c41b04a655f35c64` | +| **Base commit** | historical (`68ad7c8…`); tip uses `set_identity` / `send_identify` instead | | **Upstream PR** | https://github.com/ratspeak/rsLXMF/pull/4 | **Modifies (1 file):** -- `crates/lxmf-core/src/propagation_sync.rs` — identify/stamp before `/offer`; sync task peering + sticky finish fields +- `crates/lxmf-core/src/propagation_sync.rs` — identify/stamp before `/offer` on older checkouts. On current `main`, apply script no-ops when `set_identity` is already present. ### Apply locally @@ -298,24 +320,27 @@ From mesh-client repo root (sibling `../rsLXMF` required): ### Regenerate +On current floated `origin/main`, the apply script **no-ops** when `set_identity` is already present — regenerate only if you still need the overlay for an older pin: + ```bash cd ../rsLXMF -git fetch origin -git diff 68ad7c835187c052c763bb28c41b04a655f35c64 -- crates/lxmf-core/src/propagation_sync.rs \ +git fetch origin && git checkout --detach origin/main +# only needed for older pins without set_identity / send_identify: +git diff -- crates/lxmf-core/src/propagation_sync.rs \ > ../mesh-client/reticulum-sidecar/patches/rsLXMF-propagation-sync-peering.patch ``` ### Sunset -When [ratspeak/rsLXMF#4](https://github.com/ratspeak/rsLXMF/pull/4) merges, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. +When floated `origin/main` always includes identify/stamp before `/offer` (tip already does via `set_identity`), remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. ## rsLXMF-propagation-node-policy-setters.patch -Live mutators for local PN hosting policy updates (`set_peering_cost`, `set_max_storage`, `set_max_message_size`). Upstream pin only exposes `set_min_stamp_cost`; mesh-client `pn_hosting_apply` needs the others so policy edits apply without recreating the node. +Live mutators for local PN hosting policy updates (`set_peering_cost`, `set_max_storage`, `set_max_message_size`). Floated rsLXMF tip only exposes `set_min_stamp_cost`; mesh-client `pn_hosting_apply` needs the others so policy edits apply without recreating the node. | Field | Value | | ----- | ----- | -| **Base commit** | `68ad7c835187c052c763bb28c41b04a655f35c64` | +| **Base commit** | tip of `ratspeak/rsLXMF` `main` (regenerated for float-to-main) | | **Upstream PR** | https://github.com/ratspeak/rsLXMF/pull/6 | **Modifies (1 file):** @@ -336,22 +361,23 @@ From mesh-client repo root (sibling `../rsLXMF` required): ```bash cd ../rsLXMF -git fetch origin -git diff 68ad7c835187c052c763bb28c41b04a655f35c64 -- crates/lxmf-core/src/propagation_node.rs \ +git fetch origin && git checkout --detach origin/main +# apply local setter edits, then: +git diff -- crates/lxmf-core/src/propagation_node.rs \ > ../mesh-client/reticulum-sidecar/patches/rsLXMF-propagation-node-policy-setters.patch ``` ### Sunset -When [ratspeak/rsLXMF#6](https://github.com/ratspeak/rsLXMF/pull/6) merges and the clone pin includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. +When [ratspeak/rsLXMF#6](https://github.com/ratspeak/rsLXMF/pull/6) merges and floated `origin/main` includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. ## rsLXMF-link-delivery-has-pending-to.patch -Expose `LinkDeliveryManager::has_pending_to` so the sidecar can serialize packed Propagated deposits (and propagation sync) against an in-flight Link to the same PN. Pinned rsLXMF only has `delivery_link_available` (reusable idle link), which is the wrong predicate for one-shot packed sessions. +Expose `LinkDeliveryManager::has_pending_to` so the sidecar can serialize packed Propagated deposits (and propagation sync) against an in-flight Link to the same PN. Floated rsLXMF tip only has `delivery_link_available` (reusable idle link), which is the wrong predicate for one-shot packed sessions. | Field | Value | | ----- | ----- | -| **Base commit** | `68ad7c835187c052c763bb28c41b04a655f35c64` | +| **Base commit** | tip of `ratspeak/rsLXMF` `main` (regenerated for float-to-main) | | **Upstream PR** | (none yet — mesh-client local API) | **Modifies (1 file):** @@ -370,14 +396,15 @@ Expose `LinkDeliveryManager::has_pending_to` so the sidecar can serialize packed ```bash cd ../rsLXMF -git fetch origin -git diff 68ad7c835187c052c763bb28c41b04a655f35c64 -- crates/lxmf-core/src/link_delivery.rs \ +git fetch origin && git checkout --detach origin/main +# apply local has_pending_to edit, then: +git diff -- crates/lxmf-core/src/link_delivery.rs \ > ../mesh-client/reticulum-sidecar/patches/rsLXMF-link-delivery-has-pending-to.patch ``` ### Sunset -When upstream ships `has_pending_to` (or an equivalent) on the clone pin, remove this patch and drop the apply step. +When upstream ships `has_pending_to` (or an equivalent) on floated `origin/main`, remove this patch and drop the apply step. ## rsReticulum-path-medium-slots.patch diff --git a/reticulum-sidecar/patches/rsLXMF-link-delivery-has-pending-to.patch b/reticulum-sidecar/patches/rsLXMF-link-delivery-has-pending-to.patch index 893ce3f7f..fe77d751a 100644 --- a/reticulum-sidecar/patches/rsLXMF-link-delivery-has-pending-to.patch +++ b/reticulum-sidecar/patches/rsLXMF-link-delivery-has-pending-to.patch @@ -1,15 +1,19 @@ diff --git a/crates/lxmf-core/src/link_delivery.rs b/crates/lxmf-core/src/link_delivery.rs -index fc12604..30c84ae 100644 +index 778c30a..b684d72 100644 --- a/crates/lxmf-core/src/link_delivery.rs +++ b/crates/lxmf-core/src/link_delivery.rs -@@ -2301,6 +2301,12 @@ impl LinkDeliveryManager { +@@ -2303,6 +2303,16 @@ impl LinkDeliveryManager { || self.backchannel_links.contains_key(dest_hash) } -+ /// True when any in-flight (one-shot or Direct) session targets `dest_hash`. -+ /// Used to serialize packed Propagated deposits vs a second LinkRequest to the same PN. ++ /// True when any non-closed delivery link is pending to `dest_hash` (in-flight or queued). ++ /// Unlike `delivery_link_available`, this includes non-reusable one-shot links. + pub fn has_pending_to(&self, dest_hash: &[u8; 16]) -> bool { -+ self.pending.values().any(|d| d.dest_hash == *dest_hash) ++ self.direct_links ++ .get(dest_hash) ++ .and_then(|link_id| self.pending.get(link_id)) ++ .is_some_and(|delivery| delivery.link.state != LinkState::Closed) ++ || self.backchannel_links.contains_key(dest_hash) + } + pub fn direct_link_snapshot(&self, dest_hash: [u8; 16]) -> Option { diff --git a/reticulum-sidecar/patches/rsLXMF-propagation-node-policy-setters.patch b/reticulum-sidecar/patches/rsLXMF-propagation-node-policy-setters.patch index ba805fb39..e2ebad8f2 100644 --- a/reticulum-sidecar/patches/rsLXMF-propagation-node-policy-setters.patch +++ b/reticulum-sidecar/patches/rsLXMF-propagation-node-policy-setters.patch @@ -1,8 +1,8 @@ diff --git a/crates/lxmf-core/src/propagation_node.rs b/crates/lxmf-core/src/propagation_node.rs -index 5032269..33772f0 100644 +index d3d58da..6f9dd33 100644 --- a/crates/lxmf-core/src/propagation_node.rs +++ b/crates/lxmf-core/src/propagation_node.rs -@@ -172,6 +172,18 @@ impl PropagationNode { +@@ -307,6 +307,18 @@ impl PropagationNode { self.config.min_stamp_cost = cost; } @@ -10,14 +10,14 @@ index 5032269..33772f0 100644 + self.config.peering_cost = cost; + } + -+ pub fn set_max_storage(&mut self, max: usize) { -+ self.config.max_storage = max; ++ pub fn set_max_storage(&mut self, max_storage: usize) { ++ self.config.max_storage = max_storage; + } + -+ pub fn set_max_message_size(&mut self, max: usize) { -+ self.config.max_message_size = max; ++ pub fn set_max_message_size(&mut self, max_message_size: usize) { ++ self.config.max_message_size = max_message_size; + } + - /// Disk-backed node. Loads existing messages from `storage_path` on startup. - pub fn with_storage( - config: PropagationNodeConfig, + pub fn offer_generation(&self) -> u64 { + self.offer_generation + } diff --git a/reticulum-sidecar/src/stack/live.rs b/reticulum-sidecar/src/stack/live.rs index aff07cfd6..1ca8dc338 100644 --- a/reticulum-sidecar/src/stack/live.rs +++ b/reticulum-sidecar/src/stack/live.rs @@ -1986,6 +1986,7 @@ impl LiveBridge { stamp_cost: Some(parsed.stamp_cost), stamp_flexibility: Some(parsed.stamp_flex), peering_cost: Some(parsed.peering_cost), + metadata: Some(parsed.metadata.clone()), hops: Some(evt.hops), }); if !peered { @@ -3011,8 +3012,10 @@ impl LiveBridge { return Err(format!("propagation establish failed: {err}")); } let progress = self.propagation.sync_progress(); + let peak = self.propagation.last_peak_progress(); // Offering / later stages prove /offer was accepted enough to proceed. - if progress >= 25.0 { + // Peak survives tip's Complete/Failed → Idle collapse (live progress drops to 0). + if progress >= 25.0 || peak >= 25.0 { self.cancel_propagation_sync().await; return Ok(()); } diff --git a/reticulum-sidecar/src/stack/lxmf_delivery.rs b/reticulum-sidecar/src/stack/lxmf_delivery.rs index b2b70c537..75e0f9788 100644 --- a/reticulum-sidecar/src/stack/lxmf_delivery.rs +++ b/reticulum-sidecar/src/stack/lxmf_delivery.rs @@ -181,7 +181,9 @@ pub fn spawn_lxmf_inbound_receiver( router: Arc>, ) { let delivery_rx = register_destination(&transport_tx, lxmf_dest_hash, LXMF_APP); - let (link_packet_tx, mut link_packet_rx) = mpsc::channel::<(Vec, [u8; 16])>(256); + // Unbounded: rsReticulum LinkManager::set_link_packet_channel requires UnboundedSender. + // Bound only if upstream grows a bounded setter; do not buffer-copy into a second queue. + let (link_packet_tx, mut link_packet_rx) = mpsc::unbounded_channel::<(Vec, [u8; 16])>(); let (resource_tx, mut resource_rx) = mpsc::channel::<(Vec, [u8; 16])>(256); let mut link_mgr = LinkManager::with_destination( diff --git a/reticulum-sidecar/src/stack/pn_hosting_apply.rs b/reticulum-sidecar/src/stack/pn_hosting_apply.rs index 783655d91..3b911e0e9 100644 --- a/reticulum-sidecar/src/stack/pn_hosting_apply.rs +++ b/reticulum-sidecar/src/stack/pn_hosting_apply.rs @@ -13,11 +13,14 @@ pub fn apply_pn_hosting_policy_to_router(router: &mut LxmRouter, policy: &PnHost router.set_stamp_requirements(policy.propagation_stamp_cost, policy.propagation_stamp_flex); router.set_message_storage_limit(Some(policy.message_storage_limit_bytes())); router.set_authentication(policy.auth_required); - router.set_enforce_stamps(policy.enforce_stamps); - router.set_enforce_ratchets(policy.enforce_ratchets); + // rsLXMF tip dropped set_enforce_stamps/ratchets (stamp gating via set_stamp_requirements). + let _ = (policy.enforce_stamps, policy.enforce_ratchets); router.config.sync_limit_kb = policy.sync_limit_kb; - router.config.delivery_limit_kb = policy.delivery_limit_kb; + #[allow(clippy::cast_precision_loss)] // KB policy is integer; router tip uses f64 + { + router.config.delivery_limit_kb = policy.delivery_limit_kb as f64; + } router.config.ext.peering_cost = policy.peering_cost; router.config.ext.max_peering_cost = policy.max_peering_cost; router.config.ext.autopeer_maxdepth = policy.autopeer_maxdepth; diff --git a/reticulum-sidecar/src/stack/propagation_bridge.rs b/reticulum-sidecar/src/stack/propagation_bridge.rs index f779d92c7..4a82ed8dc 100644 --- a/reticulum-sidecar/src/stack/propagation_bridge.rs +++ b/reticulum-sidecar/src/stack/propagation_bridge.rs @@ -6,8 +6,9 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use std::time::{Duration, Instant}; +use lxmf_core::peer::OutboundOfferPolicy; use lxmf_core::propagation_node::{PropagationNode, PropagationNodeConfig}; -use lxmf_core::propagation_sync::{PropagationSyncTask, SyncTaskState}; +use lxmf_core::propagation_sync::{PeerSyncTerminalState, PropagationSyncTask, SyncTaskState}; use lxmf_core::router::LxmRouter; use rns_identity::identity::Identity; use rns_transport::messages::TransportMessage; @@ -20,6 +21,14 @@ pub struct PropagationBridge { local_serving: AtomicBool, /// Serializes sync-run generation changes with emitter cancel / pin / event side effects. sync_lifecycle: Mutex<()>, + /// Sticky offer failure label (rsLXMF tip keeps terminal state on the task, not these fields). + last_offer_error: Mutex>, + /// Sticky establish failure label for UI / offer-probe. + last_establish_error: Mutex>, + /// Sticky success/failure after Complete/Failed collapses to Idle. + last_finished_ok: Mutex>, + /// Peak sync progress seen before tip collapses Complete/Failed → Idle. + peak_progress: Mutex, } impl PropagationBridge { @@ -37,6 +46,7 @@ impl PropagationBridge { min_stamp_cost: policy.min_stamp_cost(), peering_cost: policy.peering_cost, max_message_size: policy.propagation_limit_kb.saturating_mul(1024), + max_offer_size: policy.sync_limit_kb.saturating_mul(1000), }; let local_node = Arc::new(Mutex::new( PropagationNode::with_storage(node_config, local_dest_hash, storage_dir) @@ -46,16 +56,32 @@ impl PropagationBridge { let signing_key = identity .get_signing_key() .ok_or_else(|| "propagation sync: identity has no signing key".to_string())?; - sync_task.set_local_identity(identity.get_public_key(), signing_key); + sync_task.set_identity(identity.get_public_key(), signing_key); Ok(Self { local_dest_hash, local_node, sync_task: Mutex::new(sync_task), local_serving: AtomicBool::new(false), sync_lifecycle: Mutex::new(()), + last_offer_error: Mutex::new(None), + last_establish_error: Mutex::new(None), + last_finished_ok: Mutex::new(None), + peak_progress: Mutex::new(0.0), }) } + /// Map rsLXMF sync-task state to UI / probe progress (single source of truth). + pub fn progress_for_state(state: SyncTaskState) -> f64 { + match state { + SyncTaskState::Establishing => 10.0, + SyncTaskState::Offering => 25.0, + SyncTaskState::AwaitingResponse => 40.0, + SyncTaskState::Transferring => 70.0, + SyncTaskState::Complete => 100.0, + SyncTaskState::Idle | SyncTaskState::Failed => 0.0, + } + } + pub fn local_node(&self) -> Arc> { self.local_node.clone() } @@ -84,7 +110,53 @@ impl PropagationBridge { .unwrap_or((0, 0)) } - #[allow(clippy::type_complexity)] // peering tuple mirrors RNS PropagationSyncTask::configure_peering + fn clear_sticky_errors(&self) { + if let Ok(mut slot) = self.last_offer_error.lock() { + *slot = None; + } + if let Ok(mut slot) = self.last_establish_error.lock() { + *slot = None; + } + if let Ok(mut slot) = self.last_finished_ok.lock() { + *slot = None; + } + if let Ok(mut slot) = self.peak_progress.lock() { + *slot = 0.0; + } + } + + fn note_peak_progress(&self, progress: f64) { + if progress <= 0.0 { + return; + } + if let Ok(mut peak) = self.peak_progress.lock() { + if progress > *peak { + *peak = progress; + } + } + } + + /// Peak progress observed for the current/last sync run (survives Idle collapse). + pub fn last_peak_progress(&self) -> f64 { + self.peak_progress.lock().map(|p| *p).unwrap_or(0.0) + } + + fn stamp_terminal_failure_from_peak(&self, peak: f64) { + // Tip collapses Complete/Failed → Idle in the same tick, so task.state after + // take_terminal is always Idle (progress 0). Classify from peak instead. + // Never invent "Unknown" (probe maps that to PROPAGATION_OFFER_UNSUPPORTED). + // peak >= 25 (Offering+): leave offer_error unset so probe can treat it as OK. + if peak >= 25.0 { + return; + } + if let Ok(mut slot) = self.last_establish_error.lock() { + if slot.is_none() { + *slot = Some("NoLinkProof"); + } + } + } + + #[allow(clippy::type_complexity)] // peering tuple: local_id, peer_id, cost, optional key pub fn start_sync( &self, remote_hash: [u8; 16], @@ -93,18 +165,29 @@ impl PropagationBridge { let Ok(mut task) = self.sync_task.lock() else { return false; }; - if let Some((local_id, peer_id, cost, key)) = peering { - task.configure_peering(local_id, peer_id, cost, key); + self.clear_sticky_errors(); + let mut policy = OutboundOfferPolicy::unrestricted(remote_hash); + if let Some((_local_id, _peer_id, cost, key)) = peering { + policy.peering_cost = cost; + if let Some(k) = key { + policy.peering_key = k; + } } - task.request_sync_now(remote_hash); - true + task.request_sync_now_with_policy(policy) } pub fn cancel_sync(&self) { + // Tip `cancel_peer_sync` leaves Idle + clears terminal_result. Do not force + // Failed afterward — that blocks the next `request_sync_now_*` (Idle required). if let Ok(mut task) = self.sync_task.lock() { - task.state = SyncTaskState::Failed; - // Sticky fail so the progress emitter does not emit a terminal progress=100. - task.last_finished_ok = Some(false); + if let Some(hash) = task.node_dest_hash() { + let _ = task.cancel_peer_sync(&hash); + } else { + task.state = SyncTaskState::Idle; + } + } + if let Ok(mut slot) = self.last_finished_ok.lock() { + *slot = Some(false); } } @@ -157,43 +240,43 @@ impl PropagationBridge { pub fn sync_progress(&self) -> f64 { self.sync_task .lock() - .map(|task| match task.state { - SyncTaskState::Establishing => 10.0, - SyncTaskState::Offering => 25.0, - SyncTaskState::AwaitingResponse => 40.0, - SyncTaskState::Transferring => 70.0, - SyncTaskState::Complete => 100.0, - SyncTaskState::Idle | SyncTaskState::Failed => 0.0, - }) + .map(|task| Self::progress_for_state(task.state)) .unwrap_or(0.0) } pub fn last_offer_error(&self) -> Option<&'static str> { - self.sync_task - .lock() - .ok() - .and_then(|task| task.last_offer_error) + self.last_offer_error.lock().ok().and_then(|slot| *slot) } pub fn last_establish_error(&self) -> Option<&'static str> { - self.sync_task - .lock() - .ok() - .and_then(|task| task.last_establish_error) + self.last_establish_error.lock().ok().and_then(|slot| *slot) } /// Sticky success/failure after Complete/Failed collapses to Idle. pub fn last_finished_ok(&self) -> Option { - self.sync_task - .lock() - .ok() - .and_then(|task| task.last_finished_ok) + self.last_finished_ok.lock().ok().and_then(|slot| *slot) } pub fn tick(&self, known_identities: &HashMap) { - if let Ok(mut task) = self.sync_task.lock() { + let terminal = if let Ok(mut task) = self.sync_task.lock() { + // Sample before drain/tick: tip collapses Complete|Failed → Idle in tick(). + self.note_peak_progress(Self::progress_for_state(task.state)); task.drain_events(known_identities); + self.note_peak_progress(Self::progress_for_state(task.state)); task.tick(); + task.take_terminal_peer_result() + .map(|result| matches!(result.state, PeerSyncTerminalState::Complete)) + } else { + None + }; + if let Some(ok) = terminal { + if let Ok(mut slot) = self.last_finished_ok.lock() { + *slot = Some(ok); + } + if !ok { + let peak = self.last_peak_progress(); + self.stamp_terminal_failure_from_peak(peak); + } } } @@ -248,8 +331,15 @@ impl PropagationBridge { }; if active && progress <= 10.0 && started.elapsed() > SYNC_STALL_TIMEOUT { bridge.run_if_current(&active_run_id, run_id, || { + if let Ok(mut slot) = bridge.last_establish_error.lock() { + if slot.is_none() { + *slot = Some("NoLinkProof"); + } + } bridge.cancel_sync(); - let message = establish_error + let message = bridge + .last_establish_error() + .or(establish_error) .map(|e| format!("propagation establish failed: {e}")) .unwrap_or_else(|| { "propagation establish failed: NoLinkProof".to_string() @@ -405,6 +495,50 @@ mod tests { bridge.cancel_sync(); assert_eq!(bridge.last_finished_ok(), Some(false)); assert!(!bridge.sync_active()); + // Tip requires Idle for the next request_sync_now_*; cancel must not leave Failed. + assert!(matches!( + bridge.sync_task.lock().expect("lock").state, + SyncTaskState::Idle + )); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn progress_for_state_maps_offer_threshold() { + assert!( + (PropagationBridge::progress_for_state(SyncTaskState::Establishing) - 10.0).abs() + < f64::EPSILON + ); + assert!( + (PropagationBridge::progress_for_state(SyncTaskState::Offering) - 25.0).abs() + < f64::EPSILON + ); + assert!(PropagationBridge::progress_for_state(SyncTaskState::Failed).abs() < f64::EPSILON); + } + + #[test] + fn early_terminal_failure_stamps_establish_not_unknown() { + let dir = + std::env::temp_dir().join(format!("mesh-prop-bridge-stamp-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("tmpdir"); + let (tx, _rx) = mpsc::channel(8); + let identity = rns_identity::identity::Identity::new(); + let bridge = PropagationBridge::new( + tx, + [0xab; 16], + dir.clone(), + &identity, + &super::super::pn_hosting_policy::PnHostingPolicy::default(), + ) + .expect("bridge"); + bridge.stamp_terminal_failure_from_peak(10.0); + assert_eq!(bridge.last_establish_error(), Some("NoLinkProof")); + assert_eq!(bridge.last_offer_error(), None); + bridge.clear_sticky_errors(); + bridge.stamp_terminal_failure_from_peak(40.0); + assert_eq!(bridge.last_establish_error(), None); + assert_eq!(bridge.last_offer_error(), None); let _ = std::fs::remove_dir_all(&dir); } } diff --git a/scripts/apply-rsLXMF-link-delivery-has-pending-to.sh b/scripts/apply-rsLXMF-link-delivery-has-pending-to.sh index 88d2968be..18bc1d4ab 100755 --- a/scripts/apply-rsLXMF-link-delivery-has-pending-to.sh +++ b/scripts/apply-rsLXMF-link-delivery-has-pending-to.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # Apply mesh-client rsLXMF LinkDeliveryManager::has_pending_to for rns-stack builds. -# Serializes packed Propagated deposits vs a second LinkRequest to the same PN -# (pinned rsLXMF lacks this query helper). +# Serializes packed Propagated deposits vs a second LinkRequest to the same PN. set -euo pipefail -RS_LXMF_REF="${RS_LXMF_REF:-68ad7c835187c052c763bb28c41b04a655f35c64}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsLXMF-link-delivery-has-pending-to.patch" -LXMF_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsLXMF" +LXMF_DIR="${RS_LXMF_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsLXMF}" LINK_RS="${LXMF_DIR}/crates/lxmf-core/src/link_delivery.rs" if [[ ! -d "${LXMF_DIR}/.git" ]]; then @@ -32,36 +32,7 @@ if overlay_already_present; then exit 0 fi -if ! git -C "${LXMF_DIR}" diff --quiet || ! git -C "${LXMF_DIR}" diff --cached --quiet; then - echo "warning: ${LXMF_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${LXMF_DIR}" apply --check "${PATCH_FILE}" - git -C "${LXMF_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsLXMF @ $(git -C "${LXMF_DIR}" rev-parse --short HEAD)" +if apply_ratspeak_overlay_or_die "${LXMF_DIR}" "${PATCH_FILE}" "link-delivery-has-pending-to"; then exit 0 fi - -echo "has_pending_to patch did not apply on current HEAD; checking out pinned ref ${RS_LXMF_REF:0:12}" -if [[ -n "$(git -C "${LXMF_DIR}" status --porcelain)" ]]; then - echo "error: ${LXMF_DIR} has uncommitted changes; cannot checkout ${RS_LXMF_REF:0:12} to apply overlay" >&2 - echo "Stash/commit sibling changes, or ensure has_pending_to is already present." >&2 - exit 1 -fi -current_head="$(git -C "${LXMF_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_LXMF_REF}" ]]; then - git -C "${LXMF_DIR}" fetch origin --tags - git -C "${LXMF_DIR}" checkout "${RS_LXMF_REF}" -fi - -if overlay_already_present; then - echo "link-delivery has_pending_to overlay already present on rsLXMF @ ${RS_LXMF_REF:0:12}" - exit 0 -fi - -apply_patch -echo "applied ${PATCH_FILE} on rsLXMF @ ${RS_LXMF_REF:0:12}" +exit 1 diff --git a/scripts/apply-rsLXMF-propagation-node-policy-setters.sh b/scripts/apply-rsLXMF-propagation-node-policy-setters.sh index 00d4dd026..6af13b808 100755 --- a/scripts/apply-rsLXMF-propagation-node-policy-setters.sh +++ b/scripts/apply-rsLXMF-propagation-node-policy-setters.sh @@ -4,11 +4,12 @@ # policy updates can mutate a running local node (upstream only has set_min_stamp_cost). set -euo pipefail -RS_LXMF_REF="${RS_LXMF_REF:-68ad7c835187c052c763bb28c41b04a655f35c64}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsLXMF-propagation-node-policy-setters.patch" -LXMF_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsLXMF" +LXMF_DIR="${RS_LXMF_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsLXMF}" NODE_RS="${LXMF_DIR}/crates/lxmf-core/src/propagation_node.rs" if [[ ! -d "${LXMF_DIR}/.git" ]]; then @@ -34,36 +35,7 @@ if overlay_already_present; then exit 0 fi -if ! git -C "${LXMF_DIR}" diff --quiet || ! git -C "${LXMF_DIR}" diff --cached --quiet; then - echo "warning: ${LXMF_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${LXMF_DIR}" apply --check "${PATCH_FILE}" - git -C "${LXMF_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsLXMF @ $(git -C "${LXMF_DIR}" rev-parse --short HEAD)" +if apply_ratspeak_overlay_or_die "${LXMF_DIR}" "${PATCH_FILE}" "propagation-node-policy-setters"; then exit 0 fi - -echo "propagation-node policy setters patch did not apply on current HEAD; checking out pinned ref ${RS_LXMF_REF:0:12}" -if [[ -n "$(git -C "${LXMF_DIR}" status --porcelain)" ]]; then - echo "error: ${LXMF_DIR} has uncommitted changes; cannot checkout ${RS_LXMF_REF:0:12} to apply overlay" >&2 - echo "Stash/commit sibling changes, or ensure the policy setters are already present." >&2 - exit 1 -fi -current_head="$(git -C "${LXMF_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_LXMF_REF}" ]]; then - git -C "${LXMF_DIR}" fetch origin --tags - git -C "${LXMF_DIR}" checkout "${RS_LXMF_REF}" -fi - -if overlay_already_present; then - echo "propagation-node policy setters overlay already present on rsLXMF @ ${RS_LXMF_REF:0:12}" - exit 0 -fi - -apply_patch -echo "applied ${PATCH_FILE} on rsLXMF @ ${RS_LXMF_REF:0:12}" +exit 1 diff --git a/scripts/apply-rsLXMF-propagation-sync-peering.sh b/scripts/apply-rsLXMF-propagation-sync-peering.sh index 06910de10..b31e86689 100755 --- a/scripts/apply-rsLXMF-propagation-sync-peering.sh +++ b/scripts/apply-rsLXMF-propagation-sync-peering.sh @@ -1,12 +1,15 @@ #!/usr/bin/env bash # Apply mesh-client rsLXMF PropagationSyncTask peering/identity overlay for rns-stack builds. +# On current rsLXMF main, LinkIdentify lives as set_identity()/send_identify() (API rewrite); +# the historical set_local_identity overlay is only needed on older checkouts. set -euo pipefail -RS_LXMF_REF="${RS_LXMF_REF:-68ad7c835187c052c763bb28c41b04a655f35c64}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsLXMF-propagation-sync-peering.patch" -LXMF_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsLXMF" +LXMF_DIR="${RS_LXMF_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsLXMF}" SYNC_RS="${LXMF_DIR}/crates/lxmf-core/src/propagation_sync.rs" if [[ ! -d "${LXMF_DIR}/.git" ]]; then @@ -20,31 +23,16 @@ if [[ ! -f "${PATCH_FILE}" ]]; then exit 1 fi -if [[ -f "${SYNC_RS}" ]] && grep -q 'fn set_local_identity' "${SYNC_RS}"; then - echo "propagation-sync peering overlay already applied on rsLXMF @ $(git -C "${LXMF_DIR}" rev-parse --short HEAD)" +# Tip (post API-parity rewrite) or historical overlay. +if [[ -f "${SYNC_RS}" ]] && { + grep -qE 'fn set_identity\(' "${SYNC_RS}" \ + || grep -qE 'fn set_local_identity\(' "${SYNC_RS}" +}; then + echo "propagation-sync peering/identity capability present on rsLXMF @ $(git -C "${LXMF_DIR}" rev-parse --short HEAD)" exit 0 fi -if ! git -C "${LXMF_DIR}" diff --quiet || ! git -C "${LXMF_DIR}" diff --cached --quiet; then - echo "warning: ${LXMF_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${LXMF_DIR}" apply --check "${PATCH_FILE}" - git -C "${LXMF_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsLXMF @ $(git -C "${LXMF_DIR}" rev-parse --short HEAD)" +if apply_ratspeak_overlay_or_die "${LXMF_DIR}" "${PATCH_FILE}" "propagation-sync-peering"; then exit 0 fi - -echo "propagation-sync peering patch did not apply on current HEAD; checking out pinned ref ${RS_LXMF_REF:0:12}" -current_head="$(git -C "${LXMF_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_LXMF_REF}" ]]; then - git -C "${LXMF_DIR}" fetch origin --tags - git -C "${LXMF_DIR}" checkout "${RS_LXMF_REF}" -fi - -apply_patch -echo "applied ${PATCH_FILE} on rsLXMF @ ${RS_LXMF_REF:0:12}" +exit 1 diff --git a/scripts/apply-rsReticulum-auto-beacon-utun.sh b/scripts/apply-rsReticulum-auto-beacon-utun.sh index 575d44cd3..64567da42 100755 --- a/scripts/apply-rsReticulum-auto-beacon-utun.sh +++ b/scripts/apply-rsReticulum-auto-beacon-utun.sh @@ -2,11 +2,12 @@ # Apply mesh-client rsReticulum AutoInterface beacon overlay for rns-stack local builds. set -euo pipefail -RS_RETICULUM_REF="${RS_RETICULUM_REF:-9928abed269a83ec5a7ef165ff1142d938cad706}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsReticulum-auto-beacon-utun.patch" -RNS_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum" +RNS_DIR="${RS_RETICULUM_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum}" AUTO_RS="${RNS_DIR}/crates/rns-interface/src/auto.rs" if [[ ! -d "${RNS_DIR}/.git" ]]; then @@ -26,36 +27,7 @@ if [[ -f "${AUTO_RS}" ]] && grep -q 'beacon_tx_backoff_secs' "${AUTO_RS}"; then exit 0 fi -if ! git -C "${RNS_DIR}" diff --quiet || ! git -C "${RNS_DIR}" diff --cached --quiet; then - echo "warning: ${RNS_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${RNS_DIR}" apply --check "${PATCH_FILE}" - git -C "${RNS_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" - exit 0 -fi - -if git -C "${RNS_DIR}" apply --check "${PATCH_FILE}" 2>&1 | grep -q 'patch does not apply'; then - echo "auto-beacon utun overlay not needed (already upstream or incompatible with current rsReticulum HEAD)" +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "auto-beacon-utun"; then exit 0 fi - -echo "auto-beacon patch did not apply on current HEAD; checking out pinned ref ${RS_RETICULUM_REF:0:12}" -current_head="$(git -C "${RNS_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_RETICULUM_REF}" ]]; then - git -C "${RNS_DIR}" fetch origin --tags - git -C "${RNS_DIR}" checkout "${RS_RETICULUM_REF}" -fi - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsReticulum @ ${RS_RETICULUM_REF:0:12}" - exit 0 -fi - -echo "auto-beacon utun overlay not needed (already upstream or incompatible with pinned rsReticulum ref)" -exit 0 +exit 1 diff --git a/scripts/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh b/scripts/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh index 815ed2435..7bb8df624 100755 --- a/scripts/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh +++ b/scripts/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh @@ -4,11 +4,12 @@ # re-fired the OS passkey dialog while the user was typing; wait 30s instead. set -euo pipefail -RS_RETICULUM_REF="${RS_RETICULUM_REF:-9928abed269a83ec5a7ef165ff1142d938cad706}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsReticulum-ble-rnode-pairing-transition-debounce.patch" -RNS_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum" +RNS_DIR="${RS_RETICULUM_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum}" BLE_RNODE_RS="${RNS_DIR}/crates/rns-interface/src/ble_rnode.rs" if [[ ! -d "${RNS_DIR}/.git" ]]; then @@ -35,31 +36,7 @@ if overlay_already_present; then exit 0 fi -if ! git -C "${RNS_DIR}" diff --quiet || ! git -C "${RNS_DIR}" diff --cached --quiet; then - echo "warning: ${RNS_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${RNS_DIR}" apply --check "${PATCH_FILE}" - git -C "${RNS_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "ble-rnode-pairing-transition-debounce"; then exit 0 fi - -echo "ble_rnode pairing-transition debounce patch did not apply on current HEAD; checking out pinned ref ${RS_RETICULUM_REF:0:12}" -current_head="$(git -C "${RNS_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_RETICULUM_REF}" ]]; then - git -C "${RNS_DIR}" fetch origin --tags - git -C "${RNS_DIR}" checkout "${RS_RETICULUM_REF}" -fi - -if overlay_already_present; then - echo "ble_rnode pairing-transition debounce overlay already present on rsReticulum @ ${RS_RETICULUM_REF:0:12}" - exit 0 -fi - -apply_patch -echo "applied ${PATCH_FILE} on rsReticulum @ ${RS_RETICULUM_REF:0:12}" +exit 1 diff --git a/scripts/apply-rsReticulum-discovery-announce-egress.sh b/scripts/apply-rsReticulum-discovery-announce-egress.sh index cade74d0a..c76baa0c7 100755 --- a/scripts/apply-rsReticulum-discovery-announce-egress.sh +++ b/scripts/apply-rsReticulum-discovery-announce-egress.sh @@ -5,11 +5,12 @@ # (BLE RNode late bring-up). Upstream: https://github.com/ratspeak/rsReticulum/pull/19 set -euo pipefail -RS_RETICULUM_REF="${RS_RETICULUM_REF:-9928abed269a83ec5a7ef165ff1142d938cad706}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsReticulum-discovery-announce-egress.patch" -RNS_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum" +RNS_DIR="${RS_RETICULUM_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum}" RETICULUM_RS="${RNS_DIR}/crates/rns-runtime/src/reticulum.rs" if [[ ! -d "${RNS_DIR}/.git" ]]; then @@ -39,36 +40,7 @@ if overlay_already_present; then exit 0 fi -if ! git -C "${RNS_DIR}" diff --quiet || ! git -C "${RNS_DIR}" diff --cached --quiet; then - echo "warning: ${RNS_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${RNS_DIR}" apply --check "${PATCH_FILE}" - git -C "${RNS_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "discovery-announce-egress"; then exit 0 fi - -echo "discovery-announce egress patch did not apply on current HEAD; checking out pinned ref ${RS_RETICULUM_REF:0:12}" -if [[ -n "$(git -C "${RNS_DIR}" status --porcelain)" ]]; then - echo "error: ${RNS_DIR} has uncommitted changes; cannot checkout ${RS_RETICULUM_REF:0:12} to apply overlay" >&2 - echo "Stash/commit sibling changes, or ensure the discovery announce fix is already present." >&2 - exit 1 -fi -current_head="$(git -C "${RNS_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_RETICULUM_REF}" ]]; then - git -C "${RNS_DIR}" fetch origin --tags - git -C "${RNS_DIR}" checkout "${RS_RETICULUM_REF}" -fi - -if overlay_already_present; then - echo "discovery-announce egress overlay already present on rsReticulum @ ${RS_RETICULUM_REF:0:12}" - exit 0 -fi - -apply_patch -echo "applied ${PATCH_FILE} on rsReticulum @ ${RS_RETICULUM_REF:0:12}" +exit 1 diff --git a/scripts/apply-rsReticulum-link-client-nomad.sh b/scripts/apply-rsReticulum-link-client-nomad.sh index fa302481d..5c1355fbb 100755 --- a/scripts/apply-rsReticulum-link-client-nomad.sh +++ b/scripts/apply-rsReticulum-link-client-nomad.sh @@ -2,11 +2,12 @@ # Apply mesh-client rsReticulum LinkClient Nomad overlay for rns-stack local builds. set -euo pipefail -RS_RETICULUM_REF="${RS_RETICULUM_REF:-9928abed269a83ec5a7ef165ff1142d938cad706}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsReticulum-link-client-nomad.patch" -RNS_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum" +RNS_DIR="${RS_RETICULUM_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum}" MESSAGES_RS="${RNS_DIR}/crates/rns-transport/src/messages.rs" if [[ ! -d "${RNS_DIR}/.git" ]]; then @@ -26,36 +27,7 @@ if [[ -f "${MESSAGES_RS}" ]] && grep -q 'RecallDestinationPublicKey' "${MESSAGES exit 0 fi -if ! git -C "${RNS_DIR}" diff --quiet || ! git -C "${RNS_DIR}" diff --cached --quiet; then - echo "warning: ${RNS_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${RNS_DIR}" apply --check "${PATCH_FILE}" - git -C "${RNS_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" - exit 0 -fi - -if git -C "${RNS_DIR}" apply --check "${PATCH_FILE}" 2>&1 | grep -q 'patch does not apply'; then - echo "link-client nomad overlay not needed (already upstream or incompatible with current rsReticulum HEAD)" +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "link-client-nomad"; then exit 0 fi - -echo "link-client nomad patch did not apply on current HEAD; checking out pinned ref ${RS_RETICULUM_REF:0:12}" -current_head="$(git -C "${RNS_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_RETICULUM_REF}" ]]; then - git -C "${RNS_DIR}" fetch origin --tags - git -C "${RNS_DIR}" checkout "${RS_RETICULUM_REF}" -fi - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsReticulum @ ${RS_RETICULUM_REF:0:12}" - exit 0 -fi - -echo "link-client nomad overlay not needed (already upstream or incompatible with pinned rsReticulum ref)" -exit 0 +exit 1 diff --git a/scripts/apply-rsReticulum-packet-tap.sh b/scripts/apply-rsReticulum-packet-tap.sh index 09b3a8efc..bfed9bce3 100755 --- a/scripts/apply-rsReticulum-packet-tap.sh +++ b/scripts/apply-rsReticulum-packet-tap.sh @@ -2,11 +2,12 @@ # Apply mesh-client rsReticulum packet-tap overlay for rns-stack local builds. set -euo pipefail -RS_RETICULUM_REF="${RS_RETICULUM_REF:-9928abed269a83ec5a7ef165ff1142d938cad706}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsReticulum-packet-tap.patch" -RNS_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum" +RNS_DIR="${RS_RETICULUM_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum}" RETICULUM_RS="${RNS_DIR}/crates/rns-runtime/src/reticulum.rs" if [[ ! -d "${RNS_DIR}/.git" ]]; then @@ -25,26 +26,7 @@ if [[ -f "${RETICULUM_RS}" ]] && grep -q 'register_packet_tap' "${RETICULUM_RS}" exit 0 fi -if ! git -C "${RNS_DIR}" diff --quiet || ! git -C "${RNS_DIR}" diff --cached --quiet; then - echo "warning: ${RNS_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${RNS_DIR}" apply --check "${PATCH_FILE}" - git -C "${RNS_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "packet-tap"; then exit 0 fi - -echo "packet-tap patch did not apply on current HEAD; checking out pinned ref ${RS_RETICULUM_REF:0:12}" -current_head="$(git -C "${RNS_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_RETICULUM_REF}" ]]; then - git -C "${RNS_DIR}" fetch origin --tags - git -C "${RNS_DIR}" checkout "${RS_RETICULUM_REF}" -fi - -apply_patch -echo "applied ${PATCH_FILE} on rsReticulum @ ${RS_RETICULUM_REF:0:12}" +exit 1 diff --git a/scripts/apply-rsReticulum-path-medium-slots.sh b/scripts/apply-rsReticulum-path-medium-slots.sh index f69f29777..8d153a2da 100755 --- a/scripts/apply-rsReticulum-path-medium-slots.sh +++ b/scripts/apply-rsReticulum-path-medium-slots.sh @@ -3,11 +3,12 @@ # Keeps up to 3 ranked path slots per destination and RF/network preference. set -euo pipefail -RS_RETICULUM_REF="${RS_RETICULUM_REF:-9928abed269a83ec5a7ef165ff1142d938cad706}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsReticulum-path-medium-slots.patch" -RNS_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum" +RNS_DIR="${RS_RETICULUM_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum}" MARKER="${RNS_DIR}/crates/rns-transport/src/constants.rs" if [[ ! -d "${RNS_DIR}/.git" ]]; then @@ -26,28 +27,9 @@ if [[ -f "${MARKER}" ]] && grep -q 'MAX_PATH_SLOTS' "${MARKER}"; then exit 0 fi -if ! git -C "${RNS_DIR}" diff --quiet || ! git -C "${RNS_DIR}" diff --cached --quiet; then - echo "warning: ${RNS_DIR} has uncommitted changes; checkout may fail or overwrite work" >&2 -fi - -apply_patch() { - git -C "${RNS_DIR}" apply --check "${PATCH_FILE}" - git -C "${RNS_DIR}" apply "${PATCH_FILE}" -} - -if apply_patch 2> /dev/null; then - echo "applied ${PATCH_FILE} on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" - exit 0 -fi - -echo "path-medium-slots patch did not apply on current HEAD; checking out pinned ref ${RS_RETICULUM_REF:0:12}" -current_head="$(git -C "${RNS_DIR}" rev-parse HEAD)" -if [[ "${current_head}" != "${RS_RETICULUM_REF}" ]]; then - git -C "${RNS_DIR}" fetch origin --tags - git -C "${RNS_DIR}" checkout "${RS_RETICULUM_REF}" -fi - # Prerequisites (packet-tap, discovery egress, …) must already be applied by # ensure-rsReticulum-patches.sh / clone-ratspeak-stack.sh before this script. -apply_patch -echo "applied ${PATCH_FILE} on rsReticulum @ ${RS_RETICULUM_REF:0:12}" +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "path-medium-slots"; then + exit 0 +fi +exit 1 diff --git a/scripts/clone-ratspeak-stack.sh b/scripts/clone-ratspeak-stack.sh index d07e2c479..9ba21cb9d 100755 --- a/scripts/clone-ratspeak-stack.sh +++ b/scripts/clone-ratspeak-stack.sh @@ -1,19 +1,31 @@ #!/usr/bin/env bash -# Clone rsReticulum (pinned ref + mesh-client patches), rsLXMF, and rsNomad for rns-stack sidecar builds. +# Clone rsReticulum + rsLXMF + rsNomad (float origin/main by default), +# then apply mesh-client overlays for rns-stack sidecar builds. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" WORKSPACE_ROOT="${WORKSPACE_ROOT:-$(cd "${REPO_ROOT}/.." && pwd)}" +# shellcheck source=lib/ratspeak-overlay-apply-list.sh +source "${SCRIPT_DIR}/lib/ratspeak-overlay-apply-list.sh" RNS_DIR="${WORKSPACE_ROOT}/rsReticulum" LXMF_DIR="${WORKSPACE_ROOT}/rsLXMF" NOMAD_DIR="${WORKSPACE_ROOT}/rsNomad" +# So apply-*.sh targets the same siblings as this script (WORKSPACE_ROOT may differ from ..). +export RS_RETICULUM_DIR="${RNS_DIR}" +export RS_LXMF_DIR="${LXMF_DIR}" + +# Optional bisect / known-good overrides. Unset or empty → float to origin/main. +RS_RETICULUM_REF="${RS_RETICULUM_REF:-}" +RS_LXMF_REF="${RS_LXMF_REF:-}" +RS_NOMAD_REF="${RS_NOMAD_REF:-}" + # Ensure an existing checkout has the correct origin remote and is on the -# pinned ref. If the directory does not exist, clone it first. +# requested ref (or floated origin/main). If the directory does not exist, clone it first. ensure_repo() { - local dir="$1" expected_origin="$2" pinned_ref="$3" label="$4" + local dir="$1" expected_origin="$2" ref_or_empty="$3" label="$4" if [[ ! -d "${dir}/.git" ]]; then git clone "${expected_origin}" "${dir}" fi @@ -23,47 +35,83 @@ ensure_repo() { echo "info: ${label} origin is ${actual_origin}; updating to ${expected_origin}" git -C "${dir}" remote set-url origin "${expected_origin}" fi - if [[ -n "${pinned_ref}" ]]; then - local current_head - current_head="$(git -C "${dir}" rev-parse HEAD 2> /dev/null || true)" - if [[ "${current_head}" != "${pinned_ref}" ]]; then - if [[ -n "$(git -C "${dir}" status --porcelain)" ]]; then - echo "warning: ${dir} has uncommitted changes; skipping pin to ${pinned_ref:0:12}" >&2 - else - git -C "${dir}" fetch --quiet origin "${pinned_ref}" 2> /dev/null \ - || git -C "${dir}" fetch --quiet origin - git -C "${dir}" checkout --quiet "${pinned_ref}" - fi + + git -C "${dir}" fetch --quiet origin + + local target_ref='' target_sha='' current_head + current_head="$(git -C "${dir}" rev-parse HEAD 2> /dev/null || true)" + + if [[ -n "${ref_or_empty}" ]]; then + target_ref="${ref_or_empty}" + git -C "${dir}" fetch --quiet origin "${ref_or_empty}" 2> /dev/null || true + target_sha="$(git -C "${dir}" rev-parse --verify "${ref_or_empty}^{commit}" 2> /dev/null || true)" + if [[ -z "${target_sha}" ]]; then + echo "error: ${label}: cannot resolve pin ${ref_or_empty:0:12}" >&2 + exit 1 + fi + else + # Float to tip of origin/main (fallback: origin/master). + if git -C "${dir}" rev-parse --verify --quiet 'origin/main' > /dev/null; then + target_ref='origin/main' + elif git -C "${dir}" rev-parse --verify --quiet 'origin/master' > /dev/null; then + target_ref='origin/master' + else + echo "error: ${label}: neither origin/main nor origin/master after fetch" >&2 + exit 1 + fi + target_sha="$(git -C "${dir}" rev-parse --verify "${target_ref}^{commit}")" + fi + + if [[ -n "$(git -C "${dir}" status --porcelain)" ]]; then + # Overlays leave siblings dirty after a successful float — allow that when already + # on the target tip. Refuse only when checkout would rewrite a dirty tree. + if [[ "${current_head}" == "${target_sha}" ]]; then + echo "warning: ${dir} has uncommitted changes; already at ${target_ref} (${target_sha:0:12}), skipping checkout" >&2 + return 0 + fi + echo "error: ${dir} has uncommitted changes; refuse to float/pin to ${target_ref} (${target_sha:0:12}) from ${current_head:0:12} (stash or reset, then re-run)" >&2 + git -C "${dir}" status --short >&2 || true + exit 1 + fi + + if [[ "${current_head}" != "${target_sha}" ]]; then + if [[ -n "${ref_or_empty}" ]]; then + git -C "${dir}" checkout --quiet "${ref_or_empty}" + else + git -C "${dir}" checkout --quiet --detach "${target_ref}" fi fi } +echo "Preparing Ratspeak stack (rsReticulum/rsLXMF/rsNomad float origin/main unless RS_*_REF set)..." ensure_repo "${RNS_DIR}" 'https://github.com/ratspeak/rsReticulum.git' \ - '9928abed269a83ec5a7ef165ff1142d938cad706' 'rsReticulum' + "${RS_RETICULUM_REF}" 'rsReticulum' -"${SCRIPT_DIR}/apply-rsReticulum-packet-tap.sh" -"${SCRIPT_DIR}/apply-rsReticulum-auto-beacon-utun.sh" -"${SCRIPT_DIR}/apply-rsReticulum-link-client-nomad.sh" -"${SCRIPT_DIR}/apply-rsReticulum-link-client-proof-budget.sh" -"${SCRIPT_DIR}/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh" -"${SCRIPT_DIR}/apply-rsReticulum-discovery-announce-egress.sh" -"${SCRIPT_DIR}/apply-rsReticulum-path-medium-slots.sh" +apply_ratspeak_rns_overlays "${SCRIPT_DIR}" ensure_repo "${LXMF_DIR}" 'https://github.com/ratspeak/rsLXMF.git' \ - '68ad7c835187c052c763bb28c41b04a655f35c64' 'rsLXMF' + "${RS_LXMF_REF}" 'rsLXMF' -"${SCRIPT_DIR}/apply-rsLXMF-propagation-sync-peering.sh" -"${SCRIPT_DIR}/apply-rsLXMF-propagation-node-policy-setters.sh" -"${SCRIPT_DIR}/apply-rsLXMF-link-delivery-has-pending-to.sh" +apply_ratspeak_lxmf_overlays "${SCRIPT_DIR}" -# Pin rsNomad so CI/release do not float on an unreviewed main tip. -# Override with RS_NOMAD_REF=... or skip with RS_NOMAD_SKIP_PIN=1 (local hardening work). -RS_NOMAD_REF="${RS_NOMAD_REF:-6e3b288fbc6931b1e2633d986cf0d49608d578b7}" +# Float Colorado-Mesh/rsNomad to origin/main (override via RS_NOMAD_REF above). +ensure_repo "${NOMAD_DIR}" 'https://github.com/Colorado-Mesh/rsNomad.git' "${RS_NOMAD_REF}" 'rsNomad' -if [[ "${RS_NOMAD_SKIP_PIN:-}" != "1" ]]; then - ensure_repo "${NOMAD_DIR}" 'https://github.com/Colorado-Mesh/rsNomad.git' "${RS_NOMAD_REF}" 'rsNomad' -else - ensure_repo "${NOMAD_DIR}" 'https://github.com/Colorado-Mesh/rsNomad.git' '' 'rsNomad' +rns_mode='floated origin/main' +lxmf_mode='floated origin/main' +nomad_mode='floated origin/main' +if [[ -n "${RS_RETICULUM_REF}" ]]; then + rns_mode="pinned ${RS_RETICULUM_REF:0:12}" +fi +if [[ -n "${RS_LXMF_REF}" ]]; then + lxmf_mode="pinned ${RS_LXMF_REF:0:12}" +fi +if [[ -n "${RS_NOMAD_REF}" ]]; then + nomad_mode="pinned ${RS_NOMAD_REF:0:12}" fi -echo "Ratspeak stack ready: rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD), rsLXMF @ $(git -C "${LXMF_DIR}" rev-parse --short HEAD), rsNomad @ $(git -C "${NOMAD_DIR}" rev-parse --short HEAD)" +rns_sha="$(git -C "${RNS_DIR}" rev-parse HEAD)" +lxmf_sha="$(git -C "${LXMF_DIR}" rev-parse HEAD)" +nomad_sha="$(git -C "${NOMAD_DIR}" rev-parse HEAD)" +echo "Ratspeak stack ready: rsReticulum @ ${rns_sha:0:12} (${rns_mode}), rsLXMF @ ${lxmf_sha:0:12} (${lxmf_mode}), rsNomad @ ${nomad_sha:0:12} (${nomad_mode})" +echo "Ratspeak stack SHAs (full): rsReticulum=${rns_sha} rsLXMF=${lxmf_sha} rsNomad=${nomad_sha}" diff --git a/scripts/clone-ratspeak-stack.test.mjs b/scripts/clone-ratspeak-stack.test.mjs new file mode 100644 index 000000000..5fcce772a --- /dev/null +++ b/scripts/clone-ratspeak-stack.test.mjs @@ -0,0 +1,53 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +const cloneScriptPath = fileURLToPath(new URL('./clone-ratspeak-stack.sh', import.meta.url)); +const cloneScript = readFileSync(cloneScriptPath, 'utf8'); + +describe('clone-ratspeak-stack.sh float policy', () => { + it('floats rsReticulum and rsLXMF to origin/main by default', () => { + expect(cloneScript).toContain("target_ref='origin/main'"); + expect(cloneScript).toContain('checkout --quiet --detach'); + expect(cloneScript).toMatch(/RS_RETICULUM_REF="\$\{RS_RETICULUM_REF:-\}"/); + expect(cloneScript).toMatch(/RS_LXMF_REF="\$\{RS_LXMF_REF:-\}"/); + expect(cloneScript).toContain('export RS_RETICULUM_DIR='); + expect(cloneScript).toContain('export RS_LXMF_DIR='); + expect(cloneScript).toContain('refuse to float/pin'); + expect(cloneScript).toContain('already at'); + expect(cloneScript).toContain('skipping checkout'); + expect(cloneScript).toContain('Ratspeak stack SHAs (full)'); + expect(cloneScript).toContain('ratspeak-overlay-apply-list.sh'); + expect(cloneScript).not.toMatch(/9928abed269a83ec5a7ef165ff1142d938cad706/); + expect(cloneScript).not.toMatch(/68ad7c835187c052c763bb28c41b04a655f35c64/); + }); + + it('floats rsNomad to origin/main with optional RS_NOMAD_REF pin', () => { + expect(cloneScript).toMatch(/RS_NOMAD_REF="\$\{RS_NOMAD_REF:-\}"/); + expect(cloneScript).toContain('Colorado-Mesh/rsNomad.git'); + expect(cloneScript).toContain("nomad_mode='floated origin/main'"); + expect(cloneScript).not.toContain('6e3b288fbc6931b1e2633d986cf0d49608d578b7'); + expect(cloneScript).not.toContain('RS_NOMAD_SKIP_PIN'); + }); + + it('applies rsReticulum and rsLXMF overlays after checkout via shared list', () => { + expect(cloneScript).toContain('apply_ratspeak_rns_overlays'); + expect(cloneScript).toContain('apply_ratspeak_lxmf_overlays'); + const rnsEnsure = cloneScript.indexOf('ensure_repo "${RNS_DIR}"'); + const rnsApply = cloneScript.indexOf('apply_ratspeak_rns_overlays'); + const lxmfEnsure = cloneScript.indexOf('ensure_repo "${LXMF_DIR}"'); + const lxmfApply = cloneScript.indexOf('apply_ratspeak_lxmf_overlays'); + expect(rnsEnsure).toBeGreaterThanOrEqual(0); + expect(rnsApply).toBeGreaterThan(rnsEnsure); + expect(lxmfEnsure).toBeGreaterThan(rnsApply); + expect(lxmfApply).toBeGreaterThan(lxmfEnsure); + + const listPath = fileURLToPath( + new URL('./lib/ratspeak-overlay-apply-list.sh', import.meta.url), + ); + const listScript = readFileSync(listPath, 'utf8'); + expect(listScript).toContain('apply-rsReticulum-packet-tap.sh'); + expect(listScript).toContain('apply-rsLXMF-propagation-node-policy-setters.sh'); + }); +}); diff --git a/scripts/ensure-rsReticulum-patches.sh b/scripts/ensure-rsReticulum-patches.sh index 26248d86c..19b80748e 100755 --- a/scripts/ensure-rsReticulum-patches.sh +++ b/scripts/ensure-rsReticulum-patches.sh @@ -4,6 +4,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/ratspeak-overlay-apply-list.sh +source "${SCRIPT_DIR}/lib/ratspeak-overlay-apply-list.sh" RNS_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum" LXMF_DIR="$(cd "${REPO_ROOT}/.." && pwd)/rsLXMF" @@ -12,19 +14,11 @@ if [[ ! -d "${RNS_DIR}/.git" ]]; then exit 0 fi -"${SCRIPT_DIR}/apply-rsReticulum-packet-tap.sh" -"${SCRIPT_DIR}/apply-rsReticulum-auto-beacon-utun.sh" -"${SCRIPT_DIR}/apply-rsReticulum-link-client-nomad.sh" -"${SCRIPT_DIR}/apply-rsReticulum-link-client-proof-budget.sh" -"${SCRIPT_DIR}/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh" -"${SCRIPT_DIR}/apply-rsReticulum-discovery-announce-egress.sh" -"${SCRIPT_DIR}/apply-rsReticulum-path-medium-slots.sh" +apply_ratspeak_rns_overlays "${SCRIPT_DIR}" if [[ ! -d "${LXMF_DIR}/.git" ]]; then echo "rsLXMF not found at ${LXMF_DIR}; skipping lxmf overlay apply" exit 0 fi -"${SCRIPT_DIR}/apply-rsLXMF-propagation-sync-peering.sh" -"${SCRIPT_DIR}/apply-rsLXMF-propagation-node-policy-setters.sh" -"${SCRIPT_DIR}/apply-rsLXMF-link-delivery-has-pending-to.sh" +apply_ratspeak_lxmf_overlays "${SCRIPT_DIR}" diff --git a/scripts/lib/apply-ratspeak-overlay.sh b/scripts/lib/apply-ratspeak-overlay.sh new file mode 100644 index 000000000..a2419edbf --- /dev/null +++ b/scripts/lib/apply-ratspeak-overlay.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Shared fail-loud overlay apply helper (keeps git apply stderr on failure). +# shellcheck shell=bash + +# apply_ratspeak_overlay_or_die REPO_DIR PATCH_FILE ERROR_LABEL +# ERROR_LABEL is a short name used in the error line (e.g. "packet-tap"). +apply_ratspeak_overlay_or_die() { + local repo_dir="$1" + local patch_file="$2" + local error_label="$3" + local check_out='' apply_out='' + local short_head + short_head="$(git -C "${repo_dir}" rev-parse --short HEAD 2> /dev/null || echo '?')" + + if ! check_out="$(git -C "${repo_dir}" apply --check "${patch_file}" 2>&1)"; then + echo "error: ${error_label} patch did not apply on $(basename "${repo_dir}") @ ${short_head}" >&2 + if [[ -n "${check_out}" ]]; then + printf '%s\n' "${check_out}" >&2 + fi + echo "error: regenerate overlay (reticulum-sidecar/patches/README.md) or set RS_*_REF to a compatible commit and re-run clone-ratspeak-stack.sh" >&2 + return 1 + fi + if ! apply_out="$(git -C "${repo_dir}" apply "${patch_file}" 2>&1)"; then + echo "error: ${error_label} patch apply failed on $(basename "${repo_dir}") @ ${short_head}" >&2 + if [[ -n "${apply_out}" ]]; then + printf '%s\n' "${apply_out}" >&2 + fi + echo "error: regenerate overlay (reticulum-sidecar/patches/README.md) or set RS_*_REF to a compatible commit and re-run clone-ratspeak-stack.sh" >&2 + return 1 + fi + echo "applied ${patch_file} on $(basename "${repo_dir}") @ ${short_head}" + return 0 +} diff --git a/scripts/lib/ratspeak-overlay-apply-list.sh b/scripts/lib/ratspeak-overlay-apply-list.sh new file mode 100644 index 000000000..d51977dd5 --- /dev/null +++ b/scripts/lib/ratspeak-overlay-apply-list.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Single source of apply-script order for clone-ratspeak-stack / ensure-rsReticulum-patches. +# Keep patch basenames in sync with RATSPEAK_PATCH_ENTRIES in scripts/update.sh. +# shellcheck shell=bash + +RS_RETICULUM_APPLY_SCRIPTS=( + apply-rsReticulum-packet-tap.sh + apply-rsReticulum-auto-beacon-utun.sh + apply-rsReticulum-link-client-nomad.sh + apply-rsReticulum-link-client-proof-budget.sh + apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh + apply-rsReticulum-discovery-announce-egress.sh + apply-rsReticulum-path-medium-slots.sh +) + +RS_LXMF_APPLY_SCRIPTS=( + apply-rsLXMF-propagation-sync-peering.sh + apply-rsLXMF-propagation-node-policy-setters.sh + apply-rsLXMF-link-delivery-has-pending-to.sh +) + +apply_ratspeak_rns_overlays() { + local script_dir="$1" + local s + for s in "${RS_RETICULUM_APPLY_SCRIPTS[@]}"; do + "${script_dir}/${s}" + done +} + +apply_ratspeak_lxmf_overlays() { + local script_dir="$1" + local s + for s in "${RS_LXMF_APPLY_SCRIPTS[@]}"; do + "${script_dir}/${s}" + done +} diff --git a/scripts/lib/ratspeak-overlay-apply-list.test.mjs b/scripts/lib/ratspeak-overlay-apply-list.test.mjs new file mode 100644 index 000000000..862a519ae --- /dev/null +++ b/scripts/lib/ratspeak-overlay-apply-list.test.mjs @@ -0,0 +1,39 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +const listPath = fileURLToPath(new URL('./ratspeak-overlay-apply-list.sh', import.meta.url)); +const helperPath = fileURLToPath(new URL('./apply-ratspeak-overlay.sh', import.meta.url)); +const updatePath = fileURLToPath(new URL('../update.sh', import.meta.url)); + +describe('ratspeak overlay apply list', () => { + it('lists every apply-rs script used by clone/ensure', () => { + const list = readFileSync(listPath, 'utf8'); + expect(list).toContain('RS_RETICULUM_APPLY_SCRIPTS'); + expect(list).toContain('RS_LXMF_APPLY_SCRIPTS'); + expect(list).toContain('apply_ratspeak_rns_overlays'); + expect(list).toContain('apply-rsReticulum-path-medium-slots.sh'); + expect(list).toContain('apply-rsLXMF-link-delivery-has-pending-to.sh'); + }); + + it('keeps apply helper fail-loud with stderr capture', () => { + const helper = readFileSync(helperPath, 'utf8'); + expect(helper).toContain('apply_ratspeak_overlay_or_die'); + expect(helper).toContain('apply --check'); + // Must not swallow git-apply diagnostics (rev-parse may still redirect). + expect(helper).not.toMatch(/git -C .* apply .*2>\s*\/dev\/null/); + }); + + it('patch basenames in update.sh match apply-list overlays', () => { + const list = readFileSync(listPath, 'utf8'); + const update = readFileSync(updatePath, 'utf8'); + const applyNames = [...list.matchAll(/apply-(rs(?:Reticulum|LXMF)-[a-z0-9-]+)\.sh/g)].map( + (m) => `${m[1]}.patch`, + ); + expect(applyNames.length).toBeGreaterThanOrEqual(10); + for (const patch of applyNames) { + expect(update).toContain(patch); + } + }); +}); diff --git a/scripts/update.sh b/scripts/update.sh index 7b22ecea4..389d9af2e 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -203,7 +203,8 @@ process.stdin.on("end", () => { } # Warn when local Ratspeak overlays may be obsolete after upstream merges. -# Keep in sync with reticulum-sidecar/patches/*.patch and patches/README.md. +# Keep patch basenames in sync with scripts/lib/ratspeak-overlay-apply-list.sh +# and reticulum-sidecar/patches/*.patch / patches/README.md. check_ratspeak_patches() { # Format: "patch-basename|github-owner/repo|pr-number-or-empty|display-label|review-url" local RATSPEAK_PATCH_ENTRIES=( @@ -320,6 +321,191 @@ check_ratspeak_patches() { fi } +# GET GitHub API path (gh preferred, curl fallback). Empty on failure. +# Warns once per update run when the response is a rate-limit / abuse payload. +github_api_get() { + local api_path="$1" + local body='' + if command -v gh > /dev/null 2>&1; then + body="$(gh api "${api_path}" 2> /dev/null || true)" + elif command -v curl > /dev/null 2>&1; then + body="$( + curl -fsSL \ + -H 'Accept: application/vnd.github+json' \ + -H 'User-Agent: mesh-client-update' \ + "https://api.github.com/${api_path}" 2> /dev/null || true + )" + else + echo '' + return 0 + fi + if [[ -n "${body}" ]] && printf '%s' "${body}" | grep -qiE 'rate limit exceeded|API rate limit|secondary rate limit'; then + if [[ "${GITHUB_API_RATE_LIMIT_WARNED:-0}" != "1" ]]; then + echo -e " ${YELLOW}GitHub API rate limit:${NC} further Ratspeak upstream checks may be incomplete (retry later or use authenticated gh)." + GITHUB_API_RATE_LIMIT_WARNED=1 + HAS_WARNING=1 + fi + echo '' + return 0 + fi + printf '%s' "${body}" +} + +# Latest release summary: "tag|published_at|first_body_line" or empty. +github_latest_release_summary() { + local repo="$1" + local json + json="$(github_api_get "repos/${repo}/releases/latest")" + if [ -z "${json}" ]; then + echo '' + return 0 + fi + printf '%s' "${json}" | node -e ' +let s = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (c) => { s += c; }); +process.stdin.on("end", () => { + try { + const j = JSON.parse(s); + if (j.message === "Not Found" || (!j.tag_name && !j.name)) { + process.stdout.write(""); + return; + } + const tag = String(j.tag_name || j.name || "").replace(/\|/g, "/"); + const published = String(j.published_at || "").slice(0, 10); + const body = String(j.body || "").split(/\r?\n/).find((l) => l.trim()) || ""; + const first = body.replace(/\|/g, "/").slice(0, 120); + process.stdout.write(`${tag}|${published}|${first}`); + } catch { + process.stdout.write(""); + } +}); +' 2> /dev/null || echo '' +} + +# Curated release watch + known org repos (keep in sync when adopting new ratspeak libs). +# Format: "owner/repo|stub-kind-or-empty|display-label" +# stub-kind: voice|games → warn while mesh-client still has sidecar stubs only. +RATSPEAK_RELEASE_WATCH_ENTRIES=( + 'ratspeak/rsLXST|voice|rsLXST voice (sidecar stub)' + 'ratspeak/lrgp-rs|games|lrgp-rs games (sidecar stub)' + 'ratspeak/Ratspeak||Ratspeak client (reference)' + 'ratspeak/LXMFace||LXMFace identicons (vendored in renderer)' +) + +RATSPEAK_KNOWN_ORG_REPOS=( + '.github' + 'C6-Reticulum-ASM' + 'LXMFace' + 'Ratspeak' + 'lrgp-py' + 'lrgp-rs' + 'microReticulum' + 'ratkey' + 'rathole' + 'ratspeak-docs' + 'ratspeak-website' + 'revanity-go' + 'rsCardputer' + 'rsDeck' + 'rsLXMF' + 'rsLXST' + 'rsPager' + 'rsReticulum' +) + +print_ratspeak_upstream_catalog() { + local entry + echo 'RATSPEAK_RELEASE_WATCH_ENTRIES:' + for entry in "${RATSPEAK_RELEASE_WATCH_ENTRIES[@]}"; do + echo " ${entry}" + done + echo 'RATSPEAK_KNOWN_ORG_REPOS:' + for entry in "${RATSPEAK_KNOWN_ORG_REPOS[@]}"; do + echo " ${entry}" + done +} + +# Surface new Ratspeak library releases and brand-new org repos (stack floats via clone). +check_ratspeak_upstream() { + local has_upstream_warning=0 + local entry repo stub label summary tag published first url + + echo '' + echo 'Checking Ratspeak upstream releases and new org repos...' + echo ' (rsReticulum/rsLXMF float to origin/main via clone-ratspeak-stack.sh)' + + for entry in "${RATSPEAK_RELEASE_WATCH_ENTRIES[@]}"; do + IFS='|' read -r repo stub label <<< "${entry}" + url="https://github.com/${repo}/releases" + summary="$(github_latest_release_summary "${repo}")" + if [ -z "${summary}" ]; then + echo " ${label}: no GitHub release (or query failed) — ${url}" + continue + fi + IFS='|' read -r tag published first <<< "${summary}" + echo " ${label}: ${tag} (${published}) — ${first}" + if [ "${stub}" = 'voice' ] || [ "${stub}" = 'games' ]; then + warn_box "${label}" "sidecar stub" "${tag} available" "${url}" + echo " Reason tracked: mesh-client still stubs this feature; review integrating ${repo} @ ${tag}" + has_upstream_warning=1 + HAS_WARNING=1 + fi + done + + local repos_json + repos_json="$(github_api_get 'orgs/ratspeak/repos?per_page=100&sort=created&direction=desc')" + if [ -z "${repos_json}" ]; then + echo ' Could not list ratspeak org repos (install gh or check network) — skip new-repo scan.' + else + local new_repos + new_repos="$( + printf '%s' "${repos_json}" | node -e ' +const known = new Set(process.argv.slice(2)); +let s = ""; +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (c) => { s += c; }); +process.stdin.on("end", () => { + try { + const repos = JSON.parse(s); + if (!Array.isArray(repos)) return; + const cutoff = Date.now() - 90 * 24 * 60 * 60 * 1000; + for (const r of repos) { + const name = String(r.name || ""); + const created = Date.parse(r.created_at || ""); + if (!name || !Number.isFinite(created) || created < cutoff) continue; + if (known.has(name)) continue; + process.stdout.write(`${name}\t${String(r.created_at || "").slice(0, 10)}\t${r.html_url || ""}\n`); + } + } catch { + // ignore parse errors + } +}); +' "${RATSPEAK_KNOWN_ORG_REPOS[@]}" + )" + if [ -n "${new_repos}" ]; then + while IFS=$'\t' read -r name created url; do + [ -n "${name}" ] || continue + warn_box "ratspeak/${name} (new org repo)" "unknown" "created ${created}" "${url}" + echo " Reason tracked: created within ~90 days and not in RATSPEAK_KNOWN_ORG_REPOS — review for mesh-client use" + has_upstream_warning=1 + HAS_WARNING=1 + done <<< "${new_repos}" + else + echo ' No unfamiliar ratspeak org repos created in the last ~90 days.' + fi + fi + + if [ "${has_upstream_warning}" -eq 0 ]; then + echo ' Ratspeak upstream watch complete (no stub/new-repo warnings).' + fi +} + +if [ "${UPDATE_SH_TEST_HOOK:-}" = 'upstream-catalog-only' ]; then + print_ratspeak_upstream_catalog + exit 0 +fi + # --- Guard: must be project root --- if [ ! -f "${LOCKFILE}" ]; then echo "Error: ${LOCKFILE} not found. Run this script from the project root." >&2 @@ -411,6 +597,7 @@ for i in "${!KEYS[@]}"; do done check_ratspeak_patches +check_ratspeak_upstream if [ "${HAS_WARNING}" -eq 0 ]; then echo 'No updates to watched packages — safe to proceed.' diff --git a/scripts/update.test.mjs b/scripts/update.test.mjs index 02173cbe8..b72eea001 100644 --- a/scripts/update.test.mjs +++ b/scripts/update.test.mjs @@ -93,6 +93,31 @@ describe('update.sh Reticulum stack functionality check', () => { expect(result.stderr).toContain('Usage: scripts/update.sh [--clean-target]'); }); + it('prints Ratspeak upstream catalog (upstream-catalog-only)', () => { + const result = runUpdate([], { UPDATE_SH_TEST_HOOK: 'upstream-catalog-only' }); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain('RATSPEAK_RELEASE_WATCH_ENTRIES:'); + expect(result.stdout).toContain('ratspeak/rsLXST|voice|'); + expect(result.stdout).toContain('ratspeak/lrgp-rs|games|'); + expect(result.stdout).toContain('ratspeak/Ratspeak||'); + expect(result.stdout).toContain('ratspeak/LXMFace||'); + expect(result.stdout).toContain('RATSPEAK_KNOWN_ORG_REPOS:'); + expect(result.stdout).toContain(' rsReticulum'); + expect(result.stdout).toContain(' rsLXMF'); + expect(result.stdout).toContain(' rsLXST'); + expect(result.stdout).toContain(' lrgp-rs'); + }); + + it('wires check_ratspeak_upstream after overlay PR checks', () => { + expect(updateScript).toContain('check_ratspeak_upstream()'); + expect(updateScript).toContain('RATSPEAK_RELEASE_WATCH_ENTRIES'); + expect(updateScript).toContain('RATSPEAK_KNOWN_ORG_REPOS'); + const patchesCall = updateScript.lastIndexOf('\ncheck_ratspeak_patches\n'); + const upstreamCall = updateScript.lastIndexOf('\ncheck_ratspeak_upstream\n'); + expect(patchesCall).toBeGreaterThanOrEqual(0); + expect(upstreamCall).toBeGreaterThan(patchesCall); + }); + it('runs cargo clean after a successful rebuild when CLEAN_SIDECAR_TARGET=1', () => { const fixture = prepareRebuildFixture({ buildExit: 0 }); const result = runUpdate( diff --git a/src/renderer/components/ReticulumPeerDetailModal.tsx b/src/renderer/components/ReticulumPeerDetailModal.tsx index 49ee26d78..93b4e949e 100644 --- a/src/renderer/components/ReticulumPeerDetailModal.tsx +++ b/src/renderer/components/ReticulumPeerDetailModal.tsx @@ -400,7 +400,12 @@ export default function ReticulumPeerDetailModal({ ) : (
- +

- + {displayLabel} {verified ? ( {t('networkPanel.reticulumPnHosting.authRequired')} - -

+

+ {t('networkPanel.reticulumPnHosting.enforceUnavailableTip')} +